Skip to content
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Error handling
anyhow = "1"
thiserror = "2"

# Async trait objects
async-trait = "0.1"

# CLI
clap = { version = "4", features = ["derive"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/charon-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ description = "Shared types, traits, and config for Charon"
[dependencies]
alloy = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
thiserror = { workspace = true }
5 changes: 4 additions & 1 deletion crates/charon-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Charon core — shared types, traits, and config.

pub mod traits;
pub mod types;

pub use traits::{LendingProtocol, LendingProtocolError, Result as LendingResult};
pub use types::{
FlashLoanSource, LiquidationOpportunity, Position, ProtocolId, SwapRoute,
FlashLoanSource, LiquidationOpportunity, LiquidationParams, Position,
ProtocolId, SwapRoute,
};
117 changes: 117 additions & 0 deletions crates/charon-core/src/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Traits that define the contracts between layers of the bot.
//!
//! `LendingProtocol` is the main boundary: each protocol (Venus, Aave, …)
//! implements it, and the scanner/executor consume it without caring which
//! protocol is behind the adapter.

use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, U256};
use async_trait::async_trait;

use crate::types::{LiquidationParams, Position, ProtocolId};

/// Structured error for lending-protocol adapter operations.
///
/// Callers match on the variant to decide retry vs skip vs abort:
/// - `Rpc` — transient transport failure, retry with backoff.
/// - `InvalidPosition` — borrower data is malformed or inconsistent, skip.
/// - `UnsupportedAsset` — asset not listed on this protocol, skip market.
/// - `ProtocolState` — an invariant broken (e.g. vToken ↔ underlying mismatch),
/// escalate / alert.
/// - `Abi` — calldata encode/decode failure, treat as bug.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LendingProtocolError {
#[error("rpc error: {0}")]
Rpc(String),
#[error("invalid position: {0}")]
InvalidPosition(String),
#[error("unsupported asset: {0}")]
UnsupportedAsset(Address),
#[error("protocol state: {0}")]
ProtocolState(String),
#[error("abi: {0}")]
Abi(String),
}

/// Shorthand `Result` for trait methods.
pub type Result<T> = std::result::Result<T, LendingProtocolError>;

/// A lending protocol adapter.
///
/// - Scanner calls [`fetch_positions`](LendingProtocol::fetch_positions) on
/// each block (or on relevant events) to pick up health-factor changes.
/// - Scanner also calls [`get_health_factor`](LendingProtocol::get_health_factor)
/// for single-borrower refreshes on mempool oracle updates.
/// - Executor calls [`get_liquidation_params`](LendingProtocol::get_liquidation_params)
/// and [`build_liquidation_calldata`](LendingProtocol::build_liquidation_calldata)
/// when a position crosses the liquidation threshold, to encode the
/// on-chain call to `CharonLiquidator.executeLiquidation(...)`.
///
/// Implementations must be `Send + Sync` so the scanner can hold them in
/// `Arc<dyn LendingProtocol>` and share across tokio tasks.
///
/// `#[async_trait]` is used for `dyn`-compatibility. Native AFIT (Rust 1.75+)
/// is not object-safe without `trait_variant` bounds, and the scanner stores
/// protocol adapters behind `Arc<dyn>`; the per-call boxing overhead is
/// negligible relative to the RPC round-trip dominating every method.
#[async_trait]
pub trait LendingProtocol: Send + Sync {
/// Stable identifier for this protocol.
fn id(&self) -> ProtocolId;

/// Fetch current position state for the given borrowers, pinned to
/// a specific block for snapshot consistency.
///
/// Scanner passes `BlockNumberOrTag::Latest` for routine scans and a
/// specific block number to reconcile state against an observed head.
/// Mempool-driven paths pass `BlockNumberOrTag::Pending`.
async fn fetch_positions(
&self,
borrowers: &[Address],
block: BlockNumberOrTag,
) -> Result<Vec<Position>>;

/// Return the 1e18-scaled health factor for a single borrower at `block`.
///
/// Cheaper than `fetch_positions` for gating decisions (e.g. mempool
/// monitor refresh) because only one aggregate call is needed.
async fn get_health_factor(
&self,
borrower: Address,
block: BlockNumberOrTag,
) -> Result<U256>;

/// Close factor for a market, 1e18-scaled.
///
/// Venus default is 0.5e18 (50% of debt per liquidation) but is
/// per-market and governed. Aave V3 caps at 0.5e18 structurally.
/// Required by the profit calculator to bound `repay_amount`.
fn get_close_factor(&self, market: Address) -> Result<U256>;

/// Liquidation incentive for the given collateral market, 1e18-scaled.
///
/// Venus uses `liquidationIncentiveMantissa` (default 1.1e18 = 10%
/// bonus) and it is per-market. Aave uses `LiquidationBonus` in bps.
/// Profit calculator scales seized collateral by this value.
async fn get_liquidation_incentive(
&self,
collateral_market: Address,
) -> Result<U256>;

/// Compute protocol-specific liquidation parameters for a position.
///
/// Handles close-factor math (Aave's 50% cap, Compound's 100% absorb,
/// etc.) and resolves any protocol-specific token addresses (e.g., Venus
/// vToken addresses).
fn get_liquidation_params(
&self,
position: &Position,
) -> Result<LiquidationParams>;

/// Encode the ABI calldata for `CharonLiquidator.executeLiquidation(...)`.
fn build_liquidation_calldata(
&self,
params: &LiquidationParams,
) -> Result<Vec<u8>>;
}
24 changes: 24 additions & 0 deletions crates/charon-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ pub struct SwapRoute {
pub pool_fee: u32,
}

/// Protocol-specific parameters needed to build a liquidation call.
///
/// Every lending protocol has its own quirks (Aave allows partial liquidation,
/// Compound absorbs 100%, Venus uses vToken addresses, etc.). Each variant
/// captures exactly the fields its protocol needs — no shared bag of options.
///
/// Marked `#[non_exhaustive]` at both enum and variant level so adding new
/// variants (AaveV3, Compound, …) or new fields on existing variants is not
/// a semver-breaking change for downstream exhaustive matches.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LiquidationParams {
#[non_exhaustive]
Venus {
borrower: Address,
/// vToken of the collateral asset (the token seized).
collateral_vtoken: Address,
/// vToken of the debt asset (the token repaid).
debt_vtoken: Address,
/// Amount of debt to repay, in underlying-debt-token units (not vToken units).
repay_amount: U256,
},
}

/// A profitable liquidation that has passed all off-chain gates and is
/// ready to be built into a transaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down