Skip to content

Instantly share code, notes, and snippets.

@y2kappa
Last active February 25, 2022 19:56
Show Gist options
  • Save y2kappa/3fc45df4a93ae604425f680470f480eb to your computer and use it in GitHub Desktop.
Save y2kappa/3fc45df4a93ae604425f680470f480eb to your computer and use it in GitHub Desktop.
Liquidations bot hubble
pub fn calc_system_mode(
global_deposited_collateral: &CollateralAmounts,
global_debt: u64,
prices: &TokenPrices,
) -> BorrowResult<(SystemMode, Decimal)> {
let _150 = Decimal::from_percent(150);
let tcr = CollateralInfo::calculate_collateral_value(
global_debt,
global_deposited_collateral,
prices,
)?
.collateral_ratio;
Ok((
if tcr < _150 {
SystemMode::Recovery
} else {
SystemMode::Normal
},
tcr,
))
}
#![allow(unaligned_references)]
use anchor_lang::prelude::*;
use anyhow::{format_err, Result};
use borrowing::state::CollateralTokenActive;
use borrowing::{
stability_pool::liquidations_queue,
state::{LiquidationsQueue, UserMetadata},
};
use decimal_wad::common::TryAdd;
use decimal_wad::decimal::Decimal;
use futures::future::join_all;
use log::info;
use rayon::iter::{ParallelDrainRange, ParallelIterator};
use rayon::prelude::ParallelSliceMut;
use std::path::PathBuf;
use strum::IntoEnumIterator;
use url::Url;
use std::time::Duration;
use structopt::StructOpt;
use hubble_bot::get_ids::get_liq_ids;
use hubble_bot::types::LiquidatorAccounts;
use hubble_bot::{consts, ok_or_continue, sentry_add_txn, sentry_error, sentry_info, sentry_init};
use hubble_bot::{ClientBuilder, Hubble, HubbleState};
use hubble_client::{client::SendTransaction, hubble::HubbleResult, HubbleProgram};
use solana_sdk::signer::Signer;
mod fs_setup;
mod maths;
mod web;
#[derive(StructOpt, Debug)]
#[structopt(name = "Hubble liquidations bot")]
struct Opt {
#[structopt(long)]
integration_test: bool,
#[structopt(long, env, default_value = "1000")]
poll_interval_ms: u64,
/// Connect to local solana validator
#[structopt(long, env, default_value = "http://localhost:8899")]
validator_rpc_url: Url,
/// Liquidator keypair
#[structopt(long, env, parse(from_os_str))]
keypair: PathBuf,
/// Liquidator atas
#[structopt(long, env, parse(try_from_str))]
atas: PathBuf,
/// Program Id
#[structopt(long, env, parse(try_from_str))]
program_id: Pubkey,
/// Config file
#[structopt(long, env, parse(from_os_str))]
config: PathBuf,
/// Run with embedded webserver
// todo this should be env but there is a bug in clap https://github.com/clap-rs/clap/issues/2539
#[structopt(short, long)]
server: bool,
/// Embedded webserver port
/// Only valid if --server is also used
#[structopt(long, env, default_value = "8080")]
server_port: u16,
}
#[tokio::main]
async fn main() -> Result<()> {
log4rs::init_config(fs_setup::setup_log_files()?)?;
let opts = Opt::from_args();
info!("Starting liquidation bot ...");
info!("Started with {:#?}", opts);
if opts.server {
tokio::spawn(warp::serve(web::routes::routes()).run(([0, 0, 0, 0], opts.server_port)));
}
let _guard = sentry_init!();
// build our signer
let liquidator_kp = solana_sdk::signature::read_keypair_file(opts.keypair.clone())
.map_err(|_| format_err!("failed to read keypair from {:?}", opts.keypair))?;
// build the hubble rpc client
let client_builder = ClientBuilder::new(&opts.validator_rpc_url);
let hubble_client =
hubble_client::build_rpc_client(client_builder.build(), opts.program_id, liquidator_kp);
let hubble = Hubble::new(opts.program_id, &opts.config)?;
let hubble_state = hubble.fetch_state(&client_builder).await?;
let liquidator_atas = get_liq_ids(&opts.atas)
.map_err(|_| format_err!("failed to read keypair from {:?}", opts.atas))?;
if opts.integration_test {
tokio::time::sleep(Duration::from_secs(1)).await;
println!(r#"{{"state": "ready", "hubble_state": {}}}"#, hubble_state);
}
loop {
let hubble_state = ok_or_continue!(hubble.fetch_state(&client_builder).await);
info!(
"Borrowing market state ID {}:\n Number of users {}\n Number of active users {}\n Borrowed stablecoin: {}\n",
hubble_state.hubble.borrowing_market_state,
hubble_state.market.num_users,
hubble_state.market.num_active_users,
hubble_state.market.stablecoin_borrowed,
);
if let Err(err) =
process_liquidation_queue(&opts, &hubble_client, &hubble_state, &liquidator_atas).await
{
tracing::error!(error=?err, "process_liquidation_queue::error");
if opts.integration_test {
println!(
r#"{{"type": "process_liquidation_queue::error", "error": "{}" }}"#,
err
);
} else {
sentry_error!("Error when processing the liquidation queue");
sentry_anyhow::capture_anyhow(&err);
}
}
tokio::time::sleep(Duration::from_millis(opts.poll_interval_ms)).await;
}
}
fn get_clear_liquidation_token_accounts<'a>(
hubble_state: &'a HubbleState,
liquidator_atas: &'a LiquidatorAccounts,
token: CollateralTokenActive,
) -> (&'a Pubkey, &'a Pubkey, &'a Pubkey) {
let (clearer_ata, collateral_vault, liquidation_rewards_vault) = match token {
CollateralTokenActive::SOL => (
&liquidator_atas.sol_ata,
&hubble_state.borrowing_vaults.collateral_vault_sol,
&hubble_state.stability_vaults.liquidation_rewards_vault_sol,
),
CollateralTokenActive::ETH => (
&liquidator_atas.eth_ata,
&hubble_state.borrowing_vaults.collateral_vault_eth,
&hubble_state.stability_vaults.liquidation_rewards_vault_eth,
),
CollateralTokenActive::BTC => (
&liquidator_atas.btc_ata,
&hubble_state.borrowing_vaults.collateral_vault_btc,
&hubble_state.stability_vaults.liquidation_rewards_vault_btc,
),
CollateralTokenActive::SRM => (
&liquidator_atas.srm_ata,
&hubble_state.borrowing_vaults.collateral_vault_srm,
&hubble_state.stability_vaults.liquidation_rewards_vault_srm,
),
CollateralTokenActive::RAY => (
&liquidator_atas.ray_ata,
&hubble_state.borrowing_vaults.collateral_vault_ray,
&hubble_state.stability_vaults.liquidation_rewards_vault_ray,
),
CollateralTokenActive::FTT => (
&liquidator_atas.ftt_ata,
&hubble_state.borrowing_vaults.collateral_vault_ftt,
&hubble_state.stability_vaults.liquidation_rewards_vault_ftt,
),
CollateralTokenActive::MSOL => (
&liquidator_atas.msol_ata,
&hubble_state.borrowing_vaults.collateral_vault_msol,
&hubble_state.stability_vaults.liquidation_rewards_vault_msol,
),
};
(clearer_ata, collateral_vault, liquidation_rewards_vault)
}
async fn clear_liquidation_gains<ST: SendTransaction, S: Signer>(
hubble_client: &HubbleProgram<ST, S>,
hubble_state: &HubbleState,
liquidator_atas: &LiquidatorAccounts,
) -> Result<()> {
for token in CollateralTokenActive::iter() {
let (clearer_ata, collateral_vault, liquidation_rewards_vault) =
get_clear_liquidation_token_accounts(hubble_state, liquidator_atas, token);
hubble_client
.clear_liquidation_gain_token(
&hubble_state.hubble.program,
&hubble_client.get_payer().pubkey(),
clearer_ata,
&hubble_state.hubble.borrowing_market_state,
&hubble_state.hubble.global_config,
&hubble_state.hubble.borrowing_vaults,
&hubble_state.hubble.stability_pool_state,
&hubble_state.hubble.stability_vaults,
&hubble_state.stability_pool_state.liquidations_queue,
collateral_vault,
&hubble_state.borrowing_vaults.collateral_vaults_authority,
liquidation_rewards_vault,
token.into(),
)
.await?;
}
Ok(())
}
async fn try_liquidate<ST: SendTransaction, S: Signer>(
hubble_client: &HubbleProgram<ST, S>,
hubble_state: &HubbleState,
user_metadata: &UserMetadata,
) -> HubbleResult<ST::Output> {
hubble_client
.try_liquidate(
&hubble_state.hubble.program,
&hubble_client.get_payer().pubkey(),
&hubble_state.hubble.borrowing_market_state,
&hubble_state.hubble.global_config,
&hubble_state.hubble.stability_pool_state,
&user_metadata.metadata_pk,
&hubble_state.stability_pool_state.epoch_to_scale_to_sum,
&hubble_state.hubble.stability_vaults,
&hubble_state.hubble.borrowing_vaults,
&hubble_state.stability_pool_state.liquidations_queue,
&hubble_state.market.stablecoin_mint,
&hubble_state.market.stablecoin_mint_authority,
&hubble_state
.stability_vaults
.stablecoin_stability_pool_vault,
&hubble_state
.stability_vaults
.stablecoin_stability_pool_vault_authority,
&hubble_state.hubble.oracle_mappings,
&hubble_state.oracle_mappings.pyth_sol_price_info,
&hubble_state.oracle_mappings.pyth_eth_price_info,
&hubble_state.oracle_mappings.pyth_btc_price_info,
&hubble_state.oracle_mappings.pyth_srm_price_info,
&hubble_state.oracle_mappings.pyth_ray_price_info,
&hubble_state.oracle_mappings.pyth_ftt_price_info,
&hubble_state.oracle_mappings.pyth_msol_price_info,
)
.await
}
async fn process_liquidation_queue<ST: SendTransaction, S: Signer>(
opts: &Opt,
hubble_client: &HubbleProgram<ST, S>,
hubble_state: &HubbleState,
liquidator_atas: &LiquidatorAccounts,
) -> Result<()> {
let liquidation_queue = hubble_state
.deserialize::<LiquidationsQueue>(&hubble_state.stability_pool_state.liquidations_queue)?;
let mcr = maths::mcr(&hubble_state.market, &hubble_state.token_prices)?;
let margin = Decimal::from_bps(10);
let mcr_range = mcr.try_add(margin).unwrap();
// Check if there is anything on the queue to be cleared
let pending_events = liquidations_queue::num_pending_events(&liquidation_queue);
if pending_events > 0 {
match clear_liquidation_gains(hubble_client, hubble_state, liquidator_atas).await {
Ok(()) => {
sentry_info!("Clear gains tx executed successfully");
}
Err(err) => {
tracing::error!(error=?err, "clear_liquidation_gains::error");
if opts.integration_test {
println!(
r#"{{"type": "clear_liquidation_gains::error", "error": "{}" }}"#,
&err
);
} else {
sentry_error!("Error when sending the clear gains tx");
sentry_anyhow::capture_anyhow(&err);
}
}
};
}
if hubble_state.market.num_active_users > 1 {
let mut candidate_users: Vec<&UserMetadata> = hubble_state
.users
.values()
.collect::<Vec<&UserMetadata>>()
.par_drain(..)
.filter(|user| {
let position_data = hubble_bot::math::evaluate_position(
(*user).clone(),
&hubble_state.market,
&hubble_state.token_prices,
)
.unwrap();
position_data.collateral_ratio < mcr_range
})
.collect();
candidate_users.par_sort_unstable_by_key(|user| {
let position_data = hubble_bot::math::evaluate_position(
(*user).clone(),
&hubble_state.market,
&hubble_state.token_prices,
)
.unwrap();
position_data.collateral_ratio
});
let mut futures = Vec::new();
for user in candidate_users {
futures.push(try_liquidate(hubble_client, hubble_state, user));
}
let results = join_all(futures).await;
for result in results {
match result {
Ok(sig) => {
sentry_info!("Liquidate tx executed successfully");
sentry_add_txn!(sig);
}
Err(err) => {
tracing::error!(error=?err, "try_liquidate::error");
if opts.integration_test {
println!(
r#"{{"type": "try_liquidate::error", "error": "{}" }}"#,
&err
);
} else {
sentry_error!("Error when sending the try liquidate tx");
sentry_anyhow::capture_anyhow(&err.into());
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use anchor_lang::prelude::Pubkey;
use anchor_lang::AccountDeserialize;
use anchor_lang::Discriminator;
use borrowing::state::GlobalConfig;
use borrowing::state::StabilityPoolState;
use hubble_client::client::ProgramClient;
use solana_client::rpc_client::RpcClient;
use solana_sdk::account::Account;
#[test]
fn test_deserialize() {
let client = RpcClient::new("api.solana-mainnet.com".to_string());
// global config
let gc_pk = Pubkey::from_str("8HuhMF7ppoYdPD9fAdn9d9ZU1rL4ipty6CJEcZ5F17o1").unwrap();
let acc = client.get_account(&gc_pk).unwrap();
let val = deserialize::<GlobalConfig>(&acc).unwrap();
println!("GC {:?}", val);
// stability pool state
let sp_pk = Pubkey::from_str("245U3MMJ57YDGUSYRQxsJurqUhnXK4mjjvavbrvSn3uh").unwrap();
let acc = client.get_account(&sp_pk).unwrap();
let val = deserialize::<StabilityPoolState>(&acc).unwrap();
println!("SP {:?}", val);
}
/// deserialization of accounts
pub fn deserialize<T: AccountDeserialize + Discriminator>(
account: &Account,
) -> Result<T, &'static str> {
let discriminator = &account.data[..8];
if discriminator != T::discriminator() {
return Err("err2");
}
let mut data: &[u8] = &account.data;
let deserialized: T = T::try_deserialize(&mut data).map_err(|_| "err")?;
Ok(deserialized)
}
}
use borrowing::{
borrowing_market::liquidation_calcs,
state::{BorrowingMarketState, TokenPrices},
BorrowResult,
};
use decimal_wad::decimal::Decimal;
pub fn mcr(market: &BorrowingMarketState, prices: &TokenPrices) -> BorrowResult<Decimal> {
let (mode, _tcr) = liquidation_calcs::calc_system_mode(
&market.deposited_collateral,
market.stablecoin_borrowed,
prices,
)?;
Ok(match mode {
liquidation_calcs::SystemMode::Normal => Decimal::from_percent(110),
liquidation_calcs::SystemMode::Recovery => Decimal::from_percent(150),
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment