The Chainlink adapter rejects every Chronicle answer (startedAt is always 0), so Chronicle-priced assets cannot be filled or are silently repriced
OctarineChainlinkOracle is the AggregatorV3 adapter for "Chainlink, RedStone, Chronicle, Superstate" (src/shared/README.md L11). getPrice treats a zero startedAt as an invalid round (L45):
Affected files
src/shared/oracles/OctarineChainlinkOracle.sol#L45
Description
OctarineChainlinkOracle is the AggregatorV3 adapter for "Chainlink, RedStone, Chronicle, Superstate" (src/shared/README.md L11). getPrice treats a zero startedAt as an invalid round (L45):
if (roundId == 0 || answeredInRound < roundId || startedAt == 0 || updatedAt == 0) return (0, updatedAt);
Chronicle's Scribe implements latestRoundData() with startedAt hard-coded to zero. In src/Scribe.sol L289-L307, it sets roundId = 1, startedAt = 0, updatedAt to the poke age, and answeredInRound = roundId. Every Chronicle answer, however fresh, therefore returns price 0 from getPrice, while latestPriceUnchecked (which has no startedAt check) serves the same answer. What happens next depends on how the adapter is registered in OctarineRFQ:
- As the asset's leg to the reference asset:
_unitPricereverts withNoPriceAvailable, so no fill on that asset can settle. - As a direct pair:
_tryOracletreats the feed as down, and the fill settles on the reference-asset route instead of the Chronicle price, without a revert or any other signal.
The other checks already cover what startedAt == 0 was meant to catch: updatedAt == 0 rejects a round that never completed, and both staleness bounds reject an old one.
Recommendations
Drop the startedAt clause (and its now-unused binding):
@@ -38,11 +38,10 @@ contract OctarineChainlinkOracle is OctarineOracleBase, Ownable {{_checkPair(_base, _quote);- (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) =- feed.latestRoundData();+ (uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData();lastUpdated = updatedAt;- if (roundId == 0 || answeredInRound < roundId || startedAt == 0 || updatedAt == 0) return (0, updatedAt);+ if (roundId == 0 || answeredInRound < roundId || updatedAt == 0) return (0, updatedAt);if (answer <= 0) return (0, updatedAt);if (updatedAt > block.timestamp || block.timestamp - updatedAt > maxStaleness) return (0, updatedAt);
With this change the full test suite passes (706 passed, 1 skipped).
Remediation
Fixed in 413a466, as recommended. The Chronicle PoC's harm tests now fail, and the lifecycle test passes.
A runnable Foundry proof of concept for this finding is in the corresponding issue on the project tracker.
Report provenance
Source: the final report dated 28 October 2026. Original finding identifiers are retained, including M-03 and M-04, which were regraded Low. Final dispositions and residual conditions are preserved in this write-up. Read the full report.