Skip to content

obchain/Charon

Repository files navigation

Charon

Multi-chain, flash-loan-backed liquidation bot — written in Rust.

License: MIT Rust Release Container

Charon monitors under-collateralized positions across major DeFi lending protocols and executes profitable liquidations using flash loans — zero upfront capital, zero position risk. If a liquidation turns out to be unprofitable at execution time, the entire transaction reverts atomically; the only cost is a failed simulation's gas.

Named after the mythological ferryman. Charon carries underwater positions to their final destination.


Table of Contents


Status

v0.1 — work in progress. The foundation is in place: Cargo workspace, normalized types, the LendingProtocol trait, a TOML config loader with env-var substitution, and a runnable CLI that validates and reports on the loaded config. Chain listening, health scanning, and on-chain execution arrive in the next phases of the build.

Current scope: Venus Protocol on BNB Chain. Other protocols and chains are on the roadmap.

⚠️ Do not run this against mainnet with real funds yet. Nothing has been battle-tested. Treat v0.1 as development-only.


How it works

flowchart LR
    BNB[BNB Chain<br/>WebSocket]:::chain

    subgraph Core["Charon Core — Rust"]
        direction TB
        L[Chain Listener] --> A[Venus Adapter]
        A --> S[Health Scanner]
        P[Price Engine<br/>Chainlink + TWAP] --> S
        S --> Pr[Profit Calculator]
        Pr --> R[Flash Loan Router]
        R --> B[Tx Builder]
    end

    subgraph OnChain["On-chain — Solidity"]
        direction TB
        Liq[CharonLiquidator.sol]
        F[Flash Loan Source<br/>Aave V3 Pool]
        D[DEX Swap<br/>PancakeSwap / Uniswap V3]
        V[Venus Protocol]
        Liq --> F
        Liq --> V
        Liq --> D
    end

    BNB --> L
    B --> Liq

    classDef chain fill:#0f1e36,stroke:#3b82f6,color:#e2e8f0
Loading
  1. Listen — A WebSocket listener receives new blocks and log events from the chain.
  2. Decode — Protocol adapters normalize raw events into a shared Position struct — the rest of the pipeline doesn't care whether the source is Venus, Aave, or anything else.
  3. Price — A price engine reads live USD prices from Chainlink, with Uniswap V3 TWAPs as a fallback when Chainlink is unavailable or stale.
  4. Scan — The health scanner recomputes health factors and flags any position that drops below 1.0.
  5. Estimate — The profit calculator simulates the full liquidation end-to-end (gas + flash-loan fee + expected DEX slippage) and drops anything below a per-chain USD threshold.
  6. Route — The flash-loan router picks the cheapest available source (Balancer 0 % → Aave V3 0.05 % → Uniswap V3 pool fee).
  7. Build — The transaction builder encodes the call, dry-runs it via eth_call, signs, and submits (via Flashbots on Ethereum, private RPC on L2s).
  8. Execute — On-chain, CharonLiquidator.sol atomically: flash-borrows → calls the protocol's liquidation entry point → swaps seized collateral back to the debt token → repays the flash loan → forwards profit to the bot's hot wallet. If any step fails, the entire transaction reverts.

Key features

  • Zero capital required. Every liquidation is flash-loan-backed. No pre-funded position, no locked inventory.
  • Protocol-agnostic. Adding a new lending protocol means implementing a single Rust trait (LendingProtocol). No changes to scanning, routing, or execution.
  • Multi-chain by design. A single binary monitors multiple EVM chains in parallel. v0.1 ships BSC; v0.3 expands to Ethereum, Arbitrum, Polygon, Base, and Avalanche.
  • Rust performance. tokio async runtime, lock-free concurrent state via DashMap, sub-50 ms block-to-broadcast latency target. Designed to run comfortably on a $5 VPS.
  • Flash-loan atomicity. Bad slippage, race conditions, and math errors all revert the transaction — the protocol never loses its liquidity, and the bot never loses capital.
  • Open source, MIT licensed. Community extensions welcome.

Safety model

Every liquidation has the atomic form:

borrow (flash) → liquidate → swap → repay flash → profit

If the chain of operations cannot repay the flash loan in full, the EVM reverts the entire transaction — including the flash borrow itself. Concretely:

Failure mode Outcome
Profit estimate was wrong Tx reverts, flash source gets its capital back, bot pays only gas
DEX swap slippage exceeds slippage guard Tx reverts atomically — no capital change
Another bot won the race eth_call simulation catches 99 %+ before submit, so no gas spent
Oracle update mid-transaction pushes health back ≥ 1.0 Tx reverts on the liquidation call

Worst case: gas for a single failed transaction (typically $0.01–$5 depending on chain). Best case: profit lands in the bot's hot wallet. No intermediate case where bot capital is lost — this is the fundamental guarantee of flash-loan design.


Getting started

Prerequisites

  • Rust — edition 2024 / rustc 1.85+. Install via rustup.rs.
  • A BNB Chain RPC endpoint.env.example ships with public-node defaults (good for light testing, rate-limited in production). For real use, point it at a private endpoint (QuickNode, Ankr, Blast, or your own node).

