Liquidator: Fix rebalance to empty token accounts

By withdrawing to the liqor wallet if necessary.
This commit is contained in:
Christian Kamm 2022-08-10 09:30:58 +02:00
parent f169bcdafe
commit 405c41edcf
1 changed files with 94 additions and 30 deletions

View File

@ -14,7 +14,7 @@ pub struct Config {
#[derive(Debug)] #[derive(Debug)]
struct TokenState { struct TokenState {
_price: I80F48, price: I80F48,
native_position: I80F48, native_position: I80F48,
} }
@ -24,13 +24,20 @@ impl TokenState {
position: &TokenPosition, position: &TokenPosition,
account_fetcher: &chain_data::AccountFetcher, account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?; let bank = Self::bank(token, account_fetcher)?;
Ok(Self { Ok(Self {
_price: Self::fetch_price(token, &bank, account_fetcher)?, price: Self::fetch_price(token, &bank, account_fetcher)?,
native_position: position.native(&bank), native_position: position.native(&bank),
}) })
} }
fn bank(
token: &TokenContext,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<Bank> {
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
}
fn fetch_price( fn fetch_price(
token: &TokenContext, token: &TokenContext,
bank: &Bank, bank: &Bank,
@ -72,17 +79,50 @@ pub fn zero_all_non_quote(
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?; .collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
log::trace!("account tokens: {:?}", tokens); log::trace!("account tokens: {:?}", tokens);
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
let refresh_mango_account =
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
) {
// If we don't get fresh data, maybe the tx landed on a fork?
// Rebalance is technically still ok.
log::info!("could not refresh account data: {}", e);
return Ok(false);
}
Ok(true)
};
for (token_index, token_state) in tokens { for (token_index, token_state) in tokens {
let token = mango_client.context.token(token_index); let token = mango_client.context.token(token_index);
if token_index == quote_token.token_index { if token_index == quote_token.token_index {
continue; continue;
} }
let token_mint = token.mint_info.mint;
let quote_mint = quote_token.mint_info.mint;
let maybe_txsig = if token_state.native_position > 0 { // It's not always possible to bring the native balance to 0 through swaps:
let amount = token_state.native_position; // Consider a price <1. You need to sell a bunch of tokens to get 1 USDC native and
// similarly will get multiple tokens when buying.
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
// would not be worth a single USDC-native.
//
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
// to sell them. Instead they will be withdrawn at the end.
// Purchases will aim to purchase slightly more than is needed, such that we can
// again withdraw the dust at the end.
let dust_threshold = I80F48::from(2) / token_state.price;
let mut amount = token_state.native_position;
if amount > dust_threshold {
// Sell
let txsig = mango_client.jupiter_swap( let txsig = mango_client.jupiter_swap(
token.mint_info.mint, token_mint,
quote_token.mint_info.mint, quote_mint,
amount.to_num::<u64>(), amount.to_num::<u64>(),
config.slippage, config.slippage,
client::JupiterSwapMode::ExactIn, client::JupiterSwapMode::ExactIn,
@ -94,42 +134,66 @@ pub fn zero_all_non_quote(
quote_token.name, quote_token.name,
txsig txsig
); );
Some(txsig) if !refresh_mango_account(account_fetcher, txsig)? {
return Ok(());
}
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.token_get(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
} else if token_state.native_position < 0 { } else if token_state.native_position < 0 {
let amount = (-token_state.native_position).ceil(); // Buy
let buy_amount = (-token_state.native_position).ceil()
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
let txsig = mango_client.jupiter_swap( let txsig = mango_client.jupiter_swap(
quote_token.mint_info.mint, quote_mint,
token.mint_info.mint, token_mint,
amount.to_num::<u64>(), buy_amount.to_num::<u64>(),
config.slippage, config.slippage,
client::JupiterSwapMode::ExactOut, client::JupiterSwapMode::ExactOut,
)?; )?;
log::info!( log::info!(
"bought {} {} for {} in tx {}", "bought {} {} for {} in tx {}",
token.native_to_ui(amount), token.native_to_ui(buy_amount),
token.name, token.name,
quote_token.name, quote_token.name,
txsig txsig
); );
Some(txsig) if !refresh_mango_account(account_fetcher, txsig)? {
} else {
None
};
// The swaps aim to close token positions on the account. That means sending
// a second swap would fail due to including the wrong set of health accounts.
if let Some(txsig) = maybe_txsig {
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
) {
// If we don't get fresh data, maybe the tx landed on a fork?
// Rebalance is technically still ok.
log::info!("could not refresh account data: {}", e);
return Ok(()); return Ok(());
} }
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.token_get(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
}
// Any remainder that could not be sold just gets withdrawn to ensure the
// TokenPosition is freed up
if amount > 0 && amount <= dust_threshold {
// TODO: fix to false once program updated to fix allow_borrow bug
let allow_borrow = true;
let txsig =
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
log::info!(
"withdrew {} {} to liqor wallet in {}",
token.native_to_ui(amount),
token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
return Ok(());
}
} else {
anyhow::bail!(
"unexpected {} position after rebalance swap: {} native",
token.name,
amount
);
} }
} }