diff --git a/crates/charon-cli/src/main.rs b/crates/charon-cli/src/main.rs index 2a8ed24..793125c 100644 --- a/crates/charon-cli/src/main.rs +++ b/crates/charon-cli/src/main.rs @@ -482,6 +482,47 @@ fn parse_borrower_file(path: &std::path::Path) -> Vec
{ out } +/// Resolve the global Chainlink staleness window from +/// `CHARON_PRICE_MAX_AGE_SECS`, falling back to the scanner's +/// `DEFAULT_MAX_AGE` on missing or unparseable values. +/// +/// Per-symbol overrides in `[chainlink_max_age_secs.]` always +/// win over this global default — they are merged on top in the +/// `PriceCache::with_per_symbol_max_age` call sites. The env var +/// only re-anchors the floor for symbols that have no per-symbol +/// override, which is the only case operators actually need on a +/// fork demo where every feed reads stale relative to wall-clock. +/// +/// Logs a one-shot info on success, warn on parse failure (so an +/// operator typo like `CHARON_PRICE_MAX_AGE_SECS=86400s` does not +/// silently revert to the 600s default). See #413. +fn resolve_default_price_max_age() -> Duration { + const ENV_VAR: &str = "CHARON_PRICE_MAX_AGE_SECS"; + match std::env::var(ENV_VAR) { + Ok(raw) => match raw.trim().parse::() { + Ok(secs) => { + let dur = Duration::from_secs(secs); + info!( + env_var = ENV_VAR, + secs, "chainlink default staleness window overridden via env" + ); + dur + } + Err(err) => { + warn!( + env_var = ENV_VAR, + raw_value = %raw, + error = ?err, + default_secs = DEFAULT_MAX_AGE.as_secs(), + "CHARON_PRICE_MAX_AGE_SECS unparseable — falling back to DEFAULT_MAX_AGE" + ); + DEFAULT_MAX_AGE + } + }, + Err(_) => DEFAULT_MAX_AGE, + } +} + fn resolve_execute_submit_url( private_rpc_url: Option<&SecretString>, http_url: &str, @@ -924,7 +965,7 @@ async fn run_listen( let prices = Arc::new(PriceCache::with_per_symbol_max_age( provider.clone(), price_feeds, - DEFAULT_MAX_AGE, + resolve_default_price_max_age(), per_symbol_max_age, )); // Native-feed preflight with bounded retry. Free-tier @@ -1668,7 +1709,7 @@ async fn run_replay(config: &Config, block: u64, borrower_file: PathBuf) -> Resu let prices = Arc::new(PriceCache::with_per_symbol_max_age( provider.clone(), price_feeds, - DEFAULT_MAX_AGE, + resolve_default_price_max_age(), per_symbol_max_age, )); prices.refresh_all().await; diff --git a/crates/charon-core/src/config.rs b/crates/charon-core/src/config.rs index 4082717..b400dec 100644 --- a/crates/charon-core/src/config.rs +++ b/crates/charon-core/src/config.rs @@ -135,10 +135,13 @@ pub struct Config { /// Per-feed Chainlink staleness window overrides, keyed by chain /// then asset symbol (`chainlink_max_age_secs.bnb.USDT = 86400`). /// Stable feeds (USDC/USDT/FDUSD on BSC) update on deviation, not - /// heartbeat, so the global 600s default routinely flags them as - /// stale even though the price has not moved. Missing entry = - /// fall back to the global default (`DEFAULT_MAX_AGE` / the - /// `CHARON_PRICE_MAX_AGE_SECS` env override). See #331. + /// heartbeat, so the global default routinely flags them as stale + /// even though the price has not moved. Missing entry = fall back + /// to the global default, which is itself anchored at + /// `DEFAULT_MAX_AGE` and may be re-anchored at runtime by the + /// `CHARON_PRICE_MAX_AGE_SECS` env override (see #331, #413). + /// Per-symbol overrides here always win — the env var only moves + /// the floor for symbols that have no per-symbol entry. /// /// Top-level section (not nested under `[chainlink.]`) /// because the existing `chainlink.` table is symbol-keyed