From 338a9cb7b85680690da098382624056102f3e9ef Mon Sep 17 00:00:00 2001 From: Serge Farny Date: Mon, 19 Feb 2024 10:20:12 +0100 Subject: [PATCH] liquidator: add allow/forbid token list (#883) liquidator: add a way to restrict token accepted by the liquidator - add allow/forbid list of token for liquidation & conditional token swap triggering - add allow/forbid list for perp market liquidation - housekeeping: extract cli args to a dedicated file - move more hardcoded thing to config and stop using token name (replace with token index) --- bin/liquidator/src/cli_args.rs | 207 ++++++++++++++++++++++++++++++ bin/liquidator/src/liquidate.rs | 130 ++++++++++++------- bin/liquidator/src/main.rs | 203 ++++------------------------- bin/liquidator/src/trigger_tcs.rs | 39 +++++- 4 files changed, 356 insertions(+), 223 deletions(-) create mode 100644 bin/liquidator/src/cli_args.rs diff --git a/bin/liquidator/src/cli_args.rs b/bin/liquidator/src/cli_args.rs new file mode 100644 index 000000000..fe0eb1b76 --- /dev/null +++ b/bin/liquidator/src/cli_args.rs @@ -0,0 +1,207 @@ +use crate::trigger_tcs; +use anchor_lang::prelude::Pubkey; +use clap::Parser; +use mango_v4_client::{jupiter, priority_fees_cli}; +use std::collections::HashSet; + +#[derive(Parser, Debug)] +#[clap()] +pub(crate) struct CliDotenv { + // When --dotenv is passed, read the specified dotenv file before parsing args + #[clap(long)] + pub(crate) dotenv: std::path::PathBuf, + + pub(crate) remaining_args: Vec, +} + +// Prefer "--rebalance false" over "--no-rebalance" because it works +// better with REBALANCE=false env values. +#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum BoolArg { + True, + False, +} + +#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum JupiterVersionArg { + Mock, + V6, +} + +impl From for jupiter::Version { + fn from(a: JupiterVersionArg) -> Self { + match a { + JupiterVersionArg::Mock => jupiter::Version::Mock, + JupiterVersionArg::V6 => jupiter::Version::V6, + } + } +} + +#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TcsMode { + BorrowBuy, + SwapSellIntoBuy, + SwapCollateralIntoBuy, +} + +impl From for trigger_tcs::Mode { + fn from(a: TcsMode) -> Self { + match a { + TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken, + TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy, + TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy, + } + } +} + +pub(crate) fn cli_to_hashset>( + str_list: Option>, +) -> HashSet { + return str_list + .map(|v| v.iter().map(|x| T::from(*x)).collect::>()) + .unwrap_or_default(); +} + +#[derive(Parser)] +#[clap()] +pub struct Cli { + #[clap(short, long, env)] + pub(crate) rpc_url: String, + + #[clap(long, env, value_delimiter = ';')] + pub(crate) override_send_transaction_url: Option>, + + #[clap(long, env)] + pub(crate) liqor_mango_account: Pubkey, + + #[clap(long, env)] + pub(crate) liqor_owner: String, + + #[clap(long, env, default_value = "1000")] + pub(crate) check_interval_ms: u64, + + #[clap(long, env, default_value = "300")] + pub(crate) snapshot_interval_secs: u64, + + // how often do we refresh token swap route/prices + #[clap(long, env, default_value = "30")] + pub(crate) token_swap_refresh_interval_secs: u64, + + /// how many getMultipleAccounts requests to send in parallel + #[clap(long, env, default_value = "10")] + pub(crate) parallel_rpc_requests: usize, + + /// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once + #[clap(long, env, default_value = "100")] + pub(crate) get_multiple_accounts_count: usize, + + /// liquidator health ratio should not fall below this value + #[clap(long, env, default_value = "50")] + pub(crate) min_health_ratio: f64, + + /// if rebalancing is enabled + /// + /// typically only disabled for tests where swaps are unavailable + #[clap(long, env, value_enum, default_value = "true")] + pub(crate) rebalance: BoolArg, + + /// max slippage to request on swaps to rebalance spot tokens + #[clap(long, env, default_value = "100")] + pub(crate) rebalance_slippage_bps: u64, + + /// tokens to not rebalance (in addition to USDC=0); use a comma separated list of token index + #[clap(long, env, value_parser, value_delimiter = ',')] + pub(crate) rebalance_skip_tokens: Option>, + + /// When closing borrows, the rebalancer can't close token positions exactly. + /// Instead it purchases too much and then gets rid of the excess in a second step. + /// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token. + #[clap(long, env, default_value = "0.05")] + pub(crate) rebalance_borrow_settle_excess: f64, + + #[clap(long, env, default_value = "30")] + pub(crate) rebalance_refresh_timeout_secs: u64, + + /// if taking tcs orders is enabled + /// + /// typically only disabled for tests where swaps are unavailable + #[clap(long, env, value_enum, default_value = "true")] + pub(crate) take_tcs: BoolArg, + + /// profit margin at which to take tcs orders + #[clap(long, env, default_value = "0.0005")] + pub(crate) tcs_profit_fraction: f64, + + /// control how tcs triggering provides buy tokens + #[clap(long, env, value_enum, default_value = "swap-sell-into-buy")] + pub(crate) tcs_mode: TcsMode, + + /// largest tcs amount to trigger in one transaction, in dollar + #[clap(long, env, default_value = "1000.0")] + pub(crate) tcs_max_trigger_amount: f64, + + /// Minimum fraction of max_buy to buy for success when triggering, + /// useful in conjunction with jupiter swaps in same tx to avoid over-buying. + /// + /// Can be set to 0 to allow executions of any size. + #[clap(long, env, default_value = "0.7")] + pub(crate) tcs_min_buy_fraction: f64, + + #[clap(flatten)] + pub(crate) prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs, + + /// url to the lite-rpc websocket, optional + #[clap(long, env, default_value = "")] + pub(crate) lite_rpc_url: String, + + /// compute limit requested for liquidation instructions + #[clap(long, env, default_value = "250000")] + pub(crate) compute_limit_for_liquidation: u32, + + /// compute limit requested for tcs trigger instructions + #[clap(long, env, default_value = "300000")] + pub(crate) compute_limit_for_tcs: u32, + + /// control which version of jupiter to use + #[clap(long, env, value_enum, default_value = "v6")] + pub(crate) jupiter_version: JupiterVersionArg, + + /// override the url to jupiter v6 + #[clap(long, env, default_value = "https://quote-api.jup.ag/v6")] + pub(crate) jupiter_v6_url: String, + + /// provide a jupiter token, currently only for jup v6 + #[clap(long, env, default_value = "")] + pub(crate) jupiter_token: String, + + /// size of the swap to quote via jupiter to get slippage info, in dollar + /// should be larger than tcs_max_trigger_amount + #[clap(long, env, default_value = "1000.0")] + pub(crate) jupiter_swap_info_amount: f64, + + /// report liquidator's existence and pubkey + #[clap(long, env, value_enum, default_value = "true")] + pub(crate) telemetry: BoolArg, + + /// liquidation refresh timeout in secs + #[clap(long, env, default_value = "30")] + pub(crate) liquidation_refresh_timeout_secs: u8, + + /// tokens to exclude for liquidation/tcs (never liquidate any pair where base or quote is in this list) + #[clap(long, env, value_parser, value_delimiter = ' ')] + pub(crate) forbidden_tokens: Option>, + + /// tokens to allow for liquidation/tcs (only liquidate a pair if base or quote is in this list) + /// when empty, allows all pairs + #[clap(long, env, value_parser, value_delimiter = ' ')] + pub(crate) only_allow_tokens: Option>, + + /// perp market to exclude for liquidation + #[clap(long, env, value_parser, value_delimiter = ' ')] + pub(crate) liquidation_forbidden_perp_markets: Option>, + + /// perp market to allow for liquidation (only liquidate if is in this list) + /// when empty, allows all pairs + #[clap(long, env, value_parser, value_delimiter = ' ')] + pub(crate) liquidation_only_allow_perp_markets: Option>, +} diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index e03d594c3..82854eb3d 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -1,3 +1,4 @@ +use std::cmp::Reverse; use std::collections::HashSet; use std::time::Duration; @@ -20,6 +21,12 @@ pub struct Config { pub refresh_timeout: Duration, pub compute_limit_for_liq_ix: u32, + pub only_allowed_tokens: HashSet, + pub forbidden_tokens: HashSet, + + pub only_allowed_perp_markets: HashSet, + pub forbidden_perp_markets: HashSet, + /// If we cram multiple ix into a transaction, don't exceed this level /// of expected-cu. pub max_cu_per_transaction: u32, @@ -33,8 +40,6 @@ struct LiquidateHelper<'a> { health_cache: &'a HealthCache, maint_health: I80F48, liqor_min_health_ratio: I80F48, - allowed_asset_tokens: HashSet, - allowed_liab_tokens: HashSet, config: Config, } @@ -136,6 +141,25 @@ impl<'a> LiquidateHelper<'a> { let all_perp_base_positions: anyhow::Result< Vec>, > = stream::iter(self.liqee.active_perp_positions()) + .filter(|pp| async { + if self + .config + .forbidden_perp_markets + .contains(&pp.market_index) + { + return false; + } + if !self.config.only_allowed_perp_markets.is_empty() + && !self + .config + .only_allowed_perp_markets + .contains(&pp.market_index) + { + return false; + } + + true + }) .then(|pp| async { let base_lots = pp.base_position_lots(); if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills() @@ -353,6 +377,7 @@ impl<'a> LiquidateHelper<'a> { .health_cache .token_infos .iter() + .filter(|p| !self.config.forbidden_tokens.contains(&p.token_index)) .zip( self.health_cache .effective_token_balances(HealthType::LiquidationEnd) @@ -378,26 +403,9 @@ impl<'a> LiquidateHelper<'a> { is_valid_asset.then_some((ti.token_index, is_preferred, quote_value)) }) .collect_vec(); - // sort such that preferred tokens are at the end, and the one with the larget quote value is - // at the very end - potential_assets.sort_by_key(|(_, is_preferred, amount)| (*is_preferred, *amount)); - - // filter only allowed assets - let potential_allowed_assets = potential_assets.iter().filter_map(|(ti, _, _)| { - let is_allowed = self - .allowed_asset_tokens - .contains(&self.client.context.token(*ti).mint); - is_allowed.then_some(*ti) - }); - - let asset_token_index = match potential_allowed_assets.last() { - Some(token_index) => token_index, - None => anyhow::bail!( - "mango account {}, has no allowed asset tokens that are liquidatable: {:?}", - self.pubkey, - potential_assets, - ), - }; + // sort such that preferred tokens are at the start, and the one with the larget quote value is + // at 0 + potential_assets.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount))); // // find a good liab, same as for assets @@ -410,29 +418,69 @@ impl<'a> LiquidateHelper<'a> { let tokens = (-ti.balance_spot).min(-effective.spot_and_perp); let is_valid_liab = tokens > 0; let quote_value = tokens * ti.prices.oracle; - is_valid_liab.then_some((ti.token_index, quote_value)) + is_valid_liab.then_some((ti.token_index, false, quote_value)) }) .collect_vec(); - // largest liquidatable liability at the end - potential_liabs.sort_by_key(|(_, amount)| *amount); + // largest liquidatable liability at the start + potential_liabs.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount))); - // filter only allowed liabs - let potential_allowed_liabs = potential_liabs.iter().filter_map(|(ti, _)| { - let is_allowed = self - .allowed_liab_tokens - .contains(&self.client.context.token(*ti).mint); - is_allowed.then_some(*ti) - }); + // + // Find a pair + // - let liab_token_index = match potential_allowed_liabs.last() { - Some(token_index) => token_index, - None => anyhow::bail!( - "mango account {}, has no liab tokens that are liquidatable: {:?}", + fn find_best_token( + lh: &LiquidateHelper, + token_list: &Vec<(TokenIndex, bool, I80F48)>, + ) -> (Option, Option) { + let mut best_whitelisted = None; + let mut best = None; + + let allowed_token_list = token_list + .iter() + .filter_map(|(ti, _, _)| (!lh.config.forbidden_tokens.contains(ti)).then_some(ti)); + + for ti in allowed_token_list { + let whitelisted = lh.config.only_allowed_tokens.is_empty() + || lh.config.only_allowed_tokens.contains(ti); + if best.is_none() { + best = Some(*ti); + } + + if best_whitelisted.is_none() && whitelisted { + best_whitelisted = Some(*ti); + break; + } + } + + return (best, best_whitelisted); + } + + let (best_asset, best_whitelisted_asset) = find_best_token(self, &potential_assets); + let (best_liab, best_whitelisted_liab) = find_best_token(self, &potential_liabs); + + let best_pair_opt = [ + (best_whitelisted_asset, best_liab), + (best_asset, best_whitelisted_liab), + ] + .iter() + .filter_map(|(a, l)| (a.is_some() && l.is_some()).then_some((a.unwrap(), l.unwrap()))) + .next(); + + if best_pair_opt.is_none() { + anyhow::bail!( + "mango account {}, has no allowed asset/liab tokens pair that are liquidatable: assets={:?}; liabs={:?}", self.pubkey, + potential_assets, potential_liabs, - ), + ) }; + let (asset_token_index, liab_token_index) = best_pair_opt.unwrap(); + + // + // Compute max transfer size + // + let max_liab_transfer = self .max_token_liab_transfer(liab_token_index, asset_token_index) .await @@ -484,9 +532,7 @@ impl<'a> LiquidateHelper<'a> { .iter() .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { liab_usdc_equivalent.is_negative() - && self - .allowed_liab_tokens - .contains(&self.client.context.token(*liab_token_index).mint) + && !self.config.forbidden_tokens.contains(liab_token_index) }) .ok_or_else(|| { anyhow::anyhow!( @@ -643,8 +689,6 @@ pub async fn maybe_liquidate_account( let maint_health = health_cache.health(HealthType::Maint); - let all_token_mints = HashSet::from_iter(mango_client.context.tokens.values().map(|c| c.mint)); - // try liquidating let maybe_txsig = LiquidateHelper { client: mango_client, @@ -654,8 +698,6 @@ pub async fn maybe_liquidate_account( health_cache: &health_cache, maint_health, liqor_min_health_ratio, - allowed_asset_tokens: all_token_mints.clone(), - allowed_liab_tokens: all_token_mints, config: config.clone(), } .send_liq_tx() diff --git a/bin/liquidator/src/main.rs b/bin/liquidator/src/main.rs index 047a7a169..dfa9b190b 100644 --- a/bin/liquidator/src/main.rs +++ b/bin/liquidator/src/main.rs @@ -7,10 +7,9 @@ use anchor_client::Cluster; use anyhow::Context; use clap::Parser; use mango_v4::state::{PerpMarketIndex, TokenIndex}; -use mango_v4_client::priority_fees_cli; use mango_v4_client::AsyncChannelSendUnlessFull; use mango_v4_client::{ - account_update_stream, chain_data, error_tracking::ErrorTracking, jupiter, keypair_from_cli, + account_update_stream, chain_data, error_tracking::ErrorTracking, keypair_from_cli, snapshot_source, websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext, TransactionBuilderConfig, }; @@ -21,6 +20,7 @@ use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::Signer; use tracing::*; +pub mod cli_args; pub mod liquidate; pub mod metrics; pub mod rebalance; @@ -36,158 +36,6 @@ use crate::util::{is_mango_account, is_mint_info, is_perp_market}; #[global_allocator] static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; -#[derive(Parser, Debug)] -#[clap()] -struct CliDotenv { - // When --dotenv is passed, read the specified dotenv file before parsing args - #[clap(long)] - dotenv: std::path::PathBuf, - - remaining_args: Vec, -} - -// Prefer "--rebalance false" over "--no-rebalance" because it works -// better with REBALANCE=false env values. -#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] -enum BoolArg { - True, - False, -} - -#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] -enum JupiterVersionArg { - Mock, - V6, -} - -impl From for jupiter::Version { - fn from(a: JupiterVersionArg) -> Self { - match a { - JupiterVersionArg::Mock => jupiter::Version::Mock, - JupiterVersionArg::V6 => jupiter::Version::V6, - } - } -} - -#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)] -enum TcsMode { - BorrowBuy, - SwapSellIntoBuy, - SwapCollateralIntoBuy, -} - -impl From for trigger_tcs::Mode { - fn from(a: TcsMode) -> Self { - match a { - TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken, - TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy, - TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy, - } - } -} - -#[derive(Parser)] -#[clap()] -struct Cli { - #[clap(short, long, env)] - rpc_url: String, - - #[clap(long, env, value_delimiter = ';')] - override_send_transaction_url: Option>, - - #[clap(long, env)] - liqor_mango_account: Pubkey, - - #[clap(long, env)] - liqor_owner: String, - - #[clap(long, env, default_value = "1000")] - check_interval_ms: u64, - - #[clap(long, env, default_value = "300")] - snapshot_interval_secs: u64, - - /// how many getMultipleAccounts requests to send in parallel - #[clap(long, env, default_value = "10")] - parallel_rpc_requests: usize, - - /// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once - #[clap(long, env, default_value = "100")] - get_multiple_accounts_count: usize, - - /// liquidator health ratio should not fall below this value - #[clap(long, env, default_value = "50")] - min_health_ratio: f64, - - /// if rebalancing is enabled - /// - /// typically only disabled for tests where swaps are unavailable - #[clap(long, env, value_enum, default_value = "true")] - rebalance: BoolArg, - - /// max slippage to request on swaps to rebalance spot tokens - #[clap(long, env, default_value = "100")] - rebalance_slippage_bps: u64, - - /// tokens to not rebalance (in addition to USDC); use a comma separated list of names - #[clap(long, env, default_value = "")] - rebalance_skip_tokens: String, - - /// if taking tcs orders is enabled - /// - /// typically only disabled for tests where swaps are unavailable - #[clap(long, env, value_enum, default_value = "true")] - take_tcs: BoolArg, - - /// profit margin at which to take tcs orders - #[clap(long, env, default_value = "0.0005")] - tcs_profit_fraction: f64, - - /// control how tcs triggering provides buy tokens - #[clap(long, env, value_enum, default_value = "swap-sell-into-buy")] - tcs_mode: TcsMode, - - /// largest tcs amount to trigger in one transaction, in dollar - #[clap(long, env, default_value = "1000.0")] - tcs_max_trigger_amount: f64, - - #[clap(flatten)] - prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs, - - /// url to the lite-rpc websocket, optional - #[clap(long, env, default_value = "")] - lite_rpc_url: String, - - /// compute limit requested for liquidation instructions - #[clap(long, env, default_value = "250000")] - compute_limit_for_liquidation: u32, - - /// compute limit requested for tcs trigger instructions - #[clap(long, env, default_value = "300000")] - compute_limit_for_tcs: u32, - - /// control which version of jupiter to use - #[clap(long, env, value_enum, default_value = "v6")] - jupiter_version: JupiterVersionArg, - - /// override the url to jupiter v6 - #[clap(long, env, default_value = "https://quote-api.jup.ag/v6")] - jupiter_v6_url: String, - - /// provide a jupiter token, currently only for jup v6 - #[clap(long, env, default_value = "")] - jupiter_token: String, - - /// size of the swap to quote via jupiter to get slippage info, in dollar - /// should be larger than tcs_max_trigger_amount - #[clap(long, env, default_value = "1000.0")] - jupiter_swap_info_amount: f64, - - /// report liquidator's existence and pubkey - #[clap(long, env, value_enum, default_value = "true")] - telemetry: BoolArg, -} - pub fn encode_address(addr: &Pubkey) -> String { bs58::encode(&addr.to_bytes()).into_string() } @@ -356,8 +204,15 @@ async fn main() -> anyhow::Result<()> { min_health_ratio: cli.min_health_ratio, compute_limit_for_liq_ix: cli.compute_limit_for_liquidation, max_cu_per_transaction: 1_000_000, - // TODO: config - refresh_timeout: Duration::from_secs(30), + refresh_timeout: Duration::from_secs(cli.liquidation_refresh_timeout_secs as u64), + only_allowed_tokens: cli_args::cli_to_hashset::(cli.only_allow_tokens), + forbidden_tokens: cli_args::cli_to_hashset::(cli.forbidden_tokens), + only_allowed_perp_markets: cli_args::cli_to_hashset::( + cli.liquidation_only_allow_perp_markets, + ), + forbidden_perp_markets: cli_args::cli_to_hashset::( + cli.liquidation_forbidden_perp_markets, + ), }; let tcs_config = trigger_tcs::Config { @@ -366,14 +221,15 @@ async fn main() -> anyhow::Result<()> { compute_limit_for_trigger: cli.compute_limit_for_tcs, profit_fraction: cli.tcs_profit_fraction, collateral_token_index: 0, // USDC - // TODO: config - refresh_timeout: Duration::from_secs(30), jupiter_version: cli.jupiter_version.into(), jupiter_slippage_bps: cli.rebalance_slippage_bps, mode: cli.tcs_mode.into(), - min_buy_fraction: 0.7, + min_buy_fraction: cli.tcs_min_buy_fraction, + + only_allowed_tokens: liq_config.only_allowed_tokens.clone(), + forbidden_tokens: liq_config.forbidden_tokens.clone(), }; let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30)); @@ -381,16 +237,10 @@ async fn main() -> anyhow::Result<()> { let rebalance_config = rebalance::Config { enabled: cli.rebalance == BoolArg::True, slippage_bps: cli.rebalance_slippage_bps, - // TODO: config - borrow_settle_excess: 1.05, - refresh_timeout: Duration::from_secs(30), + borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64), + refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs), jupiter_version: cli.jupiter_version.into(), - skip_tokens: cli - .rebalance_skip_tokens - .split(',') - .filter(|v| !v.is_empty()) - .map(|name| mango_client.context.token_by_name(name).token_index) - .collect(), + skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()), allow_withdraws: signer_is_owner, }; @@ -532,16 +382,13 @@ async fn main() -> anyhow::Result<()> { let mut took_tcs = false; if !liquidated && cli.take_tcs == BoolArg::True { - took_tcs = match liquidation + took_tcs = liquidation .maybe_take_token_conditional_swap(account_addresses.iter()) .await - { - Ok(v) => v, - Err(err) => { + .unwrap_or_else(|err| { error!("error during maybe_take_token_conditional_swap: {err}"); false - } - } + }) } if liquidated || took_tcs { @@ -552,14 +399,15 @@ async fn main() -> anyhow::Result<()> { }); let token_swap_info_job = tokio::spawn({ - // TODO: configurable interval - let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60)); + let mut interval = mango_v4_client::delay_interval(Duration::from_secs( + cli.token_swap_refresh_interval_secs, + )); let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1)); let shared_state = shared_state.clone(); async move { loop { - startup_wait.tick().await; if !shared_state.read().unwrap().one_snapshot_done { + startup_wait.tick().await; continue; } @@ -594,6 +442,7 @@ async fn main() -> anyhow::Result<()> { )); } + use cli_args::{BoolArg, Cli, CliDotenv}; use futures::StreamExt; let mut jobs: futures::stream::FuturesUnordered<_> = vec![ data_job, diff --git a/bin/liquidator/src/trigger_tcs.rs b/bin/liquidator/src/trigger_tcs.rs index 7e3f00203..d42104846 100644 --- a/bin/liquidator/src/trigger_tcs.rs +++ b/bin/liquidator/src/trigger_tcs.rs @@ -1,8 +1,9 @@ +use std::collections::HashSet; use std::{ collections::HashMap, pin::Pin, sync::{Arc, RwLock}, - time::{Duration, Instant}, + time::Instant, }; use futures_core::Future; @@ -56,7 +57,6 @@ pub enum Mode { pub struct Config { pub min_health_ratio: f64, pub max_trigger_quote_amount: u64, - pub refresh_timeout: Duration, pub compute_limit_for_trigger: u32, pub collateral_token_index: TokenIndex, @@ -73,6 +73,9 @@ pub struct Config { pub jupiter_version: jupiter::Version, pub jupiter_slippage_bps: u64, pub mode: Mode, + + pub only_allowed_tokens: HashSet, + pub forbidden_tokens: HashSet, } pub enum JupiterQuoteCacheResult { @@ -401,11 +404,43 @@ impl Context { Ok(taker_price >= base_price * cost_over_oracle * (1.0 + self.config.profit_fraction)) } + // excluded by config + fn tcs_pair_is_allowed( + &self, + buy_token_index: TokenIndex, + sell_token_index: TokenIndex, + ) -> bool { + if self.config.forbidden_tokens.contains(&buy_token_index) { + return false; + } + + if self.config.forbidden_tokens.contains(&sell_token_index) { + return false; + } + + if self.config.only_allowed_tokens.is_empty() { + return true; + } + + if self.config.only_allowed_tokens.contains(&buy_token_index) { + return true; + } + + if self.config.only_allowed_tokens.contains(&sell_token_index) { + return true; + } + + return false; + } + // Either expired or triggerable with ok-looking price. fn tcs_is_interesting(&self, tcs: &TokenConditionalSwap) -> anyhow::Result { if tcs.is_expired(self.now_ts) { return Ok(true); } + if !self.tcs_pair_is_allowed(tcs.buy_token_index, tcs.buy_token_index) { + return Ok(false); + } let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?; let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;