PR: #35 (feat/10-chainlink-pricecache)
Commit: 5c36873
File: crates/charon-scanner/src/oracle.rs, refresh()
Problem: Chainlink latestRoundData() returns five fields: roundId, answer, startedAt, updatedAt, answeredInRound. Invariant: answeredInRound < roundId means aggregator has not finished aggregating current round. Answer in that state is value from previous older round — stale data published under new round ID. PR captures round.answeredInRound from sol! binding but never uses it.
Current staleness check (updated_at + max_age >= now) does NOT catch this: partially-completed round still carries recent updatedAt from last completed round, so freshness predicate passes while returned answer is old. Under high volatility gap can be large enough to cause mispriced liquidation.
Risk: Liquidation executed on stale answer causes:
- Under-priced collateral seized → protocol loss.
- Over-priced collateral seized → borrower griefed / Venus revert.
Fix: Before cache insertion:
if round.answeredInRound < round.roundId {
anyhow::bail!(
"feed '{symbol}': round not complete (answeredInRound={}, roundId={})",
round.answeredInRound, round.roundId
);
}
Must execute before staleness check. Add unit test with stub/mock aggregator returning answeredInRound < roundId.
PR: #35 (feat/10-chainlink-pricecache)
Commit: 5c36873
File: crates/charon-scanner/src/oracle.rs,
refresh()Problem: Chainlink
latestRoundData()returns five fields:roundId, answer, startedAt, updatedAt, answeredInRound. Invariant:answeredInRound < roundIdmeans aggregator has not finished aggregating current round. Answer in that state is value from previous older round — stale data published under new round ID. PR capturesround.answeredInRoundfrom sol! binding but never uses it.Current staleness check (
updated_at + max_age >= now) does NOT catch this: partially-completed round still carries recentupdatedAtfrom last completed round, so freshness predicate passes while returned answer is old. Under high volatility gap can be large enough to cause mispriced liquidation.Risk: Liquidation executed on stale answer causes:
Fix: Before cache insertion:
Must execute before staleness check. Add unit test with stub/mock aggregator returning
answeredInRound < roundId.