Moonwell
Summary
ExpandThe Moonwell Oracle codebase is generally well-structured with appropriate use of Chainlink oracle integration patterns, OEV capture mechanisms, and bounded composite oracles. The OEV wrappers implement proper round-delay mechanisms to capture MEV value for the protocol, and the bounded composite oracle provides sensible fallback logic for derivative asset pricing.
The most notable issue is the `ChainlinkCompositeOracle` returning `block.timestamp` instead of actual oracle update timestamps, which masks price staleness from downstream consumers. This could allow stale prices to be used for lending/borrowing decisions affecting derivative assets (wstETH, cbETH, rETH, weETH, LBTC, etc.). Additionally, there is an inconsistency in reentrancy protection between the core market and Morpho OEV wrappers, and missing stale price checks in the base `ChainlinkOracle`.
Findings
4 issues identified
ChainlinkCompositeOracle masks oracle staleness by returning block.timestamp
src/oracles/ChainlinkCompositeOracle.sol:56
View Details
Description
The latestRoundData() function returns block.timestamp as the updatedAt value instead of the actual minimum timestamp from underlying Chainlink feeds. Any downstream staleness check will always see the price as fresh, even when underlying feeds are stale. This affects derivative assets including wstETH, cbETH, rETH, weETH, wrsETH, and LBTC.
Impact
Stale prices from underlying Chainlink feeds could be used for lending/borrowing decisions without detection, potentially enabling users to supply overvalued collateral, borrow against stale prices, or avoid proper liquidation.
Recommendation
Return the minimum updatedAt timestamp from all underlying oracle feeds instead of block.timestamp.
Verification
Fix
Returns the minimum updatedAt timestamp from all underlying oracle feeds instead of block.timestamp, allowing downstream consumers to accurately detect when any underlying feed is stale.
src/oracles/ChainlinkCompositeOracle.sol
Incomplete oracle validation in ChainlinkOEVMorphoWrapper._getLoanTokenPrice
src/oracles/ChainlinkOEVMorphoWrapper.sol:388
View Details
Description
The _getLoanTokenPrice function only checks loanAnswer > 0 but does not validate updatedAt != 0 or answeredInRound >= roundId. The equivalent function in ChainlinkOEVWrapper properly calls _validateRoundData with all checks. A stale loan token price affects the collateral split calculation in updatePriceEarlyAndLiquidate.
Impact
A stale loan token price could cause incorrect collateral split between liquidator and protocol fee recipient during OEV liquidations through the Morpho wrapper.
Recommendation
Add full round data validation using _validateRoundData, matching the pattern in ChainlinkOEVWrapper._getLoanTokenPrice.
Verification
Fix
Replaces the partial validation (only answer > 0) with the full _validateRoundData call that also checks updatedAt != 0 and answeredInRound >= roundId, matching the validation standard used in ChainlinkOEVWrapper.
src/oracles/ChainlinkOEVMorphoWrapper.sol
_validateRoundData inside try block causes uncatchable revert in ChainlinkFeedOEVWrapper
src/oracles/ChainlinkFeedOEVWrapper.sol:121
View Details
Description
In latestRoundData(), _validateRoundData is called inside the try block’s success handler. The catch block only catches reverts from the external call originalFeed.getRoundData(). If the external call succeeds but the returned data fails validation, the revert propagates up uncaught, causing the entire latestRoundData() to revert. This differs from ChainlinkOEVWrapper which correctly validates only after the loop.
Impact
If any previous round has data that fails validation (e.g., incomplete round with updatedAt==0), the oracle becomes temporarily unusable, freezing all market operations (supply, borrow, redeem, liquidation) until the maxRoundDelay passes.
Recommendation
Use soft validation (if-check) inside the loop and reserve the hard revert for the final fallback, matching ChainlinkOEVWrapper’s pattern.
Verification
Fix
Replaces the hard-reverting _validateRoundData call inside the loop with a soft if-check. Invalid previous rounds are now skipped instead of causing an uncatchable revert. The hard validation is preserved for the final fallback to the latest round data.
src/oracles/ChainlinkFeedOEVWrapper.sol
Missing heartbeat/staleness duration check on Chainlink price feeds
src/oracles/ChainlinkOracle.sol:89-100
View Details
Description
The getChainlinkPrice function in ChainlinkOracle.sol checks that updatedAt != 0 and answer > 0, but does not validate that the price data is recent by comparing updatedAt against a maximum staleness threshold (heartbeat). Additionally, it is missing the answeredInRound >= roundId check that the OEV wrappers include. The same missing heartbeat check also applies to ChainlinkCompositeOracle.sol (getPriceAndDecimals at line 167) and ChainlinkBoundedCompositeOracle.sol (_getValidatedOracleData at line 213). Without a heartbeat check, if a Chainlink feed stops updating (e.g., during network congestion or feed deprecation), the protocol will continue using an arbitrarily stale price, which could allow undercollateralized borrowing or prevent valid liquidations.
Impact
An attacker could exploit a stale oracle price to borrow against inflated collateral values or avoid liquidation. If a Chainlink feed goes stale during a price crash, positions that should be liquidated will remain open, leading to bad debt accrual for the protocol. Conversely, if the feed goes stale during a price pump, users could be unfairly liquidated based on an outdated lower price.
Recommendation
Add a configurable heartbeat duration check to all oracle price validation functions. Each feed should have a maximum allowed staleness period (matching the Chainlink feed’s heartbeat, e.g., 3600s for most feeds, 86400s for some). Also add the missing answeredInRound >= roundId check to ChainlinkOracle.getChainlinkPrice.
Verification
Fix
Conclusion
ExpandThe Moonwell Oracle codebase demonstrates solid security practices overall, with proper Chainlink integration patterns, OEV capture mechanisms, and appropriate access controls. The bounded composite oracle implements sensible fallback logic, and the OEV wrappers correctly delay price updates to capture protocol value.
The primary concern is the ChainlinkCompositeOracle masking oracle staleness by returning block.timestamp, which affects multiple derivative asset markets. While the direct exploitation path requires a Chainlink feed outage (an unlikely but not unprecedented event), the fix is straightforward and recommended before any increase in TVL for affected markets.
The remaining findings are Low severity and reflect inconsistencies between similar contracts rather than fundamental design flaws. The codebase appears suitable for mainnet deployment after addressing the Medium-severity timestamp masking issue and the identified inconsistencies between the OEV wrapper implementations.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.