Last active
February 25, 2022 19:56
-
-
Save y2kappa/3fc45df4a93ae604425f680470f480eb to your computer and use it in GitHub Desktop.
Liquidations bot hubble
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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