Skip to content

Commit

Permalink
fix: Various fixes for the Liquidation process (#57)
Browse files Browse the repository at this point in the history
* fix(liquidation_process): Various fixes for the liquidation to work

* fix(liquidation_process): Almost liquidable logs

* fix(liquidation_process): Cleaning

* fix(liquidation_process): Indexin size
  • Loading branch information
akhercha authored Sep 26, 2024
1 parent 55a1ea7 commit 2bf2481
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vesu-liquidator"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
license = "MIT"
homepage = "https://www.vesu.xyz/"
Expand Down
7 changes: 5 additions & 2 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ fn main() {
let contract_files =
strk_abi_base.join(format!("vesu_liquidate_{abi_file}.contract_class.json"));
let contract_files = contract_files.to_str().unwrap();
let abigen = cainome::rs::Abigen::new(abi_file, contract_files)
.with_derives(vec!["serde::Deserialize".to_string()]);
let abigen = cainome::rs::Abigen::new(abi_file, contract_files).with_derives(vec![
"Debug".into(),
"serde::Deserialize".into(),
"serde::Serialize".into(),
]);

abigen
.generate()
Expand Down
8 changes: 6 additions & 2 deletions src/services/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::{
utils::conversions::{apibara_field_as_felt, felt_as_apibara_field},
};

const INDEXING_STREAM_CHUNK_SIZE: usize = 1024;
const INDEXING_STREAM_CHUNK_SIZE: usize = 1;

pub struct IndexerService {
config: Config,
Expand Down Expand Up @@ -167,7 +167,11 @@ impl IndexerService {
}
let position_key = new_position.key();
if self.seen_positions.insert(position_key) {
tracing::info!("[🔍 Indexer] Found new position 0x{:x}", new_position.key());
tracing::info!(
"[🔍 Indexer] Found new position 0x{:x} at block {}",
new_position.key(),
block_number
);
}
match self.positions_sender.try_send((block_number, new_position)) {
Ok(_) => {}
Expand Down
2 changes: 1 addition & 1 deletion src/services/monitoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ impl MonitoringService {
)
.await?;
let execution_fees = self.account.estimate_fees_cost(&liquidation_txs).await?;

let slippage = BigDecimal::new(BigInt::from(5), 2);
let slippage_factor = BigDecimal::from(1) - slippage;

Ok((
(simulated_profit * slippage_factor) - execution_fees,
liquidation_txs,
Expand Down
141 changes: 100 additions & 41 deletions src/types/position.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use anyhow::{anyhow, Ok, Result};
use anyhow::{anyhow, Context, Ok, Result};
use apibara_core::starknet::v1alpha2::FieldElement;
use bigdecimal::num_bigint::BigInt;
use bigdecimal::BigDecimal;
use cainome::cairo_serde::CairoSerde;
use bigdecimal::{BigDecimal, FromPrimitive};
use cainome::cairo_serde::{ContractAddress, U256};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use starknet::core::types::{BlockId, BlockTag, FunctionCall};
use starknet::core::types::{Call, Felt};
use starknet::providers::jsonrpc::HttpTransport;
Expand All @@ -15,26 +16,27 @@ use std::hash::{Hash, Hasher};
use std::sync::Arc;
use tokio::sync::RwLock;

use crate::bindings::liquidate::{Liquidate, LiquidateParams, RouteNode, Swap, TokenAmount, I129};
use crate::bindings::liquidate::{
Liquidate, LiquidateParams, PoolKey, RouteNode, Swap, TokenAmount, I129,
};

use crate::config::{Config, LiquidationMode, LIQUIDATION_CONFIG_SELECTOR};
use crate::services::oracle::LatestOraclePrices;
use crate::storages::Storage;
use crate::utils::apply_overhead;
use crate::utils::constants::VESU_RESPONSE_DECIMALS;
use crate::utils::conversions::big_decimal_to_felt;
use crate::{types::asset::Asset, utils::conversions::apibara_field_as_felt};

use super::account::StarknetAccount;

/// Threshold for which we consider a position almost liquidable.
const ALMOST_LIQUIDABLE_THRESHOLD: f64 = 0.03;

/// Thread-safe wrapper around the positions.
/// PositionsMap is a map between position position_key <=> position.
pub struct PositionsMap(pub Arc<RwLock<HashMap<u64, Position>>>);

#[derive(Deserialize)]
pub struct EkuboApiGetRouteResponse {
route: Vec<RouteNode>,
}

impl PositionsMap {
pub fn new() -> Self {
Self(Arc::new(RwLock::new(HashMap::new())))
Expand Down Expand Up @@ -173,22 +175,31 @@ impl Position {
.await
.expect("failed to retrieve ltv ratio");

let is_liquidable = ltv_ratio > self.lltv;
if is_liquidable {
self.debug_position_state(is_liquidable, ltv_ratio);
let is_liquidable = ltv_ratio >= self.lltv.clone();
let is_almost_liquidable = ltv_ratio
>= self.lltv.clone() - BigDecimal::from_f64(ALMOST_LIQUIDABLE_THRESHOLD).unwrap();
if is_liquidable || is_almost_liquidable {
self.debug_position_state(is_liquidable, is_almost_liquidable, ltv_ratio);
}
is_liquidable
}

/// Prints the status of the position and if it's liquidable or not.
fn debug_position_state(&self, is_liquidable: bool, ltv_ratio: BigDecimal) {
fn debug_position_state(
&self,
is_liquidable: bool,
is_almost_liquidable: bool,
ltv_ratio: BigDecimal,
) {
tracing::info!(
"{} is at ratio {:.2}%/{:.2}% => {}",
self,
ltv_ratio * BigDecimal::from(100),
self.lltv.clone() * BigDecimal::from(100),
if is_liquidable {
"liquidable!".green()
} else if is_almost_liquidable {
"almost liquidable 🔫".yellow()
} else {
"NOT liquidable.".red()
}
Expand Down Expand Up @@ -248,14 +259,77 @@ impl Position {
"https://mainnet-api.ekubo.org/quote/{amount_as_string}/{from_token}/{to_token}"
);
let http_client = reqwest::Client::new();

let response = http_client.get(ekubo_api_endpoint).send().await?;
let ekubo_response: EkuboApiGetRouteResponse = response.json().await?;
Ok(ekubo_response.route)

if !response.status().is_success() {
anyhow::bail!("API request failed with status: {}", response.status());
}

let response_text = response.text().await?;

let json_value: Value = serde_json::from_str(&response_text)?;

// We have to deserialize by hand into a Vec of [RouteNode].
// TODO: Make this cleaner!
let route = json_value["route"]
.as_array()
.context("'route' is not an array")?
.iter()
.map(|node| {
let pool_key = &node["pool_key"];
Ok(RouteNode {
pool_key: PoolKey {
token0: ContractAddress(Felt::from_hex(
pool_key["token0"]
.as_str()
.context("token0 is not a string")?,
)?),
token1: ContractAddress(Felt::from_hex(
pool_key["token1"]
.as_str()
.context("token1 is not a string")?,
)?),
fee: u128::from_str_radix(
pool_key["fee"]
.as_str()
.context("fee is not a string")?
.trim_start_matches("0x"),
16,
)
.context("Failed to parse fee as u128")?,
tick_spacing: pool_key["tick_spacing"]
.as_u64()
.context("tick_spacing is not a u64")?
as u128,
extension: ContractAddress(Felt::from_hex(
pool_key["extension"]
.as_str()
.context("extension is not a string")?,
)?),
},
sqrt_ratio_limit: U256::from_bytes_be(
&Felt::from_hex(
node["sqrt_ratio_limit"]
.as_str()
.context("sqrt_ratio_limit is not a string")?,
)
.unwrap()
.to_bytes_be(),
),
skip_ahead: node["skip_ahead"]
.as_u64()
.context("skip_ahead is not a u64")?
as u128,
})
})
.collect::<Result<Vec<RouteNode>>>()?;

Ok(route)
}

/// Returns the TX necessary to liquidate this position (approve + liquidate).
// See: https://github.com/vesuxyz/vesu-v1/blob/a2a59936988fcb51bc85f0eeaba9b87cf3777c49/src/singleton.cairo#L1624
#[allow(unused)]
pub async fn get_liquidation_txs(
&self,
account: &StarknetAccount,
Expand All @@ -266,59 +340,44 @@ impl Position {
// The amount is in negative because contract use a inverted route to ensure that we get the exact amount of debt token
let liquidate_token = TokenAmount {
token: cainome::cairo_serde::ContractAddress(self.debt.address),
amount: I129::cairo_deserialize(&[Felt::ZERO], 0)?,
amount: I129 { mag: 0, sign: true },
};

let withdraw_token = TokenAmount {
token: cainome::cairo_serde::ContractAddress(self.collateral.address),
amount: I129::cairo_deserialize(&[Felt::ZERO], 0)?,
amount: I129 { mag: 0, sign: true },
};

// As mentionned before the route is inverted for precision purpose
let liquidate_route: Vec<RouteNode> = Position::get_ekubo_route(
String::from("0"),
String::from("10"), // TODO: Investigate the behavior of this value with the Vesu Liquidate contract
self.debt.name.clone(),
self.collateral.name.clone(),
)
.await?;
let liquidate_limit: u128 = u128::MAX;

let withdraw_route: Vec<RouteNode> = Position::get_ekubo_route(
String::from("0"),
String::from("10"), // TODO: Investigate the behavior of this value with the Vesu Liquidate contract
self.debt.name.clone(),
String::from("usdc"),
)
.await?;
let withdraw_limit: u128 = u128::MAX;

let liquidate_contract = Liquidate::new(liquidate_contract, account.0.clone());

let liquidate_swap = Swap {
route: liquidate_route,
token_amount: liquidate_token,
limit_amount: liquidate_limit,
limit_amount: u128::MAX,
};
let withdraw_swap = Swap {
route: withdraw_route,
token_amount: withdraw_token,
limit_amount: withdraw_limit,
limit_amount: u128::MAX,
};

let min_col_to_retrieve: [u8; 32] = minimum_collateral_to_retrieve
.as_bigint_and_exponent()
.0
.to_bytes_be()
.1
.try_into()
.expect("failed to parse min col to retrieve");

let debt_to_repay: [u8; 32] = amount_to_liquidate
.as_bigint_and_exponent()
.0
.to_bytes_be()
.1
.try_into()
.expect("failed to parse min col to retrieve");
let min_col_to_retrieve = big_decimal_to_felt(minimum_collateral_to_retrieve);
let debt_to_repay = big_decimal_to_felt(amount_to_liquidate);

let liquidate_params = LiquidateParams {
pool_id: self.pool_id,
Expand All @@ -327,11 +386,11 @@ impl Position {
user: cainome::cairo_serde::ContractAddress(self.user_address),
recipient: cainome::cairo_serde::ContractAddress(account.account_address()),
min_collateral_to_receive: cainome::cairo_serde::U256::from_bytes_be(
&min_col_to_retrieve,
&min_col_to_retrieve.to_bytes_be(),
),
liquidate_swap,
withdraw_swap,
debt_to_repay: cainome::cairo_serde::U256::from_bytes_be(&debt_to_repay),
debt_to_repay: cainome::cairo_serde::U256::from_bytes_be(&debt_to_repay.to_bytes_be()),
};

let liquidate_call = liquidate_contract.liquidate_getcall(&liquidate_params);
Expand Down
6 changes: 5 additions & 1 deletion src/utils/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ pub fn apibara_field_as_felt(value: &FieldElement) -> Felt {

/// Converts a BigDecimal to a U256.
pub fn big_decimal_to_u256(value: BigDecimal) -> U256 {
U256::from(big_decimal_to_felt(value))
}

pub fn big_decimal_to_felt(value: BigDecimal) -> Felt {
let (amount, _): (BigInt, _) = value.as_bigint_and_exponent();
U256::from(Felt::from(amount.clone()))
Felt::from(amount.clone())
}

#[cfg(test)]
Expand Down

0 comments on commit 2bf2481

Please sign in to comment.