Pre-auditThree days of senior audit time for $1,500, credited in full toward your Zealynx audit.3 audit days, credited toward your auditSee how →
L-09·

The Chainlink adapter rejects every answer from the Stork feeds Morpho uses for Nest assets on Plume, because Stork reports timestamps in nanoseconds

Fixedrwarfqoracles
TL;DR

You told us that on Plume you will list Nest assets using the same Chainlink-compatible feeds Morpho uses for them.

Severity
LOW
Method
MManual review
CAT.
02Section · Affected files

Affected files

src/shared/oracles/OctarineChainlinkOracle.sol#L41-L47

03Section · Description

Description

You told us that on Plume you will list Nest assets using the same Chainlink-compatible feeds Morpho uses for them. For the Nest shares, those feeds are Stork's StorkChainlinkAdapter deployments. Their verified source states that timestamps are in nanoseconds. latestRoundData() returns the Stork update time in nanoseconds as roundId, startedAt, updatedAt and answeredInRound.

OctarineChainlinkOracle.getPrice treats an updatedAt later than the block time as invalid (L46):

solidity
if (updatedAt > block.timestamp || block.timestamp - updatedAt > maxStaleness) return (0, updatedAt);

A nanosecond timestamp is about 10^9 times the block time, so every Stork answer, however fresh, reads as no price. No maxStaleness setting changes this. Morpho's oracle reads only answer, which is why the same feeds work there.

The recommended fix for [L-03] does not cover this: it removes the startedAt check, while Stork fails the updatedAt check.

What we measured on Plume:

  • 12 of the 45 feed slots across Plume's 35 MorphoChainlinkOracleV2 deployments are Stork adapters, all in nanoseconds. They cover 10 Nest assets: nTBILL, nALPHA, nBASIS, nYIELD, nCREDIT, nELIXIR, nINSTO, nPAYFI, nETF and nWISDOM.
  • The older eOracle feeds for nTBILL, nALPHA, nBASIS and nINSTO revert, so for those assets Stork is the only live Chainlink-compatible feed.
04Section · Impact

Impact

With the Stork feed as an asset's reference leg, the asset cannot be sold through the RFQ: every fill reverts NoPriceAvailable. No value moves.

With the Stork feed on a direct pair, the direct route reads as down and the fill silently settles on the reference-asset route, as described in [L-03]. In the test, the maker pays 947.15 X instead of 889.70 X at the live Stork price. That difference is set by the test's chosen reference price, not by Stork.

The fallbacks within reach also carry open findings:

  • the setter oracle: [M-01], [L-04];
  • OctarineCallOracle on the Stork adapter's latestAnswer(): it must then run without a timestamp call, because latestTimestamp() is also in nanoseconds, which is [M-04].
05Section · Recommendations

Recommendations

  1. Register Nest assets through a seconds-based Stork adapter, not the nanosecond adapters Morpho uses. Stork already deploys such a variant on Plume (0x6022a020Ca5c611304B9E97F37AEE0C38455081A, which divides timestampNs by 1e9). Deploy one per Nest price id. In our fork tests, all ten Nest shares priced exactly and filled through it. Set oracleMaxStaleness from the feed's real cadence: Stork pushed hourly, and the largest gap we saw was 3,668 s.
  2. Refuse a misfit feed at deployment rather than at fill time. Have the adapter's constructor read the feed once and refuse an updatedAt in the future:
diff
diff --git a/src/shared/oracles/OctarineChainlinkOracle.sol b/src/shared/oracles/OctarineChainlinkOracle.sol
--- a/src/shared/oracles/OctarineChainlinkOracle.sol
+++ b/src/shared/oracles/OctarineChainlinkOracle.sol
@@ -22,6 +22,8 @@ contract OctarineChainlinkOracle is OctarineOracleBase, Ownable {
feed = IAggregatorV3(_feed);
feedDecimals = IAggregatorV3(_feed).decimals();
inverse = _inverse;
+ (,,, uint256 updatedAt,) = IAggregatorV3(_feed).latestRoundData();
+ if (updatedAt > block.timestamp) revert InvalidGuards();
}
function setMaxStaleness(uint256 newMaxStaleness) external onlyOwner {

With this check, deploying the adapter on the Stork feed reverts InvalidGuards, and your full test suite passes (706 passed, 1 skipped). A feed that has not published yet still deploys and reads as no price until its first round. A feed that reverts until its first round makes the constructor revert too, so deploy the adapter after that round.

  1. Do not "fix" this by letting a nanosecond updatedAt through. The RFQ would then compare a nanosecond time against block.timestamp + staleness, and every read would be fresh forever.
06Section · Remediation

Remediation

Fixed in 9cecec3. The adapter now reads the feed once at deployment and refuses an updatedAt in the future.

On a Plume fork:

  • deploying on the nWISDOM Stork nanosecond feed reverts InvalidGuards;
  • Stork's seconds-based adapter (0x6022a020Ca5c611304B9E97F37AEE0C38455081A) deploys and settles a 1,000-token fill at 996.80.

The check runs only at deployment, so deploy each adapter after the feed's first round.

A runnable Foundry proof of concept for this finding is in the corresponding issue on the project tracker.

07Section · Report provenance

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.

Status
Fixed
L-09