2022-06-29 02:18:59 -07:00
|
|
|
use anchor_lang::prelude::*;
|
2022-07-04 04:13:11 -07:00
|
|
|
use anchor_spl::token;
|
2022-06-29 02:18:59 -07:00
|
|
|
use fixed::types::I80F48;
|
|
|
|
|
|
|
|
use crate::accounts_zerocopy::*;
|
|
|
|
use crate::error::*;
|
2022-12-08 04:12:43 -08:00
|
|
|
use crate::health::*;
|
2022-06-29 02:18:59 -07:00
|
|
|
use crate::state::*;
|
|
|
|
|
2023-02-14 23:42:07 -08:00
|
|
|
use crate::accounts_ix::*;
|
2022-08-19 18:50:54 -07:00
|
|
|
use crate::logs::{
|
2022-10-07 12:12:55 -07:00
|
|
|
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog,
|
2022-08-19 18:50:54 -07:00
|
|
|
WithdrawLoanOriginationFeeLog,
|
|
|
|
};
|
|
|
|
|
2022-09-14 04:25:11 -07:00
|
|
|
pub fn token_liq_bankruptcy(
|
|
|
|
ctx: Context<TokenLiqBankruptcy>,
|
2022-07-04 04:13:11 -07:00
|
|
|
max_liab_transfer: I80F48,
|
2022-06-29 02:18:59 -07:00
|
|
|
) -> Result<()> {
|
2022-07-04 04:13:11 -07:00
|
|
|
let group = ctx.accounts.group.load()?;
|
2022-06-29 02:18:59 -07:00
|
|
|
let group_pk = &ctx.accounts.group.key();
|
|
|
|
|
|
|
|
// split remaining accounts into banks and health
|
|
|
|
let liab_mint_info = ctx.accounts.liab_mint_info.load()?;
|
2022-08-16 00:51:01 -07:00
|
|
|
let liab_token_index = liab_mint_info.token_index;
|
|
|
|
let (bank_ais, health_ais) = &ctx.remaining_accounts.split_at(liab_mint_info.num_banks());
|
|
|
|
liab_mint_info.verify_banks_ais(bank_ais)?;
|
2022-06-29 02:18:59 -07:00
|
|
|
|
2023-02-01 07:15:58 -08:00
|
|
|
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
|
|
|
|
|
2022-12-29 02:48:46 -08:00
|
|
|
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
2022-08-11 04:15:07 -07:00
|
|
|
// account constraint #1
|
2022-07-25 07:07:53 -07:00
|
|
|
require!(
|
|
|
|
liqor
|
|
|
|
.fixed
|
|
|
|
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
|
|
|
MangoError::SomeError
|
|
|
|
);
|
2023-01-12 00:07:13 -08:00
|
|
|
require_msg_typed!(
|
|
|
|
!liqor.fixed.being_liquidated(),
|
|
|
|
MangoError::BeingLiquidated,
|
|
|
|
"liqor account"
|
|
|
|
);
|
2022-08-11 09:15:47 -07:00
|
|
|
|
|
|
|
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
2022-06-29 02:18:59 -07:00
|
|
|
|
2022-12-29 02:48:46 -08:00
|
|
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
2022-08-11 09:15:47 -07:00
|
|
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
|
|
|
.context("create liqee health cache")?;
|
2023-01-12 00:07:13 -08:00
|
|
|
liqee_health_cache.require_after_phase2_liquidation()?;
|
2022-08-11 09:15:47 -07:00
|
|
|
liqee.fixed.set_being_liquidated(true);
|
2022-06-29 02:18:59 -07:00
|
|
|
|
2023-03-16 03:23:45 -07:00
|
|
|
let liab_is_insurance_token = liab_token_index == INSURANCE_TOKEN_INDEX;
|
2023-02-10 00:00:36 -08:00
|
|
|
let (liab_bank, liab_oracle_price, opt_quote_bank_and_price) =
|
2023-03-16 03:23:45 -07:00
|
|
|
account_retriever.banks_mut_and_oracles(liab_token_index, INSURANCE_TOKEN_INDEX)?;
|
|
|
|
assert!(liab_is_insurance_token == opt_quote_bank_and_price.is_none());
|
|
|
|
|
2022-08-19 18:50:54 -07:00
|
|
|
let mut liab_deposit_index = liab_bank.deposit_index;
|
|
|
|
let liab_borrow_index = liab_bank.borrow_index;
|
2022-08-18 04:45:31 -07:00
|
|
|
let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?;
|
2022-08-30 04:46:39 -07:00
|
|
|
let initial_liab_native = liqee_liab.native(liab_bank);
|
2023-05-17 06:50:05 -07:00
|
|
|
|
|
|
|
let liqee_health_token_balances =
|
|
|
|
liqee_health_cache.effective_token_balances(HealthType::LiquidationEnd);
|
|
|
|
let liqee_liab_health_balance = liqee_health_token_balances
|
|
|
|
[liqee_health_cache.token_info_index(liab_token_index)?]
|
|
|
|
.spot_and_perp;
|
|
|
|
|
|
|
|
// Allow token bankruptcy only while the spot position and health token position are both negative.
|
|
|
|
// In particular, a very negative perp hupnl does not allow token bankruptcy to happen,
|
|
|
|
// and if the perp hupnl is positive, we need to liquidate that before dealing with token
|
|
|
|
// bankruptcy!
|
|
|
|
let mut remaining_liab_loss = (-initial_liab_native).min(-liqee_liab_health_balance);
|
2022-07-04 04:13:11 -07:00
|
|
|
require_gt!(remaining_liab_loss, I80F48::ZERO);
|
|
|
|
|
2023-03-16 03:23:45 -07:00
|
|
|
// We pay for the liab token in quote. Example: SOL is at $20 and USDC is at $2, then for a liab
|
|
|
|
// of 3 SOL, we'd pay 3 * 20 / 2 * (1+fee) = 30 * (1+fee) USDC.
|
|
|
|
let liab_to_quote_with_fee =
|
|
|
|
if let Some((_quote_bank, quote_price)) = opt_quote_bank_and_price.as_ref() {
|
|
|
|
liab_oracle_price * (I80F48::ONE + liab_bank.liquidation_fee) / quote_price
|
|
|
|
} else {
|
|
|
|
I80F48::ONE
|
|
|
|
};
|
2022-07-04 04:13:11 -07:00
|
|
|
|
|
|
|
let liab_transfer_unrounded = remaining_liab_loss.min(max_liab_transfer);
|
2022-08-03 04:50:49 -07:00
|
|
|
|
2022-08-03 06:11:19 -07:00
|
|
|
let insurance_vault_amount = if liab_mint_info.elligible_for_group_insurance_fund() {
|
|
|
|
ctx.accounts.insurance_vault.amount
|
2022-08-03 04:50:49 -07:00
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
2022-08-03 06:11:19 -07:00
|
|
|
|
2023-03-16 03:23:45 -07:00
|
|
|
let insurance_transfer = (liab_transfer_unrounded * liab_to_quote_with_fee)
|
2023-02-24 02:56:33 -08:00
|
|
|
.ceil()
|
|
|
|
.to_num::<u64>()
|
2022-08-03 06:11:19 -07:00
|
|
|
.min(insurance_vault_amount);
|
|
|
|
|
|
|
|
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
|
2022-07-04 04:13:11 -07:00
|
|
|
|
|
|
|
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
|
|
|
|
|
|
|
// AUDIT: v3 does this, but it seems bad, because it can make liab_transfer
|
|
|
|
// exceed max_liab_transfer due to the ceil() above! Otoh, not doing it would allow
|
|
|
|
// liquidators to exploit the insurance fund for 1 native token each call.
|
2023-03-16 03:23:45 -07:00
|
|
|
let liab_transfer = insurance_transfer_i80f48 / liab_to_quote_with_fee;
|
2022-07-04 04:13:11 -07:00
|
|
|
|
2022-11-25 04:45:17 -08:00
|
|
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
|
|
|
2022-07-04 04:13:11 -07:00
|
|
|
let mut liqee_liab_active = true;
|
|
|
|
if insurance_transfer > 0 {
|
2022-08-15 06:53:05 -07:00
|
|
|
// liqee gets liab assets (enable dusting to prevent a case where the position is brought
|
|
|
|
// to +I80F48::DELTA)
|
2022-11-25 04:45:17 -08:00
|
|
|
liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab, liab_transfer, now_ts)?;
|
2023-05-17 06:50:05 -07:00
|
|
|
// update correctly even if dusting happened
|
|
|
|
remaining_liab_loss -= liqee_liab.native(liab_bank) - initial_liab_native;
|
2022-07-04 04:13:11 -07:00
|
|
|
|
|
|
|
// move insurance assets into quote bank
|
|
|
|
let group_seeds = group_seeds!(group);
|
|
|
|
token::transfer(
|
|
|
|
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
|
|
|
insurance_transfer,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
// move quote assets into liqor and withdraw liab assets
|
2022-09-23 10:42:43 -07:00
|
|
|
if let Some((quote_bank, _)) = opt_quote_bank_and_price {
|
2022-08-11 04:15:07 -07:00
|
|
|
// account constraint #2 a)
|
2022-07-04 04:13:11 -07:00
|
|
|
require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key());
|
|
|
|
require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint);
|
|
|
|
|
2022-08-19 18:50:54 -07:00
|
|
|
let quote_deposit_index = quote_bank.deposit_index;
|
|
|
|
let quote_borrow_index = quote_bank.borrow_index;
|
|
|
|
|
2022-07-04 04:13:11 -07:00
|
|
|
// credit the liqor
|
|
|
|
let (liqor_quote, liqor_quote_raw_token_index, _) =
|
2023-03-16 03:23:45 -07:00
|
|
|
liqor.ensure_token_position(INSURANCE_TOKEN_INDEX)?;
|
2022-11-25 04:45:17 -08:00
|
|
|
let liqor_quote_active =
|
|
|
|
quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
|
2022-09-28 23:04:33 -07:00
|
|
|
|
|
|
|
// liqor quote
|
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
mango_account: ctx.accounts.liqor.key(),
|
2023-03-16 03:23:45 -07:00
|
|
|
token_index: INSURANCE_TOKEN_INDEX,
|
2022-09-28 23:04:33 -07:00
|
|
|
indexed_position: liqor_quote.indexed_position.to_bits(),
|
|
|
|
deposit_index: quote_deposit_index.to_bits(),
|
|
|
|
borrow_index: quote_borrow_index.to_bits(),
|
|
|
|
});
|
2022-07-04 04:13:11 -07:00
|
|
|
|
|
|
|
// transfer liab from liqee to liqor
|
|
|
|
let (liqor_liab, liqor_liab_raw_token_index, _) =
|
2022-08-18 04:45:31 -07:00
|
|
|
liqor.ensure_token_position(liab_token_index)?;
|
2023-04-12 23:56:33 -07:00
|
|
|
let (liqor_liab_active, loan_origination_fee) =
|
|
|
|
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?;
|
2022-07-04 04:13:11 -07:00
|
|
|
|
2022-09-28 23:04:33 -07:00
|
|
|
// liqor liab
|
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
mango_account: ctx.accounts.liqor.key(),
|
|
|
|
token_index: liab_token_index,
|
|
|
|
indexed_position: liqor_liab.indexed_position.to_bits(),
|
|
|
|
deposit_index: liab_deposit_index.to_bits(),
|
|
|
|
borrow_index: liab_borrow_index.to_bits(),
|
|
|
|
});
|
|
|
|
|
2022-07-04 04:13:11 -07:00
|
|
|
// Check liqor's health
|
2022-08-22 02:02:01 -07:00
|
|
|
if !liqor.fixed.is_in_health_region() {
|
|
|
|
let liqor_health =
|
|
|
|
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
|
|
|
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
|
|
|
}
|
2022-07-04 04:13:11 -07:00
|
|
|
|
2022-08-19 18:50:54 -07:00
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-04 04:13:11 -07:00
|
|
|
if !liqor_quote_active {
|
2022-09-28 23:04:33 -07:00
|
|
|
liqor.deactivate_token_position_and_log(
|
|
|
|
liqor_quote_raw_token_index,
|
|
|
|
ctx.accounts.liqor.key(),
|
|
|
|
);
|
2022-07-04 04:13:11 -07:00
|
|
|
}
|
|
|
|
if !liqor_liab_active {
|
2022-09-28 23:04:33 -07:00
|
|
|
liqor.deactivate_token_position_and_log(
|
|
|
|
liqor_liab_raw_token_index,
|
|
|
|
ctx.accounts.liqor.key(),
|
|
|
|
);
|
2022-07-04 04:13:11 -07:00
|
|
|
}
|
|
|
|
} else {
|
2023-03-16 03:23:45 -07:00
|
|
|
// For liab_token_index == INSURANCE_TOKEN_INDEX: the insurance fund deposits directly into liqee,
|
2022-07-06 00:24:25 -07:00
|
|
|
// without a fee or the liqor being involved
|
2022-08-11 04:15:07 -07:00
|
|
|
// account constraint #2 b)
|
|
|
|
require_keys_eq!(liab_bank.vault, ctx.accounts.quote_vault.key());
|
2023-03-16 03:23:45 -07:00
|
|
|
require_eq!(liab_token_index, INSURANCE_TOKEN_INDEX);
|
|
|
|
require_eq!(liab_to_quote_with_fee, I80F48::ONE);
|
2022-07-04 04:13:11 -07:00
|
|
|
require_eq!(insurance_transfer_i80f48, liab_transfer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
drop(account_retriever);
|
|
|
|
|
2022-08-15 06:53:05 -07:00
|
|
|
// Socialize loss if there's more loss and noone else could use the
|
|
|
|
// insurance fund to cover it.
|
2022-08-19 18:50:54 -07:00
|
|
|
let mut socialized_loss = I80F48::ZERO;
|
2023-01-12 00:07:13 -08:00
|
|
|
let starting_deposit_index = liab_deposit_index;
|
2022-08-03 06:11:19 -07:00
|
|
|
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
|
2022-07-04 04:13:11 -07:00
|
|
|
// find the total deposits
|
|
|
|
let mut indexed_total_deposits = I80F48::ZERO;
|
|
|
|
for bank_ai in bank_ais.iter() {
|
|
|
|
let bank = bank_ai.load::<Bank>()?;
|
2023-02-24 02:56:33 -08:00
|
|
|
indexed_total_deposits += bank.indexed_deposits;
|
2022-07-04 04:13:11 -07:00
|
|
|
}
|
|
|
|
|
2022-07-06 00:24:25 -07:00
|
|
|
// This is the solution to:
|
|
|
|
// total_indexed_deposits * (deposit_index - new_deposit_index) = remaining_liab_loss
|
|
|
|
// AUDIT: Could it happen that remaining_liab_loss > total_indexed_deposits * deposit_index?
|
|
|
|
// Probably not.
|
2023-02-24 02:56:33 -08:00
|
|
|
let new_deposit_index = liab_deposit_index - remaining_liab_loss / indexed_total_deposits;
|
2022-08-19 18:50:54 -07:00
|
|
|
liab_deposit_index = new_deposit_index;
|
|
|
|
socialized_loss = remaining_liab_loss;
|
2022-07-04 04:13:11 -07:00
|
|
|
|
|
|
|
let mut amount_to_credit = remaining_liab_loss;
|
|
|
|
for bank_ai in bank_ais.iter() {
|
|
|
|
let mut bank = bank_ai.load_mut::<Bank>()?;
|
|
|
|
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());
|
|
|
|
if amount_for_bank.is_positive() {
|
2022-08-15 06:53:05 -07:00
|
|
|
// enable dusting, because each deposit() is allowed to round up. thus multiple deposit
|
|
|
|
// could bring the total position slightly above zero otherwise
|
2022-11-25 04:45:17 -08:00
|
|
|
liqee_liab_active =
|
|
|
|
bank.deposit_with_dusting(liqee_liab, amount_for_bank, now_ts)?;
|
2023-02-24 02:56:33 -08:00
|
|
|
amount_to_credit -= amount_for_bank;
|
2022-08-15 06:53:05 -07:00
|
|
|
if amount_to_credit <= 0 {
|
2022-07-04 04:13:11 -07:00
|
|
|
break;
|
|
|
|
}
|
2022-06-29 02:18:59 -07:00
|
|
|
}
|
|
|
|
}
|
2022-08-15 06:53:05 -07:00
|
|
|
|
|
|
|
// socialized loss always brings the position to zero
|
|
|
|
require_eq!(liqee_liab.indexed_position, I80F48::ZERO);
|
2022-06-29 02:18:59 -07:00
|
|
|
}
|
|
|
|
|
2022-08-19 18:50:54 -07:00
|
|
|
// 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(),
|
|
|
|
});
|
|
|
|
|
2022-08-11 09:15:47 -07:00
|
|
|
let liab_bank = bank_ais[0].load::<Bank>()?;
|
|
|
|
let end_liab_native = liqee_liab.native(&liab_bank);
|
2023-02-24 02:56:33 -08:00
|
|
|
liqee_health_cache.adjust_token_balance(&liab_bank, end_liab_native - initial_liab_native)?;
|
2022-08-11 09:15:47 -07:00
|
|
|
|
|
|
|
// Check liqee health again
|
2023-02-10 00:00:36 -08:00
|
|
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
2022-08-11 09:15:47 -07:00
|
|
|
liqee
|
|
|
|
.fixed
|
2023-02-10 00:00:36 -08:00
|
|
|
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
2022-06-29 02:18:59 -07:00
|
|
|
|
2022-07-04 04:13:11 -07:00
|
|
|
if !liqee_liab_active {
|
2022-09-28 23:04:33 -07:00
|
|
|
liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key());
|
2022-07-04 04:13:11 -07:00
|
|
|
}
|
2022-06-29 02:18:59 -07:00
|
|
|
|
2022-10-07 12:12:55 -07:00
|
|
|
emit!(TokenLiqBankruptcyLog {
|
2022-08-19 18:50:54 -07:00
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
liqee: ctx.accounts.liqee.key(),
|
|
|
|
liqor: ctx.accounts.liqor.key(),
|
2022-08-30 04:46:39 -07:00
|
|
|
liab_token_index,
|
2022-08-19 18:50:54 -07:00
|
|
|
initial_liab_native: initial_liab_native.to_bits(),
|
2023-02-10 00:00:36 -08:00
|
|
|
liab_price: liab_oracle_price.to_bits(),
|
2023-03-16 03:23:45 -07:00
|
|
|
insurance_token_index: INSURANCE_TOKEN_INDEX,
|
2022-08-19 18:50:54 -07:00
|
|
|
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
2023-01-12 00:07:13 -08:00
|
|
|
socialized_loss: socialized_loss.to_bits(),
|
|
|
|
starting_liab_deposit_index: starting_deposit_index.to_bits(),
|
|
|
|
ending_liab_deposit_index: liab_deposit_index.to_bits()
|
2022-08-19 18:50:54 -07:00
|
|
|
});
|
|
|
|
|
2022-06-29 02:18:59 -07:00
|
|
|
Ok(())
|
|
|
|
}
|