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
14 changes: 14 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "3"
members = [
"crates/charon-core",
"crates/charon-flashloan",
"crates/charon-protocols",
"crates/charon-scanner",
"crates/charon-cli",
Expand Down Expand Up @@ -65,5 +66,6 @@ dotenvy = "0.15"

# Internal crates
charon-core = { path = "crates/charon-core" }
charon-flashloan = { path = "crates/charon-flashloan" }
charon-protocols = { path = "crates/charon-protocols" }
charon-scanner = { path = "crates/charon-scanner" }
3 changes: 3 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ comptroller = "0xfd36e2c2a6789db23113685031d7f16329158384"
chain = "bnb"
# Aave V3 Pool on BSC (used for flashLoanSimple — 0.05% fee)
pool = "0x6807dc923806fe8fd134338eabca509979a7e0cb"
# Aave V3 PoolDataProvider on BSC — resolves aTokens and reserve
# configuration bitmaps (paused / frozen flags) for the adapter.
data_provider = "0x41393e5e337606dc3821075Af65AeE84D7688CBD"

# ── Deployed liquidator contracts ─────────────────────────────────────────
# Populated once CharonLiquidator.sol is deployed on BSC mainnet. Do not
Expand Down
2 changes: 1 addition & 1 deletion crates/charon-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ workspace = true
alloy = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true }
toml = { workspace = true }
thiserror = { workspace = true }
6 changes: 6 additions & 0 deletions crates/charon-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ pub struct FlashLoanConfig {
pub chain: String,
/// Pool / vault address (Aave V3 Pool, Balancer Vault, etc.).
pub pool: Address,
/// Optional auxiliary data-provider address used by some sources
/// to resolve per-asset state (e.g. Aave V3 `PoolDataProvider`
/// for aToken lookup and reserve configuration bitmaps). `None`
/// for sources that don't need one (Balancer, Uniswap).
#[serde(default)]
pub data_provider: Option<Address>,
}

/// Address of the deployed `CharonLiquidator` contract on a chain.
Expand Down
158 changes: 158 additions & 0 deletions crates/charon-core/src/flashloan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! Flash-loan provider abstraction.
//!
//! Every flash-loan source (Balancer V2, Aave V3, Uniswap V3, ...) plugs
//! in through the [`FlashLoanProvider`] trait. The router (in
//! `charon-flashloan`) walks a list of providers in fee-priority order
//! and picks the cheapest source with enough liquidity for the token +
//! amount it needs to borrow.
//!
//! # Fee-rate unit
//!
//! All fee rates in this module are expressed in **millionths (1e6)**,
//! the same convention Uniswap uses for pool-fee tiers. Examples:
//!
//! * Balancer V2 flash loan: `0` (fee-free)
//! * Aave V3 flash loan: `500` = 0.05%
//! * Uniswap V3 0.30% pool: `3_000`
//! * Uniswap V3 1.00% pool: `10_000`
//!
//! This intentionally differs from Aave's on-chain encoding, which
//! stores the premium as 4-decimal percent (e.g. `5` means 0.05%).
//! Adapters are expected to convert into millionths at construction.
//!
//! The trait is kept deliberately thin:
//!
//! * `available_liquidity` — can the source cover the requested amount?
//! * `fee_rate_millionths` — how expensive is borrowing from this source?
//! * `quote` — one-shot helper that rolls the two checks above into a
//! ready-to-use [`FlashLoanQuote`], or `None` when the source cannot
//! serve this borrow.
//! * `build_flashloan_calldata` — encode the outer call to the source
//! (e.g. `Pool.flashLoanSimple`, `Vault.flashLoan`) that wraps the
//! inner liquidation parameters the protocol adapter produced.

use alloy::primitives::{Address, U256};
use async_trait::async_trait;
use thiserror::Error;

use crate::types::FlashLoanSource;

/// Errors returned by [`FlashLoanProvider`] implementations.
///
/// The variants capture the failure modes the router needs to
/// distinguish (e.g. a paused reserve is not the same as a transient
/// RPC hiccup). `#[non_exhaustive]` so new sources can extend the
/// taxonomy without breaking downstream `match`es.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FlashLoanError {
/// The source does not hold enough liquidity of the requested
/// token to cover the borrow.
#[error("insufficient liquidity: have {have}, need {need}")]
InsufficientLiquidity { have: U256, need: U256 },

/// The reserve is paused or frozen on the source (Aave V3 pauses
/// reserves during incidents, freezes them during deprecation).
#[error("reserve paused or frozen for asset {asset}")]
ReservePaused { asset: Address },

