Chainlink data staleness
Description
The Wooracle contract relies on Chainlink as one of its third-party pricing sources. The latestRoundData
method is used to retrieve data, including token prices. However, the contract does not sufficiently check for stale data, which can result in inaccurate pricing information.
function _cloPriceInQuote(address _fromToken, address _toToken)
internal
view
returns (uint256 refPrice, uint256 refTimestamp)
{
address baseOracle = clOracles[_fromToken].oracle;
// NOTE: Only for chains where chainlink oracle is unavailable
// if (baseOracle == address(0)) {
// return (0, 0);
// }
require(baseOracle != address(0), "WooracleV2_2: !oracle");
address quoteOracle = clOracles[_toToken].oracle;
uint8 quoteDecimal = clOracles[_toToken].decimal;
(, int256 rawBaseRefPrice, , uint256 baseUpdatedAt, ) = AggregatorV3Interface(baseOracle).latestRoundData();
(, int256 rawQuoteRefPrice, , uint256 quoteUpdatedAt, ) = AggregatorV3Interface(quoteOracle).latestRoundData();
// ...
}
Impact
Should the data returned by Chainlink be stale, the health-factor calculation would be incorrect, which could in turn affect the entire protocol.
Recommendations
We recommend adding checks for each of the returned values to ensure that the data is not stale.
function _cloPriceInQuote(address _fromToken, address _toToken)
internal
view
returns (uint256 refPrice, uint256 refTimestamp)
{
address baseOracle = clOracles[_fromToken].oracle;
// NOTE: Only for chains where chainlink oracle is unavailable
// if (baseOracle == address(0)) {
// return (0, 0);
// }
require(baseOracle != address(0), "WooracleV2_2: !oracle");
address quoteOracle = clOracles[_toToken].oracle;
uint8 quoteDecimal = clOracles[_toToken].decimal;
(
+ uint80 baseRoundID,
int256 rawBaseRefPrice,
+ uint baseStartedAt,
uint256 baseUpdatedAt,
+ uint80 baseAnsweredInRound
) = AggregatorV3Interface(baseOracle).latestRoundData();
(
+ uint80 quoteRoundID,
int256 rawQuoteRefPrice,
+ uint quoteStartedAt,
uint256 quoteUpdatedAt
+ uint80 quoteAnsweredInRound
) = AggregatorV3Interface(quoteOracle).latestRoundData();
+ require(
+ baseUpdatedAt != 0,
+ "base round is not complete"
+ );
+
+ require(
+ baseAnsweredInRound >= baseRoundID,
+ "base stale data"
+ );
+ require(
+ quoteUpdatedAt != 0,
+ "quote round is not complete"
+ );
+ require(
+ quoteAnsweredInRound >= quoteRoundID,
+ "quote stale data"
+ );
// ...
}
Remediation
Additionally, the WOOFI team has stated that:
Checking the staleness onchain costs more gas than necessary (regarding the TTL is typically 24 hours). However, we add an offchain- script monitor for chainlink staleness (as well as L2 sequencer status), if any oracle got expired, we pause the WooPP immediately. So essentially, we're having the staleness monitor now, but in offchain.