Omniscia Euler Finance Audit

RedstoneCoreOracle Manual Review Findings

RedstoneCoreOracle Manual Review Findings

RCO-01M: Improper Integration of Redstone On-Demand Feeds

Description:

The Redstone oracle system is meant to allow an on-demand price oracle system to be utilized by smart contracts by utilizing low-level mutations of a transaction's calldata to append signatures that validate a particular price measurement that the transaction may require.

The RedstoneCoreOracle implementation by the Euler Finance team attempts to incorporate the on-demand price feeds by utilizing a push model whereby "on-demand" price measurements are submitted via the RedstoneCoreOracle::updatePrice function and consequently consumed by the RedstoneCoreOracle::_getQuote function.

This contradicts the entire premise of the on-demand data points, as the RedstoneCoreOracle functions identically to the "classic" data feeds already provided by Redstone.

Impact:

We do not consider the current Redstone oracle integration standard or secure. The RedstoneCoreOracle::_getQuote function will not succeed if a price point has not been submitted via the RedstoneCoreOracle::updatePrice function, and the oracles of the Euler Finance Oracle repository should not require active maintenance.

Example:

src/adapter/redstone/RedstoneCoreOracle.sol
57/// @notice Ingest a signed update message and cache it on the contract.
58/// @dev Validation logic inherited from PrimaryProdDataServiceConsumerBase.
59function updatePrice() external {
60 // Use the cache if the previous price is still fresh.
61 if (block.timestamp < lastUpdatedAt + RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) return;
62
63 uint256 price = getOracleNumericValueFromTxMsg(feedId);
64 if (price > type(uint208).max) revert Errors.PriceOracle_Overflow();
65 lastPrice = uint208(price);
66 lastUpdatedAt = uint48(block.timestamp);
67}
68
69/// @notice Get the quote from the Redstone feed.
70/// @param inAmount The amount of `base` to convert.
71/// @param _base The token that is being priced.
72/// @param _quote The token that is the unit of account.
73/// @return The converted amount using the Redstone feed.
74function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
75 bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote);
76
77 uint256 staleness = block.timestamp - lastUpdatedAt;
78 if (staleness > maxStaleness) revert Errors.PriceOracle_TooStale(staleness, maxStaleness);
79
80 return ScaleUtils.calcOutAmount(inAmount, lastPrice, scale, inverse);
81}

Recommendation:

In order to correctly integrate with Redstone, we advise either of the following two approaches to be incorporated to the RedstoneCoreOracle contract.

Approach A involves updating the system to utilize the "classic" oracles of the Redstone ecosystem, thereby no longer requiring the manual RedstoneCoreOracle::updatePrice mechanism and behaving similarly to the other oracles within the system that do not require active maintenance.

Approach B is more complicated, and involves taking advantage of the ProxyConnector of the Redstone ecosystem. Specifically, the RedstoneCoreOracle::_getQuote function is capable of processing Redstone oracle payloads if the top-level EulerRouter::getQuote / EulerRouter::getQuotes functions were mutated to forward the Redstone-related calldata via the ProxyConnector::proxyCalldataView function.

The second of the aforementioned approaches would ensure that the correct on-demand Redstone model is utilized, delegating responsibility of proper calldata appendment to the caller of the EulerRouter and preventing the contract from becoming inoperable unless a separate, top-level call to RedstoneCoreOracle::updatePrice is made.

Alleviation:

The Euler Finance team has evaluated our analysis and, after careful deliberation, consider it to not be reflective of the Euler Finance oracle system's actual purpose.

In detail, the Euler Finance team clarified that the EVK which is meant to integrate with the Euler Finance oracle system supports batch transactions that will permit the price of the oracle to be updated prior to its usage in a secure manner.

Furthermore, the Euler Finance team clarified that they consciously chose to integrate with the push model to allow protocols the option of utilizing such oracle providers.

As the exhibit relied on a misconception about the intentions of the Euler Finance oracle system in relation to data availability, we consider it to be invalid and have lowered its severity to informational to reflect this fact.

RCO-02M: Inexistent Capability of Functionality Overrides

Description:

The official documentation of the Redstone system indicates that contracts which interface with their oracles should be upgradeable to be able to adjust their logic.

The rationale behind this approach is that the Redstone on-demand data feed system utilizes a signature threshold scheme, and compromised signers could generate incorrect data points that would be accepted by the RedstoneCoreOracle.

Impact:

In comparison to other price feed protocols such as Chainlink, the Redstone oracle on-demand system is validated locally and cannot be influenced by external actions. As such, there is no inherent "circuit-breaker" or other action that the Redstone team can take to invalidate the signers already registered by a PrimaryProdDataServiceConsumerBase.

In case of a systematic failure / compromise of Redstone's signers, the RedstoneCoreOracle will not be equipped to upgrade its logic or halt its operations in response to such an event.

Example:

src/adapter/redstone/RedstoneCoreOracle.sol
16contract RedstoneCoreOracle is PrimaryProdDataServiceConsumerBase, BaseAdapter {

Recommendation:

We advise the contract to be made upgradeable, permitting functions such as the PrimaryProdDataServiceConsumerBase::getAuthorisedSignerIndex and PrimaryProdDataServiceConsumerBase::getUniqueSignersThreshold validation functions to be updated in case of a systematic failure of Redstone.

Alternatively, we advise a circuit breaker to be introduced, permitting price measurements via the contract to revert and thus prompting integrators to utilize alternative data feeds in case of the same scenario.

Alleviation:

The Euler Finance team evaluated this exhibit and contested its severity as the oracle adapters within the Euler Finance codebase are considered to be immutable, with the responsibility of using a correct oracle delegated to a higher-level implementation such as a router that will be capable of overwriting the oracle to be used for a particular asset pair with an updated variant.

As such, we consider this exhibit safely acknowledge and its severity diminished accordingly per the intended purpose of the Euler Finance oracle system.

RCO-03M: Potentially Unsupported Function Signature

Description:

The code of the RedstoneCoreOracle::constructor will invoke the IERC20::decimals function as exposed by the forge-std library, however, the IERC20::decimals function is not actually part of the EIP-20 specification.

Impact:

Most EIP-20 assets do implement the IERC20::decimals function signature, however, it is not mandated by the standard and as such a small subset of EIP-20 tokens is incompatible with the RedstoneCoreOracle presently.

Example:

src/adapter/redstone/RedstoneCoreOracle.sol
36/// @notice Deploy a RedstoneCoreOracle.
37/// @param _base The address of the base asset corresponding to the feed.
38/// @param _quote The address of the quote asset corresponding to the feed.
39/// @param _feedId The identifier of the price feed.
40/// @param _maxStaleness The maximum allowed age of the price.
41/// @dev Base and quote are not required to correspond to the feed assets.
42/// For example, the ETH/USD feed can be used to price WETH/USDC.
43constructor(address _base, address _quote, bytes32 _feedId, uint256 _maxStaleness) {
44 if (_maxStaleness < RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) {
45 revert Errors.PriceOracle_InvalidConfiguration();
46 }
47
48 base = _base;
49 quote = _quote;
50 feedId = _feedId;
51 maxStaleness = _maxStaleness;
52 uint8 baseDecimals = IERC20(base).decimals();
53 uint8 quoteDecimals = IERC20(quote).decimals();
54 scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, FEED_DECIMALS);
55}

Recommendation:

In case all EIP-20 assets are expected to be supported, we advise decimals to either be opportunistically queried or for decimals to be supplied as input arguments thus permitting any token to have a RedstoneCoreOracle deployed.

Alleviation:

A common BaseAdapter::_getDecimals implementation has been introduced in the BaseAdapter upstream contract that will attempt to fetch the IERC20::decimals of an asset and default to 32 if they cannot be fetched.

As such, we consider this exhibit fully alleviated.

RCO-04M: Improper Assumption of Oracle Decimals

Description:

The RedstoneCoreOracle contract assumes that all price points reported by Redstone will have 8 decimals of accuracy, however, no such trait is guaranteed.

Inspection of the official Redstone oracle mono-repo highlights that the system supports dynamic decimals across both its off-chain and on-chain infrastructure, and certain "classic" data feeds explicitly yield a number different than 8 for their decimals (example).

Impact:

Although most use-cases indicate that 8 decimals are in-use by Redstone, their system explicitly supports a different number of decimals and as such the RedstoneCoreOracle contract implementation should too.

Example:

src/adapter/redstone/RedstoneCoreOracle.sol
17uint8 internal constant FEED_DECIMALS = 8;
18/// @notice The address of the base asset corresponding to the feed.
19address public immutable base;
20/// @notice The address of the quote asset corresponding to the feed.
21address public immutable quote;
22/// @notice The identifier of the price feed.
23/// @dev See https://app.redstone.finance/#/app/data-services/redstone-primary-prod
24bytes32 public immutable feedId;
25/// @notice The maximum allowed age of the price.
26uint256 public immutable maxStaleness;
27/// @notice The scale factors used for decimal conversions.
28Scale internal immutable scale;
29/// @notice The last updated price.
30/// @dev This gets updated after calling `updatePrice`.
31uint208 public lastPrice;
32/// @notice The timestamp of the last update.
33/// @dev Gets updated ot `block.timestamp` after calling `updatePrice`.
34uint48 public lastUpdatedAt;
35
36/// @notice Deploy a RedstoneCoreOracle.
37/// @param _base The address of the base asset corresponding to the feed.
38/// @param _quote The address of the quote asset corresponding to the feed.
39/// @param _feedId The identifier of the price feed.
40/// @param _maxStaleness The maximum allowed age of the price.
41/// @dev Base and quote are not required to correspond to the feed assets.
42/// For example, the ETH/USD feed can be used to price WETH/USDC.
43constructor(address _base, address _quote, bytes32 _feedId, uint256 _maxStaleness) {
44 if (_maxStaleness < RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) {
45 revert Errors.PriceOracle_InvalidConfiguration();
46 }
47
48 base = _base;
49 quote = _quote;
50 feedId = _feedId;
51 maxStaleness = _maxStaleness;
52 uint8 baseDecimals = IERC20(base).decimals();
53 uint8 quoteDecimals = IERC20(quote).decimals();
54 scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, FEED_DECIMALS);
55}

