feat(scanner): Chainlink PriceCache with staleness check#35
Merged
Conversation
Per-chain, per-asset Chainlink feed reader that backs the scanner and (soon) the profit calculator with USD prices that are either fresh or explicitly rejected — no silent reliance on stale rounds. - `IAggregatorV3` sol! binding covering `decimals()` and `latestRoundData()` - `CachedPrice` struct (price, decimals, updatedAt, fetchedAt) - `PriceCache` — provider + `(symbol → feed address)` + DashMap cache, `refresh / refresh_all / get / is_fresh` surface - 10-minute default `max_age`; negative `answer` rejected as degraded - New `[chainlink.<chain>]` config section with the five BSC feeds used by Venus (BNB, BTCB, ETH, USDT, USDC) - CLI `listen` performs one startup refresh and logs each feed's price + decimals; cache is ready for downstream consumers - Unit tests on freshness predicate + feed iterator - Live integration tests (`tests/chainlink_refresh.rs`) hit BSC mainnet: one happy-path refresh, one forced-stale rejection
This was referenced Apr 22, 2026
Closed
Closed
Closed
… fail-loud clock, BTCB fix
- Reject answeredInRound < roundId in refresh(): a partially-aggregated
round carries the previous answer behind a fresh updatedAt, silently
serving stale prices to the liquidation path.
- Reject updatedAt == 0 explicitly — distinguishes an uninitialized
aggregator from a merely-stale one in the log message.
- unix_now() returns Result; refresh() and is_fresh() propagate the clock
error instead of coercing to 0. Previously a clock skew made every
cached entry look fresh-forever ('some_updated_at + max_age >= 0' is
always true), bypassing the staleness gate entirely.
- with_per_symbol_max_age constructor accepts a HashMap<String, Duration>
so stablecoin feeds with day-long heartbeats and sub-second deviation
feeds can coexist without needing a single global compromise.
- CachedPrice::scaled_to(target_decimals) normalizes the raw feed answer
into the consumer's decimal space using integer arithmetic (no f64).
Venus oracle (36 - underlying) and Aave 1e18 consumers now get a
correctly-sized USD value instead of being off by 10^10.
- Fix BTCB/USD feed address in config/default.toml:
0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf (malformed, 41 hex chars) ->
0x264990fbd0A4796A3E3d8E37022BdAf1A5a4C1f0 (canonical docs.chain.link).
Tag each feed with the human-readable pair so future audits are one
grep away.
Closes #108 #109 #110 #111 #112
…cecache # Conflicts: # config/default.toml # crates/charon-cli/src/main.rs # crates/charon-scanner/Cargo.toml # crates/charon-scanner/src/lib.rs
5 tasks
obchain
added a commit
that referenced
this pull request
Apr 26, 2026
…loses #109) (#337) The address shipped in #109's "fix" — 0x264990fbd0…1f0 — has no bytecode on BSC mainnet. `cast code` returns 0x against both dRPC and bsc-dataseed.binance.org, so every `decimals()` / `latestRoundData()` call against it abi-decodes garbage and BTCB-collateral positions have been silently unpriceable since #35 landed. Discovered the live address by walking the on-chain Venus oracle path: ResilientOracle (0x6592b5DE…) → ChainlinkOracle facet (0x1B210344…) → tokenConfigs(BTCB) → 0x8ECF7dE377F788A813F5215668E282556b35f300 Verified live against bsc.drpc.org: cast code → 19,145 bytes of bytecode decimals() → 18 (Venus uses 18-decimal Chainlink wrappers, not the standard 8-decimal feeds) description() → "BTC / USD" latestRoundData → answer ≈ 78,090e18 (= $78,090), fresh round Confirmed Chainlink-ops-owned: feed `owner()` is 0xcb6754D5…, the same multisig that owns the BNB/USD canonical feed. PriceCache (`crates/charon-scanner/src/oracle.rs:33,59`) reads `decimals()` per feed at startup and `CachedPrice::scaled_to` rescales to a target — the 8→18 decimal difference is invisible to consumers, no Rust changes required. Same swap applied to `config/fork.toml` since the fork profile pins mainnet block forks and would have hit the same garbage decode. Comment block above [chainlink.bnb] in default.toml records the Venus-oracle discovery path so future reviewers don't have to walk it again. Adjacent finding (separate issue recommended): the other four [chainlink.bnb] entries (BNB, ETH, USDT, USDC) were sourced the same way from docs.chain.link. Given one of five was bytecodeless, the remaining four warrant the same cast-code/decimals/description sanity check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11
Per-chain, per-asset Chainlink feed reader that backs the scanner and (soon) the profit calculator with USD prices that are either fresh or explicitly rejected — no silent reliance on stale rounds.
IAggregatorV3sol!binding coveringdecimals()andlatestRoundData()CachedPricestruct (price, decimals, updatedAt, fetchedAt)PriceCache— provider +(symbol → feed address)+ DashMap cache;refresh / refresh_all / get / is_freshmax_age; negativeanswerrejected as degraded[chainlink.<chain>]config section with five BSC feeds (BNB, BTCB, ETH, USDT, USDC)listenperforms one startup refresh and logs each feed's price + decimalsDepends on #10 (
feat/09-health-scanner).