Skip to content
Merged
16 changes: 16 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-protocols",
"crates/charon-scanner",
"crates/charon-cli",
]
Expand Down Expand Up @@ -61,4 +62,5 @@ dotenvy = "0.15"

# Internal crates
charon-core = { path = "crates/charon-core" }
charon-protocols = { path = "crates/charon-protocols" }
charon-scanner = { path = "crates/charon-scanner" }
2 changes: 2 additions & 0 deletions crates/charon-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ path = "src/main.rs"

[dependencies]
charon-core = { workspace = true }
charon-protocols = { workspace = true }
charon-scanner = { workspace = true }
alloy = { workspace = true }
clap = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
Expand Down
95 changes: 87 additions & 8 deletions crates/charon-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
//! ```text
//! CHARON_CONFIG=/etc/charon/default.toml charon listen
//! charon --config config/default.toml listen
//! charon --config config/default.toml listen --borrower 0xABC…
//! charon --config config/default.toml test-connection --chain bnb
//! ```

use std::path::PathBuf;
use std::sync::Arc;

use alloy::eips::BlockNumberOrTag;
use alloy::primitives::Address;
use alloy::providers::{ProviderBuilder, WsConnect};
use anyhow::{Context, Result};
use charon_core::Config;
use charon_core::{Config, LendingProtocol};
use charon_protocols::VenusAdapter;
use charon_scanner::{BlockListener, ChainEvent, ChainProvider};
use clap::{Parser, Subcommand};
use tokio::sync::mpsc;
Expand Down Expand Up @@ -40,11 +46,20 @@ struct Cli {

#[derive(Subcommand, Debug)]
enum Command {
/// Spawn one block listener per configured chain and drain chain events.
/// Spawn one block listener per configured chain, drain chain events,
/// and run the Venus adapter every new block for the supplied borrower
/// list.
///
/// Downstream pipeline (scanner → profit calc → executor) consumes
/// the same channel once those layers land.
Listen,
/// Borrower discovery from indexed events is a follow-up; pass
/// `--borrower 0x…` one or more times to seed a test list. An empty
/// list is allowed — the adapter still connects so the operator can
/// confirm the WS pipeline.
Listen {
/// Addresses to scan on every new block. Repeat the flag for
/// multiple borrowers.
#[arg(long = "borrower")]
borrowers: Vec<Address>,
},

/// Connect to a configured chain and print its latest block number.
TestConnection {
Expand Down Expand Up @@ -89,8 +104,8 @@ async fn main() -> Result<()> {
);

match cli.command {
Command::Listen => {
run_listen(&config).await?;
Command::Listen { borrowers } => {
run_listen(&config, borrowers).await?;
}
Command::TestConnection { chain } => {
let chain_cfg = config
Expand All @@ -110,11 +125,47 @@ async fn main() -> Result<()> {
/// configured chain, drains the shared `ChainEvent` channel, and exits
/// cleanly on SIGINT or SIGTERM so the Docker `stop` → SIGTERM → SIGKILL
/// sequence never tears mid-operation.
async fn run_listen(config: &Config) -> Result<()> {
///
/// For every `NewBlock` event on a chain with a `[protocol.venus]` entry,
/// the Venus adapter scans the supplied borrower list anchored at the
/// observed block. Chains without a Venus protocol config still flow
/// through the drain loop but trigger no protocol scans (v0.1 scope).
async fn run_listen(config: &Config, borrowers: Vec<Address>) -> Result<()> {
if config.chain.is_empty() {
anyhow::bail!("no chains configured — nothing to listen to");
}

// Venus adapter is currently single-chain (BNB) per config scope.
// Build it only if `[protocol.venus]` exists and its target chain is
// configured; otherwise run the listener pipeline without a scanner.
let venus_adapter: Option<(String, Arc<VenusAdapter>)> = match config.protocol.get("venus") {
Some(venus_cfg) => {
let chain_name = &venus_cfg.chain;
let chain_cfg = config.chain.get(chain_name).with_context(|| {
format!(
"protocol 'venus' references chain '{chain_name}' which is not in [chain.*]"
)
})?;
let adapter_ws = ProviderBuilder::new()
.on_ws(WsConnect::new(&chain_cfg.ws_url))
.await
.context("venus adapter: failed to connect over ws")?;
let adapter =
Arc::new(VenusAdapter::connect(Arc::new(adapter_ws), venus_cfg.comptroller).await?);
info!(
chain = %chain_name,
borrower_count = borrowers.len(),
market_count = adapter.markets().await.len(),
"venus adapter ready"
);
Some((chain_name.clone(), adapter))
}
None => {
info!("no [protocol.venus] configured — listener will drain events without scanning");
None
}
};

let (tx, mut rx) = mpsc::channel::<ChainEvent>(CHAIN_EVENT_CHANNEL);
let mut listeners: tokio::task::JoinSet<(String, Result<()>)> =
tokio::task::JoinSet::new();
Expand Down Expand Up @@ -144,6 +195,34 @@ async fn run_listen(config: &Config) -> Result<()> {
backfill,
"cli drained event"
);
// Route to Venus scan only when this event is for
// the chain the Venus adapter was configured on.
if let Some((venus_chain, adapter)) = venus_adapter.as_ref() {
if venus_chain == &chain {
let start = std::time::Instant::now();
let block_tag = BlockNumberOrTag::Number(number);
match adapter.fetch_positions(&borrowers, block_tag).await {
Ok(positions) => {
info!(
chain = %chain,
block = number,
timestamp,
backfill,
tracked = borrowers.len(),
returned = positions.len(),
scan_ms = start.elapsed().as_millis() as u64,
"venus scan"
);
}
Err(err) => warn!(
chain = %chain,
block = number,
error = ?err,
"venus scan failed"
),
}
}
}
}
_ => {}
}
Expand Down
11 changes: 10 additions & 1 deletion crates/charon-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,16 @@ pub struct SwapRoute {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LiquidationParams {
#[non_exhaustive]
// NOTE: this variant is intentionally *not* `#[non_exhaustive]`.
// The `LendingProtocol::get_liquidation_params` trait method is
// implemented outside `charon-core` (each adapter crate returns a
// protocol-specific variant), which requires each variant to be
// constructible via a struct expression from downstream crates.
// Enum-level `#[non_exhaustive]` still prevents breakage from adding
// new variants; that is the only forward-compat guarantee we need
// here. Adding or renaming a field on `Venus` is a semver break by
// design — every adapter call site must be audited when liquidation
// mechanics change.
Venus {
borrower: Address,
/// vToken of the collateral asset (the token seized).
Expand Down
19 changes: 19 additions & 0 deletions crates/charon-protocols/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "charon-protocols"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Lending-protocol adapters for Charon (Venus, Aave, Compound, Morpho, …)"

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

[dev-dependencies]
tokio = { workspace = true }
dotenvy = { workspace = true }
14 changes: 14 additions & 0 deletions crates/charon-protocols/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Charon protocol adapters.
//!
//! One module per lending protocol. Each module defines a struct that
//! implements the [`LendingProtocol`](charon_core::LendingProtocol) trait
//! — the scanner and executor talk to these structs instead of protocol-
//! specific RPCs directly, so adding a new protocol is a self-contained
//! change here with no scanner edits required.
//!
//! For v0.1 only the Venus adapter is wired up; Aave / Compound / Morpho
//! adapters land in later milestones.

pub mod venus;

pub use venus::VenusAdapter;
Loading