/// The adapter was connected to the wrong chain.
#[error("chain id mismatch: expected {expected}, got {actual}")]
ChainIdMismatch { expected: u64, actual: u64 },

/// An RPC call failed (timeout, provider error, decode failure, ...).
#[error("rpc error: {0}")]
Rpc(String),

/// Any other unclassified failure.
#[error("flash-loan provider error: {0}")]
Other(String),
}

impl FlashLoanError {
/// Convenience for wrapping an `anyhow::Error` into
/// [`FlashLoanError::Rpc`] at call sites that still bubble up
/// generic RPC failures.
pub fn rpc<E: std::fmt::Display>(err: E) -> Self {
Self::Rpc(err.to_string())
}

/// Convenience for unclassified failures.
pub fn other<E: std::fmt::Display>(err: E) -> Self {
Self::Other(err.to_string())
}
}

/// Snapshot of a single flash-loan opportunity from one source.
///
/// The router produces these for the top-ranked liquidations; the tx
/// builder consumes them alongside the inner liquidation parameters to
/// encode the final on-chain call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlashLoanQuote {
pub source: FlashLoanSource,
/// Chain the source is deployed on.
pub chain_id: u64,
/// Token being borrowed (the debt token of the liquidation target).
pub token: Address,
/// Amount to borrow, in the token's smallest unit.
pub amount: U256,
/// Absolute fee to repay alongside `amount`, same units as `amount`.
pub fee: U256,
/// Fee rate in millionths (1e6). `500` = 0.05% (Aave V3). `0` =
/// Balancer / Maker flash mint. See module docs for unit rationale.
pub fee_rate_millionths: u32,
/// Address to call to initiate the flash loan
/// (Aave pool, Balancer vault, Uniswap pool, ...).
pub pool_address: Address,
}

/// Flash-loan source adapter.
///
/// Implementations live in `charon-flashloan` (one per source) and are
/// consumed by the router as trait objects. The trait is `Send + Sync`
/// so a provider can be shared across the block listener, scanner, and
/// executor tasks without copying state.
///
/// All fees are returned in **millionths (1e6)** — see module docs.
#[async_trait]
pub trait FlashLoanProvider: Send + Sync {
/// Which concrete source this provider wraps.
fn source(&self) -> FlashLoanSource;

/// Chain id the source is deployed on.
fn chain_id(&self) -> u64;

/// Current liquidity available for `token`, in its smallest unit.
/// Returns `0` when the source does not support the token at all,
/// or [`FlashLoanError::ReservePaused`] when the reserve is
/// administratively disabled.
async fn available_liquidity(&self, token: Address) -> Result<U256, FlashLoanError>;

/// Fee rate in millionths (1e6). `500` = 0.05% (Aave V3). `0` =
/// Balancer / Maker flash mint. See module docs.
fn fee_rate_millionths(&self) -> u32;

/// Roll `available_liquidity` + `fee_rate_millionths` into a ready
/// quote for the requested borrow, or `None` when the source
/// cannot cover the amount on this chain/token.
async fn quote(
&self,
token: Address,
amount: U256,
) -> Result<Option<FlashLoanQuote>, FlashLoanError>;

/// Encode the outer flash-loan initiation call.
///
/// `liquidation_params` is whatever the flash-loan recipient
/// contract (`CharonLiquidator.sol`) needs inside its callback —
/// typically the ABI-encoded protocol adapter liquidation
/// parameters. It MUST be non-empty; every real
/// `executeOperation` implementation decodes this payload and will
/// revert on an empty buffer.
fn build_flashloan_calldata(
&self,
quote: &FlashLoanQuote,
liquidation_params: &[u8],
) -> Result<Vec<u8>, FlashLoanError>;
}
2 changes: 2 additions & 0 deletions crates/charon-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Charon core — shared types, traits, and config.

pub mod config;
pub mod flashloan;
pub mod traits;
pub mod types;

pub use config::{Config, ConfigError};
pub use flashloan::{FlashLoanError, FlashLoanProvider, FlashLoanQuote};
pub use traits::{LendingProtocol, LendingProtocolError, Result as LendingResult};
pub use types::{
FlashLoanSource, LiquidationOpportunity, LiquidationParams, Position, ProtocolId, SwapRoute,
Expand Down
18 changes: 18 additions & 0 deletions crates/charon-flashloan/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "charon-flashloan"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Flash-loan source adapters and router for Charon"

[dependencies]
charon-core = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }
dotenvy = { workspace = true }
Loading