From 0da52db7963691267049087289b73dd1287d8ff3 Mon Sep 17 00:00:00 2001 From: obchain Date: Sun, 3 May 2026 18:35:55 +0530 Subject: [PATCH 1/2] fix(cli): implement CHARON_PRICE_MAX_AGE_SECS env override (closes #413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docstring on `BotConfig::chainlink_max_age_secs` and the operator-facing comment in scripts/anvil_fork.sh both reference `CHARON_PRICE_MAX_AGE_SECS` as a global staleness override, but no call site ever read the env var — both `PriceCache::with_per_symbol_max_age` sites in charon-cli (listen + replay) hardcoded `DEFAULT_MAX_AGE`. Fix: add `resolve_default_price_max_age` helper that reads `CHARON_PRICE_MAX_AGE_SECS` (u64 seconds), logs once on success, warns on parse failure, and falls back to `DEFAULT_MAX_AGE`. Use it in place of the hardcoded constant at both call sites. Per-symbol overrides in `[chainlink_max_age_secs.]` continue to win. Updated the BotConfig docstring to describe the actual behaviour (env var anchors the floor for symbols without per-symbol overrides, not the headline value) so the docs stop misrepresenting the wiring. --- crates/charon-cli/src/main.rs | 46 ++++++++++++++++++++++++++++++-- crates/charon-core/src/config.rs | 11 +++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/crates/charon-cli/src/main.rs b/crates/charon-cli/src/main.rs index 2a8ed24..04fffc0 100644 --- a/crates/charon-cli/src/main.rs +++ b/crates/charon-cli/src/main.rs @@ -482,6 +482,48 @@ 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 +966,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 +1710,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 From fcf566f5ebaeea2a1947ad2cec04258c6c5ac6f1 Mon Sep 17 00:00:00 2001 From: obchain Date: Sun, 3 May 2026 20:05:26 +0530 Subject: [PATCH 2/2] style(cli): fmt info! macro per cargo fmt --- crates/charon-cli/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/charon-cli/src/main.rs b/crates/charon-cli/src/main.rs index 04fffc0..793125c 100644 --- a/crates/charon-cli/src/main.rs +++ b/crates/charon-cli/src/main.rs @@ -504,8 +504,7 @@ fn resolve_default_price_max_age() -> Duration { let dur = Duration::from_secs(secs); info!( env_var = ENV_VAR, - secs, - "chainlink default staleness window overridden via env" + secs, "chainlink default staleness window overridden via env" ); dur }