feat(flashloan): FlashLoanProvider trait + Aave V3 adapter + fee-priority router#39
Merged
Conversation
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
Closed
Closed
Closed
[flashloan] AaveFlashLoan PoolDataProvider address hardcoded — should be in config/default.toml
#143
Closed
Closed
Closed
[executor] charon-executor still missing from workspace members — fourth consecutive PR flagged
#199
Closed
Closed
Closed
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.
# Conflicts: # Cargo.lock # Cargo.toml # crates/charon-core/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
FlashLoanProvidertrait +FlashLoanQuoteincharon-core—source,chain_id,fee_rate_bps,available_liquidity,quote,build_flashloan_calldataAaveFlashLoanadapter in newcharon-flashloancrate:connect(provider, pool, receiver)cachesFLASHLOAN_PREMIUM_TOTAL+ chain idavailable_liquidityresolves asset → aToken via PoolDataProvider; reads aToken underlying balancequotechecks liquidity, computesamount × fee_bps / 10_000feebuild_flashloan_calldataencodesflashLoanSimple(receiver, asset, amount, params, 0)FlashLoanRouter— sorts providers byfee_rate_bpsascending; walks cheapest-first, returns first quote that fits;Nonewhen no source can coverfee_bps = 5(0.05%); live test + 4 router unit tests covering cheapest-picked / fallthrough / all-emptyDepends on #23 (
feat/13-foundry-fork-tests).