Problem
charon listen cannot find any underwater Venus borrowers on BSC mainnet without an operator-supplied --borrower 0x... flag. The PRD/HLD (README pipeline steps 1-2) requires:
- Listen — A WebSocket listener receives new blocks and log events from the chain.
- Decode — Protocol adapters normalize raw events into a shared
Position struct.
Today the bot only subscribes to newHeads (block ticks), never to vToken Borrow / Mint / RepayBorrow / LiquidateBorrow logs. The Venus adapter has no event-decoding path. The CLI even acknowledges the gap explicitly:
crates/charon-cli/src/main.rs:191
/// Borrower discovery from indexed events is a follow-up; pass
/// `--borrower 0x...` one or more times to seed a test list.
Impact
P0 / blocker for the PRD's primary value proposition. Without auto-discovery:
- The bot is structurally incapable of finding the next liquidation opportunity on mainnet.
- Demos require staff to know an underwater address ahead of time. None exists for long stretches (Venus BSC has had zero
LiquidateBorrow events on every major vToken across the last 50,000 blocks at the time of writing).
- Every metric downstream —
position_bucket{}, scanner_scan_duration_seconds, opportunity / queue / sim counters — stays at zero unless an operator hand-feeds borrowers.
This is the difference between "bot runs" and "bot is a liquidation bot."
Reproduction
FORK_RPC=https://bsc.drpc.org FORK_BLOCK=latest FORK_CUPS=80 ./scripts/anvil_fork.sh
RUST_LOG=info ./target/release/charon --config config/fork.toml listen
# pipeline ready: borrower_count=0 — scanner ticks every block, never classifies anything
curl -sS http://127.0.0.1:9091/metrics | grep position_bucket
# (no output — gauge never set)
Proposed fix
New crate or module charon-discovery with:
- Event indexer — subscribe via WS to
Borrow(address,uint256,uint256,uint256) (and optionally Mint, RepayBorrow, LiquidateBorrow) on every vToken returned by Comptroller.getAllMarkets(). Use eth_getLogs chunked over MAX_LOG_RANGE_BLOCKS for backfill; live tail via eth_subscribe logs.
- Persistent borrower set —
DashMap<Address, BorrowerMeta> with last-seen-block, bucket hint, decay. Persist to data/borrowers.bincode so restarts skip the backfill.
- Liquidator-driven re-scan — pull addresses from the cache into
scan_set per the existing ScanScheduler cadences (HOT every block, WARM every 10, COLD every 100), plus an unconditional sweep of the bottom-N HF addresses every block.
- CLI wiring — make
--borrower an optional override that augments (not replaces) the discovered set.
Acceptance
charon listen against config/fork.toml (no --borrower) populates position_bucket{} to non-zero within one minute of pipeline ready.
- Restart resumes from disk without re-scanning the full event range.
- Backfill window configurable via
[scanner.discovery] TOML stanza (default last 7 days).
Out of scope
- Subgraph / Goldsky integration (separate issue if we want a fast bootstrap path).
- Per-protocol discovery generalisation — Venus first; Aave when the v0.3 expansion lands.
Found during the local mainnet validation walk on 2026-04-25.
Problem
charon listencannot find any underwater Venus borrowers on BSC mainnet without an operator-supplied--borrower 0x...flag. The PRD/HLD (README pipeline steps 1-2) requires:Today the bot only subscribes to
newHeads(block ticks), never to vTokenBorrow/Mint/RepayBorrow/LiquidateBorrowlogs. The Venus adapter has no event-decoding path. The CLI even acknowledges the gap explicitly:crates/charon-cli/src/main.rs:191Impact
P0 / blocker for the PRD's primary value proposition. Without auto-discovery:
LiquidateBorrowevents on every major vToken across the last 50,000 blocks at the time of writing).position_bucket{},scanner_scan_duration_seconds, opportunity / queue / sim counters — stays at zero unless an operator hand-feeds borrowers.This is the difference between "bot runs" and "bot is a liquidation bot."
Reproduction
Proposed fix
New crate or module
charon-discoverywith:Borrow(address,uint256,uint256,uint256)(and optionallyMint,RepayBorrow,LiquidateBorrow) on every vToken returned byComptroller.getAllMarkets(). Useeth_getLogschunked overMAX_LOG_RANGE_BLOCKSfor backfill; live tail viaeth_subscribe logs.DashMap<Address, BorrowerMeta>with last-seen-block, bucket hint, decay. Persist todata/borrowers.bincodeso restarts skip the backfill.scan_setper the existingScanSchedulercadences (HOT every block, WARM every 10, COLD every 100), plus an unconditional sweep of the bottom-N HF addresses every block.--borroweran optional override that augments (not replaces) the discovered set.Acceptance
charon listenagainstconfig/fork.toml(no--borrower) populatesposition_bucket{}to non-zero within one minute of pipeline ready.[scanner.discovery]TOML stanza (default last 7 days).Out of scope
Found during the local mainnet validation walk on 2026-04-25.