Clone and build

# via HTTPS
git clone https://github.com/obchain/Charon.git

# or via SSH
git clone git@github.com:obchain/Charon.git

cd Charon
cargo build

Configure

cp .env.example .env
# edit .env with your RPC endpoints

Run

cargo run -- --config config/default.toml listen

Expected output (v0.1 — scanner not wired up yet):

INFO charon: charon starting up
INFO charon: loading config path=config/default.toml
INFO charon: config loaded chains=1 protocols=1 flashloan_sources=1 liquidators=1 min_profit_usd=5
INFO charon: listen: not wired up yet — scanner arrives in Day 2

For verbose logs, prepend RUST_LOG=debug.

Run from a published container

Tagged releases are published to GitHub Container Registry as ghcr.io/obchain/charon:

docker pull ghcr.io/obchain/charon:v0.1.0
docker run --rm \
  --env-file .env \
  -v "$PWD/config:/app/config:ro" \
  ghcr.io/obchain/charon:v0.1.0 \
  --config /app/config/default.toml listen

Tag schema: vMAJOR.MINOR.PATCH, MAJOR.MINOR.PATCH, MAJOR.MINOR, and latest. Each release page lists the published sha256 digest — pin to it in production.

For a full local stack (Charon + Alloy → Grafana Cloud), use the compose recipe in deploy/compose/ — see Deploy.


Configuration

Charon reads a TOML config file (default path: config/default.toml). Secrets — RPC URLs, keys, API tokens — are referenced as ${ENV_VAR} placeholders and substituted from the process environment (or a local .env file) at load time.

Example (abridged):

[bot]
min_profit_usd   = 5.0     # drop opportunities below this threshold
max_gas_gwei     = 10      # skip when gas spikes beyond this
scan_interval_ms = 1000    # polling cadence (ms)

[chain.bnb]
chain_id = 56
ws_url   = "${BNB_WS_URL}"
http_url = "${BNB_HTTP_URL}"

[protocol.venus]
chain       = "bnb"
comptroller = "0xfd36e2c2a6789db23113685031d7f16329158384"

[flashloan.aave_v3_bsc]
chain = "bnb"
pool  = "0x6807dc923806fe8fd134338eabca509979a7e0cb"

Environment variables expected by the default config:

Variable Purpose
BNB_WS_URL BNB Chain WebSocket RPC endpoint
BNB_HTTP_URL BNB Chain HTTPS RPC endpoint (for multicall)

Run profiles

Three TOML profiles ship in config/. Pick one with --config.

Profile File When to use
Mainnet config/default.toml Production runs against BSC mainnet (real capital).
Testnet config/testnet.toml Venus on BSC testnet (Chapel, chainId 97) — no Aave V3 on Chapel, runs read-only.
Local anvil fork config/fork.toml Full end-to-end against a local anvil fork of BSC mainnet. Zero capital risk.

Local anvil fork (full end-to-end, no capital)

Fork BSC mainnet locally. Real Venus state, real Aave V3, real PancakeSwap — liquidate real positions against a private chain.

Terminal A — boot the fork:

./scripts/anvil_fork.sh                         # forks latest block via dRPC, falls back to PublicNode
FORK_BLOCK=41000000 ./scripts/anvil_fork.sh     # pin a specific block
CHARON_ANVIL_PORT=8546 ./scripts/anvil_fork.sh  # run on a non-default port

Terminal B — run Charon against it:

cargo run -- --config config/fork.toml listen

The fork profile carries profile_tag = "fork"; Config::validate rejects it at startup if any chain's ws_url / http_url resolves to a non-loopback host. This keeps the intentionally lowered profit gate from ever pointing at mainnet by accident.

The fork profile omits [liquidator.bnb] by default — after forge create against the local anvil, add a [liquidator.bnb] section pointing at the deployed address to exercise the full liquidation path. Until then the CLI runs in read-only mode (scanner + metrics only).


Metrics

Every profile ships with a Prometheus exporter enabled. Scrape http://<host>:9091/metrics. The exporter binds :9091 (not :9090) so it doesn't collide with a co-located Prometheus server.

Key series (single source of truth in crates/charon-metrics/src/lib.rs — the names module is what dashboards and alert rules must match):

Metric Type Labels
charon_scanner_blocks_total counter chain
charon_scanner_positions gauge chain, bucket
charon_pipeline_block_duration_seconds histogram chain
charon_executor_simulations_total counter chain, result
charon_executor_opportunities_queued_total counter chain
charon_executor_opportunities_dropped_total counter chain, stage
charon_executor_profit_usd_cents histogram chain
charon_executor_queue_depth gauge

Grafana dashboard

A ready-to-import dashboard lives at deploy/grafana/charon.json and a matching alert-rule bundle at deploy/grafana/alerts.yaml. The dashboard is built against Grafana 10.4.x or newer (panel schema v39 and Grafana Cloud both satisfy this); older 9.x installs will reject the import or silently drop panels.

