mango-v4/liquidator/src/liquidate.rs

357 lines
13 KiB
Rust
Raw Normal View History

use std::time::Duration;
use crate::account_shared_data::KeyedAccountSharedData;
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
use mango_v4::state::{
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
2022-08-31 05:41:04 -07:00
Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX,
};
2022-08-31 05:41:04 -07:00
use itertools::Itertools;
use rand::seq::SliceRandom;
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
2022-08-05 04:50:44 -07:00
pub struct Config {
pub min_health_ratio: f64,
pub refresh_timeout: Duration,
2022-08-05 04:50:44 -07:00
}
pub fn new_health_cache_(
context: &MangoGroupContext,
account_fetcher: &chain_data::AccountFetcher,
account: &MangoAccountValue,
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count();
2022-08-04 08:01:00 -07:00
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
let accounts = metas
.iter()
.map(|meta| {
Ok(KeyedAccountSharedData::new(
meta.pubkey,
account_fetcher.fetch_raw(&meta.pubkey)?,
))
})
.collect::<anyhow::Result<Vec<_>>>()?;
let retriever = FixedOrderAccountRetriever {
ais: accounts,
n_banks: active_token_len,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len,
};
new_health_cache(&account.borrow(), &retriever).context("make health cache")
}
pub fn jupiter_market_can_buy(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
) -> bool {
if token == quote_token {
return true;
}
let token_mint = mango_client.context.token(token).mint_info.mint;
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
// Consider a market alive if we can swap $10 worth at 1% slippage
// TODO: configurable
// TODO: cache this, no need to recheck often
let quote_amount = 10_000_000u64;
let slippage = 1.0;
mango_client
.jupiter_route(
quote_token_mint,
token_mint,
quote_amount,
slippage,
client::JupiterSwapMode::ExactIn,
)
.is_ok()
}
pub fn jupiter_market_can_sell(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
) -> bool {
if token == quote_token {
return true;
}
let token_mint = mango_client.context.token(token).mint_info.mint;
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
// Consider a market alive if we can swap $10 worth at 1% slippage
// TODO: configurable
// TODO: cache this, no need to recheck often
let quote_amount = 10_000_000u64;
let slippage = 1.0;
mango_client
.jupiter_route(
token_mint,
quote_token_mint,
quote_amount,
slippage,
client::JupiterSwapMode::ExactOut,
)
.is_ok()
}
#[allow(clippy::too_many_arguments)]
pub fn maybe_liquidate_account(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
2022-07-13 04:02:25 -07:00
pubkey: &Pubkey,
2022-08-05 04:50:44 -07:00
config: &Config,
) -> anyhow::Result<bool> {
2022-08-05 04:50:44 -07:00
let min_health_ratio = I80F48::from_num(config.min_health_ratio);
2022-07-13 05:04:20 -07:00
let quote_token_index = 0;
let account = account_fetcher.fetch_mango_account(pubkey)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
let maint_health = health_cache.health(HealthType::Maint);
2022-09-12 06:25:50 -07:00
if !health_cache.is_liquidatable() {
return Ok(false);
}
2022-07-13 04:02:25 -07:00
log::trace!(
2022-09-12 06:25:50 -07:00
"possible candidate: {}, with owner: {}, maint health: {}",
pubkey,
account.fixed.owner,
maint_health,
);
// Fetch a fresh account and re-compute
2022-07-18 09:16:58 -07:00
// This is -- unfortunately -- needed because the websocket streams seem to not
// be great at providing timely updates to the account data.
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
2022-09-12 06:25:50 -07:00
if !health_cache.is_liquidatable() {
return Ok(false);
}
let maint_health = health_cache.health(HealthType::Maint);
2022-09-12 06:25:50 -07:00
let is_spot_bankrupt = health_cache.can_call_spot_bankruptcy();
let is_spot_liquidatable = health_cache.has_borrows() && !is_spot_bankrupt;
2022-07-13 04:02:25 -07:00
2022-07-13 05:04:20 -07:00
// find asset and liab tokens
let mut tokens = account
.active_token_positions()
.map(|token_position| {
let token = mango_client.context.token(token_position.token_index);
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
let price = bank.oracle_price(&KeyedAccountSharedData::new(
token.mint_info.oracle,
oracle.into(),
))?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
2022-07-13 05:04:20 -07:00
})
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
2022-07-13 05:04:20 -07:00
tokens.sort_by(|a, b| a.2.cmp(&b.2));
2022-08-31 05:41:04 -07:00
// look for any open serum orders or settleable balances
let serum_force_cancels = account
.active_serum3_orders()
.map(|orders| {
let open_orders_account = account_fetcher.fetch_raw_account(orders.open_orders)?;
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
let can_force_cancel = open_orders.native_coin_total > 0
|| open_orders.native_pc_total > 0
|| open_orders.referrer_rebates_accrued > 0;
if can_force_cancel {
Ok(Some(*orders))
} else {
Ok(None)
}
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
2022-07-13 05:04:20 -07:00
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
let mut liqor = account_fetcher
.fetch_fresh_mango_account(&mango_client.mango_account_address)
.context("getting liquidator account")?;
2022-07-13 05:04:20 -07:00
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
liqor.ensure_token_position(source)?;
liqor.ensure_token_position(target)?;
2022-07-13 05:04:20 -07:00
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok");
let source_price = health_cache.token_info(source).unwrap().oracle_price;
let target_price = health_cache.token_info(target).unwrap().oracle_price;
// TODO: This is where we could multiply in the liquidation fee factors
let oracle_swap_price = source_price / target_price;
let amount = health_cache
.max_swap_source_for_health_ratio(source, target, oracle_swap_price, min_health_ratio)
.context("getting max_swap_source")?;
2022-07-13 05:04:20 -07:00
Ok(amount)
};
2022-07-13 04:02:25 -07:00
// try liquidating
2022-08-31 05:41:04 -07:00
let txsig = if !serum_force_cancels.is_empty() {
// pick a random market to force-cancel orders on
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = mango_client.serum3_liq_force_cancel_orders(
(pubkey, &account),
serum_orders.market_index,
&serum_orders.open_orders,
)?;
log::info!(
"Force cancelled serum market on account {}, market index {}, maint_health was {}, tx sig {:?}",
pubkey,
serum_orders.market_index,
maint_health,
sig
);
sig
2022-09-12 06:25:50 -07:00
} else if is_spot_bankrupt {
2022-07-13 05:04:20 -07:00
if tokens.is_empty() {
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
}
let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
2022-07-13 05:04:20 -07:00
let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
2022-07-13 05:04:20 -07:00
let sig = mango_client
.liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
2022-07-13 05:04:20 -07:00
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
pubkey,
2022-07-13 05:04:20 -07:00
maint_health,
sig
);
sig
2022-09-12 06:25:50 -07:00
} else if is_spot_liquidatable {
let asset_token_index = tokens
.iter()
.rev()
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
asset_usdc_equivalent.is_positive()
&& jupiter_market_can_sell(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
2022-07-13 04:02:25 -07:00
let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
.context("getting max_liab_transfer")?;
2022-07-13 04:02:25 -07:00
//
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
//
let sig = mango_client
.liq_token_with_token(
(pubkey, &account),
asset_token_index,
liab_token_index,
max_liab_transfer,
2022-07-13 04:02:25 -07:00
)
.context("sending liq_token_with_token")?;
log::info!(
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
pubkey,
2022-07-13 04:02:25 -07:00
maint_health,
sig
);
sig
} else {
2022-09-12 06:25:50 -07:00
anyhow::bail!(
"Don't know what to do with liquidatable account {}, maint_health was {}",
pubkey,
maint_health
);
};
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
) {
log::info!("could not refresh after liquidation: {}", e);
2022-07-13 04:02:25 -07:00
}
Ok(true)
2022-07-13 04:02:25 -07:00
}
#[allow(clippy::too_many_arguments)]
pub fn maybe_liquidate_one<'a>(
2022-07-13 04:02:25 -07:00
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
2022-07-13 04:02:25 -07:00
accounts: impl Iterator<Item = &'a Pubkey>,
2022-08-05 04:50:44 -07:00
config: &Config,
) -> bool {
2022-07-13 04:02:25 -07:00
for pubkey in accounts {
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
Err(err) => {
// Not all errors need to be raised to the user's attention.
let mut log_level = log::Level::Error;
// Simulation errors due to liqee precondition failures on the liquidation instructions
// will commonly happen if our liquidator is late or if there are chain forks.
match err.downcast_ref::<MangoClientError>() {
Some(MangoClientError::SendTransactionPreflightFailure { logs }) => {
if logs.contains("HealthMustBeNegative") || logs.contains("IsNotBankrupt") {
log_level = log::Level::Trace;
}
}
_ => {}
};
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
}
Ok(true) => return true,
2022-07-13 04:02:25 -07:00
_ => {}
};
}
false
}