Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deposit limits #806

Merged
merged 1 commit into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/liquidator/src/liquidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ impl<'a> LiquidateHelper<'a> {
// TODO: This is where we could multiply in the liquidation fee factors
let price = source_price / target_price;

util::max_swap_source(
util::max_swap_source_ignoring_limits(
self.client,
self.account_fetcher,
&liqor,
Expand Down
148 changes: 100 additions & 48 deletions bin/liquidator/src/trigger_tcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ use crate::{token_swap_info, util, ErrorTracking};
/// making the whole execution fail.
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%

/// If a tcs gets limited due to exhausted net borrows, don't trigger execution if
/// the possible value is below this amount. This avoids spamming executions when net
/// borrows are exhausted.
const NET_BORROW_EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
/// If a tcs gets limited due to exhausted net borrows or deposit limits, don't trigger execution if
/// the possible value is below this amount. This avoids spamming executions when limits are exhausted.
const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Mode {
Expand Down Expand Up @@ -440,12 +439,17 @@ impl Context {
/// This includes
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
/// - reduce only banks
/// - net borrow limits on BOTH sides, even though the buy side is technically
/// a liqor limitation: the liqor could acquire the token before trying the
/// execution... but in practice the liqor will work on margin
/// - net borrow limits:
/// - the account may borrow the sell token (and the liqor side may not be a repay)
/// - the liqor may borrow the buy token (and the account side may not be a repay)
/// this is technically a liqor limitation: the liqor could acquire the token before trying the
/// execution... but in practice the liqor may work on margin
/// - deposit limits:
/// - the account may deposit the buy token (while the liqor borrowed it)
/// - the liqor may deposit the sell token (while the account borrowed it)
///
/// Returns Some((native buy amount, native sell amount)) if execution is sensible
/// Returns None if the execution should be skipped (due to net borrow limits...)
/// Returns None if the execution should be skipped (due to limits)
pub fn tcs_max_liqee_execution(
&self,
account: &MangoAccountValue,
Expand All @@ -458,18 +462,18 @@ impl Context {
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
let maker_price = tcs.maker_price(premium_price);

let buy_position = account
let liqee_buy_position = account
.token_position(tcs.buy_token_index)
.map(|p| p.native(&buy_bank))
.unwrap_or(I80F48::ZERO);
let sell_position = account
let liqee_sell_position = account
.token_position(tcs.sell_token_index)
.map(|p| p.native(&sell_bank))
.unwrap_or(I80F48::ZERO);

// this is in "buy token received per sell token given" units
let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price);
let max_sell_ignoring_net_borrows = util::max_swap_source_ignore_net_borrows(
let max_sell_ignoring_limits = util::max_swap_source_ignoring_limits(
&self.mango_client,
&self.account_fetcher,
account,
Expand All @@ -480,41 +484,31 @@ impl Context {
)?
.floor()
.to_num::<u64>()
.min(tcs.max_sell_for_position(sell_position, &sell_bank));
.min(tcs.max_sell_for_position(liqee_sell_position, &sell_bank));

let max_buy_ignoring_net_borrows = tcs.max_buy_for_position(buy_position, &buy_bank);
let max_buy_ignoring_limits = tcs.max_buy_for_position(liqee_buy_position, &buy_bank);

// What follows is a complex manual handling of net borrow limits, for the following reason:
// What follows is a complex manual handling of net borrow/deposit limits, for
// the following reason:
// Usually, we want to execute tcs even for small amounts because that will close the
// tcs order: either due to full execution or due to the health threshold being reached.
//
// However, when the net borrow limits are hit, it will not closed when no further execution
// is possible, because net borrow limit issues are considered transient. Furthermore, we
// don't even want to send a tiny tcs trigger transactions, because there's a good chance we
// would then be sending lot of those as oracle prices fluctuate.
// However, when the limits are hit, it will not closed when no further execution
// is possible, because limit issues are transient. Furthermore, we don't want to send
// tiny tcs trigger transactions, because there's a good chance we would then be sending
// lot of those as oracle prices fluctuate.
//
// Thus, we need to detect if the possible execution amount is tiny _because_ of the
// net borrow limits. Then skip. If it's tiny for other reasons we can proceed.
// limits. Then skip. If it's tiny for other reasons we can proceed.

fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
(bank.remaining_net_borrows_quote(price) / price).clamp_to_u64()
}
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);

// New borrows if max_sell_ignoring_net_borrows was withdrawn on the liqee
let sell_borrows = (I80F48::from(max_sell_ignoring_net_borrows)
- sell_position.max(I80F48::ZERO))
.clamp_to_u64();

// On the buy side, the liqor might need to borrow
let buy_borrows = match self.config.mode {
// Do the liqor buy tokens come from deposits or are they borrowed?
let mut liqor_buy_borrows = match self.config.mode {
Mode::BorrowBuyToken => {
// Assume that the liqor has enough buy token if it's collateral
if tcs.buy_token_index == self.config.collateral_token_index {
0
} else {
max_buy_ignoring_net_borrows
max_buy_ignoring_limits
}
}
Mode::SwapCollateralIntoBuy { .. } => 0,
Expand All @@ -525,19 +519,77 @@ impl Context {
}
};

// New maximums adjusted for net borrow limits
let max_sell =
max_sell_ignoring_net_borrows - sell_borrows + sell_borrows.min(available_sell_borrows);
let max_buy =
max_buy_ignoring_net_borrows - buy_borrows + buy_borrows.min(available_buy_borrows);

let tiny_due_to_net_borrows = {
let buy_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / buy_token_price;
let sell_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / sell_token_price;
max_buy < buy_threshold && max_buy_ignoring_net_borrows > buy_threshold
|| max_sell < sell_threshold && max_sell_ignoring_net_borrows > sell_threshold
// First, net borrow limits
let max_sell_net_borrows;
let max_buy_net_borrows;
{
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
bank.remaining_net_borrows_quote(price)
.saturating_div(price)
.clamp_to_u64()
}
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);

// New borrows if max_sell_ignoring_limits was withdrawn on the liqee
// We assume that on the liqor side the position is >= 0, so these are true
// new borrows.
let sell_borrows = (I80F48::from(max_sell_ignoring_limits)
- liqee_sell_position.max(I80F48::ZERO))
.ceil()
.clamp_to_u64();

// On the buy side, the liqor might need to borrow, see liqor_buy_borrows.
// On the liqee side, the bought tokens may repay a borrow, reducing net borrows again
let buy_borrows = (I80F48::from(liqor_buy_borrows)
+ liqee_buy_position.min(I80F48::ZERO))
.ceil()
.clamp_to_u64();

// New maximums adjusted for net borrow limits
max_sell_net_borrows = max_sell_ignoring_limits
- (sell_borrows - sell_borrows.min(available_sell_borrows));
max_buy_net_borrows =
max_buy_ignoring_limits - (buy_borrows - buy_borrows.min(available_buy_borrows));
liqor_buy_borrows = liqor_buy_borrows.min(max_buy_net_borrows);
}

// Second, deposit limits
let max_sell;
let max_buy;
{
let available_buy_deposits = buy_bank.remaining_deposits_until_limit().clamp_to_u64();
let available_sell_deposits = sell_bank.remaining_deposits_until_limit().clamp_to_u64();

// New deposits on the liqee side (reduced by repaid borrows)
let liqee_buy_deposits = (I80F48::from(max_buy_net_borrows)
+ liqee_buy_position.min(I80F48::ZERO))
.ceil()
.clamp_to_u64();
// the net new deposits can only be as big as the liqor borrows
// (assume no borrows, then deposits only move from liqor to liqee)
let buy_deposits = liqee_buy_deposits.min(liqor_buy_borrows);

// We assume the liqor position is always >= 0, meaning there are new sell token deposits if
// the sell token gets borrowed on the liqee side.
let sell_deposits = (I80F48::from(max_sell_net_borrows)
- liqee_sell_position.max(I80F48::ZERO))
.ceil()
.clamp_to_u64();

max_sell =
max_sell_net_borrows - (sell_deposits - sell_deposits.min(available_sell_deposits));
max_buy =
max_buy_net_borrows - (buy_deposits - buy_deposits.min(available_buy_deposits));
}

let tiny_due_to_limits = {
let buy_threshold = I80F48::from(EXECUTION_THRESHOLD) / buy_token_price;
let sell_threshold = I80F48::from(EXECUTION_THRESHOLD) / sell_token_price;
max_buy < buy_threshold && max_buy_ignoring_limits > buy_threshold
|| max_sell < sell_threshold && max_sell_ignoring_limits > sell_threshold
};
if tiny_due_to_net_borrows {
if tiny_due_to_limits {
return Ok(None);
}

Expand Down Expand Up @@ -715,7 +767,7 @@ impl Context {
.0
.native(&buy_bank);
let liqor_available_buy_token = match mode {
Mode::BorrowBuyToken => util::max_swap_source(
Mode::BorrowBuyToken => util::max_swap_source_with_limits(
&self.mango_client,
&self.account_fetcher,
&liqor,
Expand All @@ -734,7 +786,7 @@ impl Context {
self.token_bank_price_mint(collateral_token_index)?;
let buy_per_collateral_price = (collateral_price / buy_token_price)
* I80F48::from_num(jupiter_slippage_fraction);
let collateral_amount = util::max_swap_source(
let collateral_amount = util::max_swap_source_with_limits(
&self.mango_client,
&self.account_fetcher,
&liqor,
Expand All @@ -751,7 +803,7 @@ impl Context {
// How big can the sell -> buy swap be?
let buy_per_sell_price =
(I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction);
let max_sell = util::max_swap_source(
let max_sell = util::max_swap_source_with_limits(
&self.mango_client,
&self.account_fetcher,
&liqor,
Expand Down
21 changes: 11 additions & 10 deletions bin/liquidator/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ pub fn is_perp_market<'a>(
}

/// Convenience wrapper for getting max swap amounts for a token pair
pub fn max_swap_source(
///
/// This applies net borrow and deposit limits, which is useful for true swaps.
pub fn max_swap_source_with_limits(
client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
account: &MangoAccountValue,
Expand All @@ -64,7 +66,7 @@ pub fn max_swap_source(
let source_price = health_cache.token_info(source).unwrap().prices.oracle;

let amount = health_cache
.max_swap_source_for_health_ratio(
.max_swap_source_for_health_ratio_with_limits(
&account,
&source_bank,
source_price,
Expand All @@ -77,7 +79,10 @@ pub fn max_swap_source(
}

/// Convenience wrapper for getting max swap amounts for a token pair
pub fn max_swap_source_ignore_net_borrows(
///
/// This is useful for liquidations, which don't increase deposits or net borrows.
/// Tcs execution can also increase deposits/net borrows.
pub fn max_swap_source_ignoring_limits(
client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
account: &MangoAccountValue,
Expand All @@ -97,17 +102,13 @@ pub fn max_swap_source_ignore_net_borrows(
mango_v4_client::health_cache::new_sync(&client.context, account_fetcher, &account)
.expect("always ok");

let mut source_bank: Bank =
account_fetcher.fetch(&client.context.token(source).first_bank())?;
source_bank.net_borrow_limit_per_window_quote = -1;
let mut target_bank: Bank =
account_fetcher.fetch(&client.context.token(target).first_bank())?;
target_bank.net_borrow_limit_per_window_quote = -1;
let source_bank: Bank = account_fetcher.fetch(&client.context.token(source).first_bank())?;
let target_bank: Bank = account_fetcher.fetch(&client.context.token(target).first_bank())?;

let source_price = health_cache.token_info(source).unwrap().prices.oracle;

let amount = health_cache
.max_swap_source_for_health_ratio(
.max_swap_source_for_health_ratio_ignoring_limits(
&account,
&source_bank,
source_price,
Expand Down
Loading
Loading