Problem
crates/charon-scanner/src/oracle.rs:27 defines a single DEFAULT_MAX_AGE = 600s for every Chainlink feed. On BSC mainnet:
BNB / USD heartbeat ~60s — comfortable
ETH / USD heartbeat ~120-300s — comfortable
USDT / USD heartbeat ~24h, deviation-triggered (often updates every 15-30 min)
USDC / USD heartbeat ~24h, deviation-triggered (often updates every 30-60 min)
Both stable feeds regularly trip the global gate:
WARN charon_scanner::oracle: chainlink refresh failed
symbol=USDC err=feed 'USDC' is stale (updated 1006 s ago, max_age 600 s)
WARN charon_scanner::oracle: chainlink refresh failed
symbol=USDT err=feed 'USDT' is stale (updated 932 s ago, max_age 600 s)
When the BNB feed's gate fires (rare but possible during quiet periods), charon listen exits at startup:
Error: chainlink feed for 'BNB' missing or stale on chain 'bnb' — gas cost cannot be priced
Impact
P1. Stables are always treated as stale → debt and collateral pricing on USDC/USDT-denominated positions falls back to whatever the profit gate does with None. In the best case the position is dropped from consideration; in the worst it's evaluated with a stale cached value.
A single global threshold cannot fit feeds with heartbeats spanning two orders of magnitude.
Proposed fix
- Add per-symbol
max_age_secs overrides in [chainlink.bnb]:
[chainlink.bnb.max_age_secs]
USDT = 86400
USDC = 86400
FDUSD = 86400
- Read overrides in
PriceCache::new. Default remains 600s for unspecified feeds.
- Honor
CHARON_PRICE_MAX_AGE_SECS as the per-process default, but make it advisory not authoritative — per-feed overrides win.
Acceptance
charon listen against a fresh fork keeps USDT and USDC inside the freshness window indefinitely.
- BNB / ETH still gated tightly at 600s.
- Operator can adjust without rebuilding.
Found during the local mainnet validation walk on 2026-04-25.
Problem
crates/charon-scanner/src/oracle.rs:27defines a singleDEFAULT_MAX_AGE = 600sfor every Chainlink feed. On BSC mainnet:BNB / USDheartbeat ~60s — comfortableETH / USDheartbeat ~120-300s — comfortableUSDT / USDheartbeat ~24h, deviation-triggered (often updates every 15-30 min)USDC / USDheartbeat ~24h, deviation-triggered (often updates every 30-60 min)Both stable feeds regularly trip the global gate:
When the BNB feed's gate fires (rare but possible during quiet periods),
charon listenexits at startup:Impact
P1. Stables are always treated as stale → debt and collateral pricing on USDC/USDT-denominated positions falls back to whatever the profit gate does with
None. In the best case the position is dropped from consideration; in the worst it's evaluated with a stale cached value.A single global threshold cannot fit feeds with heartbeats spanning two orders of magnitude.
Proposed fix
max_age_secsoverrides in[chainlink.bnb]:PriceCache::new. Default remains 600s for unspecified feeds.CHARON_PRICE_MAX_AGE_SECSas the per-process default, but make it advisory not authoritative — per-feed overrides win.Acceptance
charon listenagainst a fresh fork keeps USDT and USDC inside the freshness window indefinitely.Found during the local mainnet validation walk on 2026-04-25.