Security — read before exposing :9091. The metrics endpoint ships unauthenticated and binds 0.0.0.0 by default. On a public VPS (Hetzner CX22, the documented target) that exposes profit histograms, build SHA, queue depth, and simulation results to the internet. Before scraping from a remote Prometheus, either bind the exporter to 127.0.0.1 and scrape over a local socket / SSH tunnel / Tailscale, or put a reverse proxy with basic auth (or mTLS) in front of :9091. See tracking issues #213 and #214.

Three steps to load it into Grafana or Grafana Cloud:

  1. Add a Prometheus data source that scrapes http://<charon-host>:9091/metrics (every ~10 s is fine). Use a loopback address, a VPN endpoint, or an authenticated reverse-proxy URL here — never a raw public-internet address.
  2. In Grafana, Dashboards → New → Import → Upload JSON file and pick the file above.
  3. On the import screen, select the Prometheus data source you created and click Import.

Dashboard UID is charon-v0 and tags are charon, liquidation, defi — re-importing over an existing copy replaces it rather than duplicating. Variables (Chain, Instance) auto-populate from label values once metrics start flowing.

Alert rules in deploy/grafana/alerts.yaml can be loaded by Prometheus via rule_files: or by Grafana unified alerting (Alerting → Contact points → Rules → Upload file). Thresholds are tuned for a single-host BSC deployment on a 3s block cadence — adjust per-environment before wiring a pager.


Deploy (single host, e.g. Hetzner CX22)

A minimal docker compose stack ships in deploy/compose/. It runs two services:

  1. charon — built from the repo-root Dockerfile (multi-stage: rust:1-slim builder → debian:bookworm-slim runtime, ~150 MB final image)
  2. alloyGrafana Alloy sidecar that scrapes charon:9091 over the internal compose network and remote_writes every series to Grafana Cloud

No local Prometheus or Grafana is deployed — the Grafana Cloud free tier is the visualisation surface, which fits the CX22 resource envelope (2 vCPU / 4 GB RAM) comfortably.

cd deploy/compose
cp .env.example .env            # fill in RPC + Grafana Cloud creds
docker compose up -d --build
docker compose logs -f charon

The metrics endpoint is not exposed to the host — Alloy reaches it by DNS name. Import deploy/grafana/charon.json into Grafana Cloud and the panels populate automatically once Alloy's first push lands.


Project structure

Charon/
├── crates/
│   ├── charon-core/        # Shared types, LendingProtocol trait, config loader
│   ├── charon-scanner/     # Chain listener + health-factor scanner (v0.1 WIP)
│   └── charon-cli/         # Command-line binary (`charon`)
├── config/
│   └── default.toml        # Default configuration (Venus on BNB)
├── .env.example            # Environment variable template
└── Cargo.toml              # Workspace root + shared dependency versions

Crates planned for later phases:

  • charon-protocols/ — per-protocol adapters (Venus, Aave V3, Compound V3, Morpho, …)
  • charon-flashloan/ — flash-loan source router
  • charon-executor/ — transaction builder, batcher, gas strategy, nonce manager
  • charon-telemetry/ — Prometheus metrics + Telegram alerting
  • contracts/ — Foundry workspace housing CharonLiquidator.sol

Roadmap

Tracked on GitHub: obchain/Charon › Milestones.

v0.1 — Venus on BNB (current)

  • Cargo workspace + three-crate skeleton
  • Core types (Position, LiquidationOpportunity, FlashLoanSource, SwapRoute, …)
  • LendingProtocol trait
  • TOML config loader with env-var substitution
  • CLI with --config flag and listen subcommand
  • Venus adapter — fetch positions, compute liquidation params
  • Chainlink price engine (with Uniswap V3 TWAP fallback)
  • Health-factor scanner + near-liquidation cache
  • CharonLiquidator.sol (Foundry) + deploy script
  • Flash-loan router (Aave V3 on BSC)
  • DEX swap optimizer (PancakeSwap / Uniswap V3)
  • Transaction builder with eth_call simulation
  • Prometheus metrics + Telegram alerts
  • Docker Compose deployment

v0.2 — Multi-protocol (planned)

  • Aave V3 adapter
  • Compound V3 adapter
  • Morpho Blue adapter
  • Protocol-specific close-factor handling

v0.3 — Multi-chain (planned)

  • Ethereum Mainnet (with Flashbots bundle submission)
  • Arbitrum One
  • Polygon PoS
  • Base
  • Avalanche C-Chain

Contributing

Contributions are welcome. A few ground rules:

  1. Open an issue first for non-trivial changes, so the design can be discussed before code is written.
  2. One logical change per PR. Keep commits focused and follow conventional titles (feat(core):, fix(scanner):, chore:, etc.).
  3. Respect the crate boundaries. Protocol changes live in charon-protocols/, execution changes in charon-executor/. Shared types belong in charon-core.
  4. No secrets in the repo — ever. .env is git-ignored. Keep it that way.

New to the codebase? Check issues tagged good first issue.


License

MIT — see LICENSE.

About

Multi-chain, flash-loan-backed liquidation bot written in Rust. Monitors under-collateralized DeFi positions and atomically liquidates via flash loans.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors