mango-v4/programs/mango-v4/src/instructions/token_liq_with_token.rs

291 lines
11 KiB
Rust

use anchor_lang::prelude::*;
use fixed::types::I80F48;
use std::cmp::min;
use crate::error::*;
use crate::health::*;
use crate::logs::{
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog,
WithdrawLoanOriginationFeeLog,
};
use crate::state::*;
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct TokenLiqWithToken<'info> {
#[account(
constraint = group.load()?.is_operational() @ MangoError::GroupIsHalted
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_operational() @ MangoError::AccountIsFrozen
// liqor_owner is checked at #1
)]
pub liqor: AccountLoader<'info, MangoAccountFixed>,
pub liqor_owner: Signer<'info>,
#[account(
mut,
has_one = group,
constraint = liqee.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub liqee: AccountLoader<'info, MangoAccountFixed>,
}
pub fn token_liq_with_token(
ctx: Context<TokenLiqWithToken>,
asset_token_index: TokenIndex,
liab_token_index: TokenIndex,
max_liab_transfer: I80F48,
) -> Result<()> {
let group_pk = &ctx.accounts.group.key();
require!(asset_token_index != liab_token_index, MangoError::SomeError);
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
// account constraint #1
require!(
liqor
.fixed
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require_msg_typed!(
!liqor.fixed.being_liquidated(),
MangoError::BeingLiquidated,
"liqor account"
);
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
// Initial liqee health check
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
.context("create liqee health cache")?;
let init_health = liqee_health_cache.health(HealthType::Init);
liqee_health_cache.require_after_phase1_liquidation()?;
if !liqee.check_liquidatable(&liqee_health_cache)? {
return Ok(());
}
//
// 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);
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);
// Liquidation fees work by giving the liqor more assets than the oracle price would
// indicate. Specifically we choose
// assets =
// liabs * liab_price/asset_price * (1 + liab_liq_fee + asset_liq_fee)
// Which means that we use a increased liab price and reduced asset price for the conversion.
// It would be more fully correct to use (1+liab_liq_fee)*(1+asset_liq_fee), but for small
// fee amounts that is nearly identical.
// For simplicity we write
// assets = liabs * liab_price / asset_price * fee_factor
// assets = liabs * liab_price_adjusted / asset_price
let fee_factor = cm!(I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee);
let liab_price_adjusted = cm!(liab_price * fee_factor);
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);
// 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);
// 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,
Clock::get()?.unix_timestamp.try_into().unwrap(),
)?;
let liqee_liab_indexed_position = liqee_liab_position.indexed_position;
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(
liqor_liab_position,
liab_transfer,
Clock::get()?.unix_timestamp.try_into().unwrap(),
liab_price,
)?;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active = asset_bank.deposit(
liqor_asset_position,
asset_transfer,
Clock::get()?.unix_timestamp.try_into().unwrap(),
)?;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(
liqee_asset_position,
asset_transfer,
Clock::get()?.unix_timestamp.try_into().unwrap(),
asset_price,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
// Update the health cache
liqee_health_cache
.adjust_token_balance(liab_bank, cm!(liqee_liab_native_after - liqee_liab_native))?;
liqee_health_cache.adjust_token_balance(
asset_bank,
cm!(liqee_assets_native_after - liqee_asset_native),
)?;
msg!(
"liquidated {} liab for {} asset",
liab_transfer,
asset_transfer
);
// 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_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.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_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.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_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.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_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.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::LiqTokenWithToken
});
}
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
if !liqee_asset_active {
liqee.deactivate_token_position_and_log(liqee_asset_raw_index, ctx.accounts.liqee.key());
}
if !liqee_liab_active {
liqee.deactivate_token_position_and_log(liqee_liab_raw_index, ctx.accounts.liqee.key());
}
if !liqor_asset_active {
liqor.deactivate_token_position_and_log(liqor_asset_raw_index, ctx.accounts.liqor.key());
}
if !liqor_liab_active {
liqor.deactivate_token_position_and_log(liqor_liab_raw_index, ctx.accounts.liqor.key())
}
// Check liqee health again
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_init_health);
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
emit!(TokenLiqWithTokenLog {
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_phase2_liquidatable() & liqee_init_health.is_negative()
});
Ok(())
}