291 lines
11 KiB
Rust
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(())
|
|
}
|