Skip to content

feat(flashloan): FlashLoanProvider trait + Aave V3 adapter + fee-priority router#39

Merged
obchain merged 5 commits into
mainfrom
feat/14-flashloan-router
Apr 24, 2026
Merged

feat(flashloan): FlashLoanProvider trait + Aave V3 adapter + fee-priority router#39
obchain merged 5 commits into
mainfrom
feat/14-flashloan-router

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Closes #14

Three archive commits consolidated on squash. Gives the executor a uniform way to source flash capital; for v0.1 Aave V3 on BSC is the only implementation, but the abstraction admits Balancer / Uniswap flash-swap adapters without refactoring.

  • FlashLoanProvider trait + FlashLoanQuote in charon-coresource, chain_id, fee_rate_bps, available_liquidity, quote, build_flashloan_calldata
  • AaveFlashLoan adapter in new charon-flashloan crate:
    • connect(provider, pool, receiver) caches FLASHLOAN_PREMIUM_TOTAL + chain id
    • available_liquidity resolves asset → aToken via PoolDataProvider; reads aToken underlying balance
    • quote checks liquidity, computes amount × fee_bps / 10_000 fee
    • build_flashloan_calldata encodes flashLoanSimple(receiver, asset, amount, params, 0)
    • BSC PoolDataProvider hardcoded (v0.1 single-chain); moves into config later
  • FlashLoanRouter — sorts providers by fee_rate_bps ascending; walks cheapest-first, returns first quote that fits; None when no source can cover
  • Live-verified on BSC: USDT liquidity > 0, fee_bps = 5 (0.05%); live test + 4 router unit tests covering cheapest-picked / fallthrough / all-empty

Depends on #23 (feat/13-foundry-fork-tests).

obchain added 3 commits April 21, 2026 18:16
Shared abstraction for every flash-loan source. Router in
`charon-flashloan` will walk a list of these in fee-priority order
(Balancer 0% → Aave 0.05% → Uniswap pool fee), pick the cheapest
source with sufficient liquidity, and hand the resulting quote to the
tx builder.

- `FlashLoanQuote` — source, chain, token, amount, absolute fee,
  fee_bps, pool address
- `FlashLoanProvider` async trait:
    - `source` / `chain_id` / `fee_rate_bps` — static metadata
    - `available_liquidity(token)` — on-chain reserve lookup
    - `quote(token, amount)` — one-shot fitness check + pricing
    - `build_flashloan_calldata(quote, inner_calldata)` — outer call
      wrapping the protocol adapter's liquidation bytes
First adapter implementing `charon_core::FlashLoanProvider`. Lives in
the new `charon-flashloan` crate alongside future Balancer / Uniswap
sources; router lands next.

- `AaveFlashLoan::connect(provider, pool, receiver)`:
    - caches `FLASHLOAN_PREMIUM_TOTAL` → `fee_bps`
    - caches `eth_chainId`
    - holds the receiver (`CharonLiquidator.sol`) for calldata emission
- `available_liquidity(token)`: resolves asset → aToken via
  `PoolDataProvider.getReserveTokensAddresses`, then reads the
  aToken's underlying balance. Missing reserves return `U256::ZERO`.
- `quote(token, amount)`: checks liquidity, computes absolute fee
  (amount × fee_bps / 10_000), returns `None` if undersized.
- `build_flashloan_calldata` encodes
  `Pool.flashLoanSimple(receiver, asset, amount, params, 0)` with the
  inner liquidation bytes as params.
- BSC `PoolDataProvider` address is hardcoded (v0.1 single-chain
  scope); moves into config when multi-chain arrives.
- Unit test pins the selector; live integration test hits BSC mainnet
  and verifies USDT liquidity + quote shape.
Picks the cheapest flash-loan source that can cover a requested borrow.
Providers are supplied as `Arc<dyn FlashLoanProvider>`; the router
sorts by `fee_rate_bps` at construction so the walk starts with the
least expensive option.

- `FlashLoanRouter::new(providers)` — sorts once, cheapest first
- `route(token, amount)` — walks in order, returns the first quote
  that fits; per-provider errors or insufficient-liquidity outcomes
  are logged + skipped rather than aborting
- Returns `None` when no source can cover the amount — caller drops
  the liquidation rather than faking capital from elsewhere
- Four unit tests via an in-memory `StubProvider`: cheapest-picked,
  fallthrough on insufficient liquidity, all-empty → None, empty
  provider list → None
This was referenced Apr 23, 2026
Closes #136: Rename FlashLoanProvider::fee_rate_bps -> fee_rate_millionths
(u32, Uniswap 1e6 convention). Aave's 4-decimal-% premium is converted by
x100 at connect time (Aave 5 -> 500). FlashLoanQuote.fee_rate_bps is
renamed to match.

Closes #138: build_flashloan_calldata argument renamed to
liquidation_params and guarded against an empty buffer — every real
executeOperation ABI-decodes this payload and reverts on 0x.

Closes #141: Introduce FlashLoanError (thiserror, non_exhaustive) with
InsufficientLiquidity, ReservePaused, ChainIdMismatch, Rpc, Other.
Trait methods now return Result<T, FlashLoanError>; anyhow is kept only
on connect() for config-time wiring.

Closes #144: #[non_exhaustive] added to FlashLoanSource and
FlashLoanError.

Closes #137: AaveFlashLoan.available_liquidity calls
getReserveConfigurationData and getReserveData on the PoolDataProvider
and rejects the borrow with FlashLoanError::ReservePaused when isActive
is false, isFrozen is true, or bits 57 (frozen) / 60 (paused) are set
in the packed configuration bitmap.

Closes #142: AaveFlashLoan::connect calls provider.get_chain_id() and
anyhow::ensure!s it equals 56, failing fast on misconfigured RPC.

Closes #143: FlashLoanConfig gains an optional data_provider: Address.
config/default.toml pins the canonical Aave V3 BSC PoolDataProvider
(0x41393e5e337606dc3821075Af65AeE84D7688CBD). connect() takes it as an
argument instead of a hardcoded constant; the constant was removed.

Closes #145: FlashLoanRouter grows with_liquidity_tiebreaker, an async
constructor that probes available_liquidity(token) for each provider
and sorts fee_rate_millionths asc, then available_liquidity desc. The
plain new() keeps the fee-only ordering for call sites that don't want
the probe cost.

Closes #139: aave_live integration test is now #[ignore]-gated so
cargo test --workspace does not require BNB_WS_URL. Run explicitly with
cargo test -p charon-flashloan -- --ignored.

Refs #140: charon-flashloan was already in workspace.members; no
change required.

thiserror is added to the workspace dependency table and pulled into
charon-core / charon-flashloan.

Gates green: cargo fmt, cargo clippy --all-targets --all-features
-D warnings, cargo test --workspace --all-targets, cargo test
--workspace --doc.
@obchain obchain changed the base branch from feat/13-foundry-fork-tests to main April 24, 2026 10:59
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/charon-core/src/lib.rs
@obchain obchain merged commit 6c1bc65 into main Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[flashloan] Router: cheapest-source selection (Aave V3 on BSC) + fallback chain

1 participant