2022-08-07 11:04:19 -07:00
|
|
|
use std::time::Duration;
|
|
|
|
|
2022-06-18 07:31:28 -07:00
|
|
|
use crate::account_shared_data::KeyedAccountSharedData;
|
|
|
|
|
2022-07-21 04:03:28 -07:00
|
|
|
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
|
2022-06-18 07:31:28 -07:00
|
|
|
use mango_v4::state::{
|
2022-07-13 03:21:02 -07:00
|
|
|
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
|
2022-08-05 11:28:14 -07:00
|
|
|
MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX,
|
2022-06-18 07:31:28 -07:00
|
|
|
};
|
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
2022-06-18 07:31:28 -07:00
|
|
|
|
2022-08-05 04:50:44 -07:00
|
|
|
pub struct Config {
|
|
|
|
pub min_health_ratio: f64,
|
2022-08-07 11:04:19 -07:00
|
|
|
pub refresh_timeout: Duration,
|
2022-08-05 04:50:44 -07:00
|
|
|
}
|
|
|
|
|
2022-07-13 03:21:02 -07:00
|
|
|
pub fn new_health_cache_(
|
2022-07-16 05:37:15 -07:00
|
|
|
context: &MangoGroupContext,
|
2022-07-21 04:03:28 -07:00
|
|
|
account_fetcher: &chain_data::AccountFetcher,
|
2022-07-25 07:07:53 -07:00
|
|
|
account: &MangoAccountValue,
|
2022-07-16 05:37:15 -07:00
|
|
|
) -> anyhow::Result<HealthCache> {
|
2022-07-25 07:07:53 -07:00
|
|
|
let active_token_len = account.token_iter_active().count();
|
|
|
|
let active_perp_len = account.perp_iter_active_accounts().count();
|
2022-07-16 05:37:15 -07:00
|
|
|
|
2022-08-04 08:01:00 -07:00
|
|
|
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
|
2022-07-16 05:37:15 -07:00
|
|
|
let accounts = metas
|
|
|
|
.iter()
|
|
|
|
.map(|meta| {
|
|
|
|
Ok(KeyedAccountSharedData::new(
|
|
|
|
meta.pubkey,
|
|
|
|
account_fetcher.fetch_raw(&meta.pubkey)?,
|
|
|
|
))
|
2022-06-18 07:31:28 -07:00
|
|
|
})
|
2022-07-16 05:37:15 -07:00
|
|
|
.collect::<anyhow::Result<Vec<_>>>()?;
|
2022-06-18 07:31:28 -07:00
|
|
|
|
|
|
|
let retriever = FixedOrderAccountRetriever {
|
2022-07-16 05:37:15 -07:00
|
|
|
ais: accounts,
|
2022-06-18 07:31:28 -07:00
|
|
|
n_banks: active_token_len,
|
|
|
|
begin_perp: active_token_len * 2,
|
|
|
|
begin_serum3: active_token_len * 2 + active_perp_len,
|
|
|
|
};
|
2022-07-25 07:07:53 -07:00
|
|
|
new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|
|
|
|
|
2022-08-05 11:28:14 -07:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-06-18 07:31:28 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-08-05 11:28:14 -07:00
|
|
|
pub fn maybe_liquidate_account(
|
2022-06-18 07:31:28 -07:00
|
|
|
mango_client: &MangoClient,
|
2022-07-21 04:03:28 -07:00
|
|
|
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,
|
2022-08-05 11:28:14 -07:00
|
|
|
) -> 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;
|
2022-07-13 03:21:02 -07:00
|
|
|
|
2022-07-25 07:07:53 -07:00
|
|
|
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
2022-08-11 09:15:47 -07:00
|
|
|
let health_cache =
|
|
|
|
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
|
|
|
|
let maint_health = health_cache.health(HealthType::Maint);
|
|
|
|
let is_bankrupt = !health_cache.has_liquidatable_assets();
|
2022-07-16 05:37:15 -07:00
|
|
|
|
2022-08-11 09:15:47 -07:00
|
|
|
if maint_health >= 0 && !is_bankrupt {
|
2022-08-05 11:28:14 -07:00
|
|
|
return Ok(false);
|
2022-07-16 05:37:15 -07:00
|
|
|
}
|
2022-07-13 04:02:25 -07:00
|
|
|
|
2022-07-16 05:37:15 -07:00
|
|
|
log::trace!(
|
|
|
|
"possible candidate: {}, with owner: {}, maint health: {}, bankrupt: {}",
|
|
|
|
pubkey,
|
2022-07-25 07:07:53 -07:00
|
|
|
account.fixed.owner,
|
2022-07-16 05:37:15 -07:00
|
|
|
maint_health,
|
2022-08-11 09:15:47 -07:00
|
|
|
is_bankrupt,
|
2022-07-16 05:37:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// 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.
|
2022-07-25 07:07:53 -07:00
|
|
|
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
|
2022-08-11 09:15:47 -07:00
|
|
|
let health_cache =
|
|
|
|
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
|
|
|
|
let maint_health = health_cache.health(HealthType::Maint);
|
|
|
|
let is_bankrupt = !health_cache.has_liquidatable_assets();
|
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
|
2022-07-25 07:07:53 -07:00
|
|
|
.token_iter_active()
|
2022-07-16 05:37:15 -07:00
|
|
|
.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)?;
|
2022-07-13 05:04:20 -07:00
|
|
|
let price = oracle_price(
|
2022-07-16 05:37:15 -07:00
|
|
|
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
2022-07-13 05:04:20 -07:00
|
|
|
bank.oracle_config.conf_filter,
|
|
|
|
bank.mint_decimals,
|
|
|
|
)?;
|
2022-07-16 05:37:15 -07:00
|
|
|
Ok((
|
|
|
|
token_position.token_index,
|
2022-08-05 11:28:14 -07:00
|
|
|
price,
|
2022-07-16 05:37:15 -07:00
|
|
|
token_position.native(&bank) * price,
|
|
|
|
))
|
2022-07-13 05:04:20 -07:00
|
|
|
})
|
2022-08-05 11:28:14 -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));
|
|
|
|
|
|
|
|
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
|
2022-07-16 05:37:15 -07:00
|
|
|
let mut liqor = account_fetcher
|
2022-07-25 07:07:53 -07:00
|
|
|
.fetch_fresh_mango_account(&mango_client.mango_account_address)
|
2022-07-30 00:49:56 -07:00
|
|
|
.context("getting liquidator account")?;
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-07-15 01:01:32 -07:00
|
|
|
// Ensure the tokens are activated, so they appear in the health cache and
|
|
|
|
// max_swap_source() will work.
|
2022-07-25 07:07:53 -07:00
|
|
|
liqor.token_get_mut_or_create(source)?;
|
|
|
|
liqor.token_get_mut_or_create(target)?;
|
2022-07-13 05:04:20 -07:00
|
|
|
|
2022-07-15 01:01:32 -07:00
|
|
|
let health_cache =
|
2022-07-16 05:37:15 -07:00
|
|
|
new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok");
|
2022-07-15 01:01:32 -07:00
|
|
|
let amount = health_cache
|
|
|
|
.max_swap_source_for_health_ratio(source, target, 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-11 09:15:47 -07:00
|
|
|
let txsig = if is_bankrupt {
|
2022-07-13 05:04:20 -07:00
|
|
|
if tokens.is_empty() {
|
|
|
|
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
|
|
|
|
}
|
2022-08-05 11:28:14 -07:00
|
|
|
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
|
|
|
|
2022-08-05 11:28:14 -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
|
2022-08-05 11:28:14 -07:00
|
|
|
.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!(
|
2022-08-05 11:28:14 -07:00
|
|
|
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
|
|
|
pubkey,
|
2022-07-13 05:04:20 -07:00
|
|
|
maint_health,
|
|
|
|
sig
|
|
|
|
);
|
2022-08-07 11:04:19 -07:00
|
|
|
sig
|
2022-07-13 05:04:20 -07:00
|
|
|
} else if maint_health.is_negative() {
|
2022-08-05 11:28:14 -07:00
|
|
|
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
|
|
|
|
2022-08-05 11:28:14 -07:00
|
|
|
let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
|
2022-07-15 01:01:32 -07:00
|
|
|
.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
|
|
|
|
// TODO: swap inherited liabs to desired asset for liqor
|
|
|
|
//
|
|
|
|
let sig = mango_client
|
|
|
|
.liq_token_with_token(
|
2022-07-16 05:37:15 -07:00
|
|
|
(pubkey, &account),
|
2022-08-05 11:28:14 -07:00
|
|
|
asset_token_index,
|
|
|
|
liab_token_index,
|
2022-07-13 03:21:02 -07:00
|
|
|
max_liab_transfer,
|
2022-07-13 04:02:25 -07:00
|
|
|
)
|
|
|
|
.context("sending liq_token_with_token")?;
|
|
|
|
log::info!(
|
2022-08-05 11:28:14 -07:00
|
|
|
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
|
|
|
pubkey,
|
2022-07-13 04:02:25 -07:00
|
|
|
maint_health,
|
|
|
|
sig
|
|
|
|
);
|
2022-08-07 11:04:19 -07:00
|
|
|
sig
|
|
|
|
} else {
|
|
|
|
return Ok(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
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
|
|
|
}
|
2022-08-07 11:04:19 -07:00
|
|
|
|
|
|
|
Ok(true)
|
2022-07-13 04:02:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-08-05 11:28:14 -07:00
|
|
|
pub fn maybe_liquidate_one<'a>(
|
2022-07-13 04:02:25 -07:00
|
|
|
mango_client: &MangoClient,
|
2022-07-21 04:03:28 -07:00
|
|
|
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,
|
2022-08-05 11:28:14 -07:00
|
|
|
) -> bool {
|
2022-07-13 04:02:25 -07:00
|
|
|
for pubkey in accounts {
|
2022-08-05 11:28:14 -07:00
|
|
|
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
|
2022-07-19 05:56:26 -07:00
|
|
|
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);
|
|
|
|
}
|
2022-08-05 11:28:14 -07:00
|
|
|
Ok(true) => return true,
|
2022-07-13 04:02:25 -07:00
|
|
|
_ => {}
|
|
|
|
};
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|
|
|
|
|
2022-08-05 11:28:14 -07:00
|
|
|
false
|
2022-06-18 07:31:28 -07:00
|
|
|
}
|