Skip to content

feat(protocols): Venus adapter (charon-protocols crate, closes #9)#33

Merged
obchain merged 8 commits intomainfrom
feat/08-venus-adapter
Apr 24, 2026
Merged

feat(protocols): Venus adapter (charon-protocols crate, closes #9)#33
obchain merged 8 commits intomainfrom
feat/08-venus-adapter

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Closes #9

New charon-protocols crate with the Venus adapter — the first LendingProtocol implementation. Six-commit series consolidated on squash:

  1. Scaffold crate — workspace member + stub VenusAdapter struct
  2. sol! ABIs — Comptroller (getAccountLiquidity, getAssetsIn, getAllMarkets, closeFactorMantissa, oracle), VToken (underlying, balanceOf, borrowBalance, balanceOfUnderlying, liquidateBorrow, redeem…), VenusOracle (getUnderlyingPrice)
  3. Async connect — snapshots oracle, market list, close factor, chain_id; builds underlying ↔ vToken map (vBNB skipped — no underlying())
  4. fetch_positions — walks getAssetsIn, reads per-vToken borrow + supply + oracle price, picks biggest-value debt + collateral pair, synthesises health factor from shortfall
  5. Liquidation params + calldata — maps underlying → vToken, caps repay at debt × close_factor / 1e18 (50% on BSC Venus), ABI-encodes inner VToken.liquidateBorrow call
  6. CLI wiringlisten builds the adapter at startup; every new block triggers one fetch_positions scan with --borrower 0x… flag for test seeding

Live-verified on BSC: 48 markets, 47 mapped, oracle 0x6592…b8A, close factor 0.5e18. 25 scans / 25 blocks in 30 s with zero warnings.

Depends on #8 (feat/07-block-listener).

obchain added 6 commits April 21, 2026 18:06
…ub (#8)

First commit in the #8 series. Lays out the crate layout so subsequent
commits can land ABIs, RPC wiring, and the LendingProtocol impl without
simultaneously introducing new module boundaries.

- New `charon-protocols` workspace member with a single `venus` module
- `VenusAdapter` is a minimal struct holding the Comptroller address;
  provider, vToken discovery, and trait impl land in follow-ups
- No scanner/executor consumption yet — zero behaviour change for
  existing commands (`listen`, `test-connection`)
#8)

Second commit in the #8 series. Defines typed RPC bindings for every
Venus method the scanner and executor need, using alloy's `sol!` macro
with `#[sol(rpc)]` so each interface gets a `new(address, provider)`
constructor and per-method decoding for free.

- `IVenusComptroller`: getAccountLiquidity, getAssetsIn, getAllMarkets,
  closeFactorMantissa, liquidationIncentiveMantissa, oracle
- `IVToken`: underlying, balanceOf, borrowBalanceStored/Current,
  balanceOfUnderlying, exchangeRateStored, decimals, symbol,
  liquidateBorrow
- `IVenusOracle`: getUnderlyingPrice (Compound-style scaling)

Still no runtime logic — bindings only. VenusAdapter continues to be a
scaffold struct; the LendingProtocol impl lands next commit.
Third commit in the #8 series. Wires the adapter to a shared pub-sub
provider and caches Venus's rarely-changing market config at connect
time, so per-block calls don't re-hit the Comptroller for invariants.

- `VenusAdapter::connect(provider, comptroller)` fetches the oracle
  address, vToken market list, and close factor via three read-only
  RPCs; stores them on the struct
- Shared provider held as `Arc<RootProvider<PubSubFrontend>>` so the
  scanner can hand the same provider to multiple adapters cheaply
- Live integration test (`tests/venus_connect.rs`) hits BSC mainnet
  when `BNB_WS_URL` is set, skipped otherwise — proves the Comptroller
  roundtrip works end to end

Venus's Diamond Comptroller does not expose
`liquidationIncentiveMantissa()` globally (per-market in Diamond
facets); that lookup is deferred to the liquidation builder in a later
commit rather than bundled into `connect`.
Fourth commit in the #8 series. Implements on-chain position discovery
for Venus — the core translation that turns Compound V2's
`(errorCode, liquidity, shortfall)` + per-vToken balances into the
shared `Position` shape the scanner consumes.

- `VenusAdapter::connect` now also resolves every vToken's `underlying()`
  address and caches both directions of the map; vBNB-style
  native-wrapping markets (no `underlying()`) are skipped with a debug
  log and left unmapped
- Adapter tracks its own `chain_id` via `eth_chainId` for position
  metadata
- `fetch_position_inner` walks `getAssetsIn`, reads per-vToken
  `borrowBalanceStored` + `balanceOfUnderlying` + oracle price, and
  picks the single biggest-value debt vToken and biggest-value
  collateral vToken as the Position to report
- Per-asset failures (oracle revert, missing market) are logged and
  skipped so one broken market can't blank an entire borrower
- Health factor synthesised as a binary 0 / 2e18 signal from Venus's
  `shortfall`: enough for the scanner's `< 1e18` predicate, precise HF
  arithmetic deferred to the 3-bucket scanner (#9)
- `LendingProtocol::get_liquidation_params` and
  `build_liquidation_calldata` are stubbed with explicit errors — they
  land in the next commit alongside per-market liquidation incentive
  lookup

Live integration test (`tests/venus_fetch.rs`) calls `fetch_positions`
for a clean address on BSC mainnet, verifying the full pipeline
returns well-formed (or empty) results without panicking.
)

Fifth commit in the #8 series. Completes the `LendingProtocol` impl on
the Rust side — the trait no longer returns `bail!("unimplemented")` and
an opportunity can now be turned into ready-to-sign bytes.

- `get_liquidation_params` maps underlying ERC-20 debt / collateral
  addresses on the `Position` back to Venus vToken addresses via the
  adapter's lookup map, and caps `repay_amount` at
  `debt_amount × close_factor / 1e18` (50% on BSC Venus). Refuses to
  emit a zero-repay params struct.
- `build_liquidation_calldata` delegates to a free
  `encode_liquidate_borrow_calldata` helper that encodes
  `VToken.liquidateBorrow(borrower, repayAmount, vTokenCollateral)`
  via alloy's generated call struct + `SolCall::abi_encode`. Split
  as a free function so it can be unit-tested without constructing
  a full adapter.
- Unit test asserts the 4-byte selector matches alloy's
  `liquidateBorrowCall::SELECTOR` constant and that calldata length is
  selector + 3 × 32-byte slots — catches any accidental ABI drift
  (wrong arg order, extra params).

The outer wrapping into `CharonLiquidator.executeLiquidation(...)` is
intentionally *not* done here; that calldata shape depends on the
Solidity contract, which is a separate milestone (M2 Execution).
Shipping the inner Venus-specific calldata now unblocks scanner +
profit-calc work without waiting on the contract.
Final commit in the #8 series. The adapter built across Parts A–E is
now driven by real block events: `charon listen` constructs a
`VenusAdapter` at startup and, for every new BSC block from the
existing `BlockListener`, runs one `fetch_positions` scan against a
caller-supplied borrower list.

- `listen --borrower 0x…` flag (repeatable, empty by default) seeds
  the scan list. Full borrower discovery from indexed `Borrow` events
  is its own task, tracked under the health-scanner milestone (#9)
- Per-block structured log: `venus scan chain=… block=… tracked=… returned=… scan_ms=…`
- Adapter and listener use separate WebSocket connections for now;
  sharing a single pub-sub provider is a cheap optimisation once the
  scanner owns the full runtime (#9)
- `charon-cli` now depends on `charon-protocols` and the alloy
  primitives crate for address parsing via clap

Live soak on BSC: 25 blocks / 25 scans in 30 s, zero warnings,
Venus adapter snapshot reports 48 markets with 47 mapped (vBNB
skipped, as intended).
…ntive, concurrent borrowers

- Replace balanceOfUnderlying (non-view, accrues interest) with
  balanceOf * exchangeRateStored / 1e18 on the scan path. Works against
  rate-limited and view-only RPC proxies that reject state-mutating
  eth_calls. balanceOfUnderlying + borrowBalanceCurrent removed from the
  ABI entirely to prevent future accidental use.
- Real health factor derived from the Comptroller's own liquidity and
  shortfall values plus the per-block sum of borrow * price:
  HF = (total_borrow_val +/- liquidity_or_shortfall) / total_borrow_val,
  1e18-scaled. Replaces the 0/2e18 binary placeholder so bucket
  classifiers can rank positions by urgency.
- vBNB special case: vBNB (0xA07c…) has no underlying() and used to be
  silently skipped, invisibling BSC's largest Venus market. connect()
  now maps vBNB -> Wrapped BNB (0xbb4C…) in both directions so BNB-
  collateral borrowers are scanned and can be liquidated.
- Add liquidationIncentiveMantissa() to the Comptroller ABI, fetch it at
  connect alongside closeFactorMantissa, and derive liquidation_bonus_bps
  from the live value at scan time: bps = (mantissa - 1e18) / 1e14.
  Governance can change the incentive without the bot running on a stale
  hardcoded 1000 bps.
- VenusAdapter fields are now pub(crate) / private, guarded by a tokio
  RwLock so they can be refreshed atomically. A new refresh() async
  method re-queries Comptroller state and swaps the snapshot; operators
  (or a timer/event watcher) call it to pick up governance changes and
  newly listed markets without restart. markets(), oracle(),
  close_factor_mantissa(), liquidation_incentive_mantissa() expose
  read-only accessors.
- fetch_positions now processes borrowers concurrently through
  FuturesUnordered so the scan walltime drops from sequential 50+
  borrowers to the slowest single borrower. Follow-up issue should
  introduce Multicall3 aggregate for per-borrower per-vToken reads.

Closes #97 #98 #99 #100 #101 #102
@obchain obchain merged commit 448bf23 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.

[protocols] charon-protocols crate + Venus adapter (getAccountLiquidity + vToken bindings)

1 participant