Recommendation:

We advise the decimals to be user-configured on deployment, ensuring that they match the accuracy of the data point reported by the on-demand oracles of Redstone.

Alleviation:

The Euler Finance team evaluated this exhibit and, after confirming with the Redstone team directly, proceeded with adjusting decimals to be configurable instead of presumed as 8 effectively alleviating this exhibit.

RCO-05M: Misconceived Data Staleness

Description:

The data staleness validated by the RedstoneCoreOracle::_getQuote function is slightly non-standard as the block.timestamp recorded by the RedstoneCoreOracle::updatePrice function does not correspond to the actual timestamp the price was recorded in.

In detail, the RedstoneCoreOracle::updatePrice function will accept a price reported at a timestamp that is up to 3 minutes behind or 1 minute ahead of the current block.timestamp. As a result, the actual "staleness" validated by the RedstoneCoreOracle::_getQuote function is staleness - 3 minutes or staleness + 1 minutes.

The latter of the two is of no concern as a fresher-than-considered price is evaluated, however, the former of the two would result in potentially incorrect data staleness being enforced.

Impact:

The actual data staleness enforced by the RedstoneCoreOracle::_getQuote differs from the one that the user has specified, permitting a data point that is maxStaleness + RedstoneDefaultsLib::DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS in the past to be consumed.

Example:

src/adapter/redstone/RedstoneCoreOracle.sol
36/// @notice Deploy a RedstoneCoreOracle.
37/// @param _base The address of the base asset corresponding to the feed.
38/// @param _quote The address of the quote asset corresponding to the feed.
39/// @param _feedId The identifier of the price feed.
40/// @param _maxStaleness The maximum allowed age of the price.
41/// @dev Base and quote are not required to correspond to the feed assets.
42/// For example, the ETH/USD feed can be used to price WETH/USDC.
43constructor(address _base, address _quote, bytes32 _feedId, uint256 _maxStaleness) {
44 if (_maxStaleness < RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) {
45 revert Errors.PriceOracle_InvalidConfiguration();
46 }
47
48 base = _base;
49 quote = _quote;
50 feedId = _feedId;
51 maxStaleness = _maxStaleness;
52 uint8 baseDecimals = IERC20(base).decimals();
53 uint8 quoteDecimals = IERC20(quote).decimals();
54 scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, FEED_DECIMALS);
55}
56
57/// @notice Ingest a signed update message and cache it on the contract.
58/// @dev Validation logic inherited from PrimaryProdDataServiceConsumerBase.
59function updatePrice() external {
60 // Use the cache if the previous price is still fresh.
61 if (block.timestamp < lastUpdatedAt + RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) return;
62
63 uint256 price = getOracleNumericValueFromTxMsg(feedId);
64 if (price > type(uint208).max) revert Errors.PriceOracle_Overflow();
65 lastPrice = uint208(price);
66 lastUpdatedAt = uint48(block.timestamp);
67}
68
69/// @notice Get the quote from the Redstone feed.
70/// @param inAmount The amount of `base` to convert.
71/// @param _base The token that is being priced.
72/// @param _quote The token that is the unit of account.
73/// @return The converted amount using the Redstone feed.
74function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
75 bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote);
76
77 uint256 staleness = block.timestamp - lastUpdatedAt;
78 if (staleness > maxStaleness) revert Errors.PriceOracle_TooStale(staleness, maxStaleness);
79
80 return ScaleUtils.calcOutAmount(inAmount, lastPrice, scale, inverse);
81}

Recommendation:

We advise the maxStaleness value to either be stored as _maxStaleness - RedstoneDefaultsLib::DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS, or to be utilized in RedstoneCoreOracle::_getQuote as maxStaleness - RedstoneDefaultsLib::DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS.

Alleviation:

The oracle was refactored to employ two distinct staleness concepts; one applicable to the actual price measurements from the Redstone system and the other applicable to the local cache system.

In addition to these changes, the Euler Finance team introduced documentation highlighting that a user can choose the most suitable Redstone price in the [block.timestamp - maxPriceStaleness, block.timestamp] interval and consider it an unavoidable trait inherent to any on-chain integration.

As the staleness concepts have been properly distinguished and the inherent risk of the Redstone oracle integration has been adequately documented, we consider all possible remediative actions in relation to this exhibit to have been carried out in the codebase.