Omniscia Euler Finance Audit

EulerRouter Manual Review Findings

EulerRouter Manual Review Findings

ERR-01M: Improper Oracle Resolution Mechanism

Description:

The oracles entry during a EulerRouter::govSetConfig function call is assigned-to only once. Per the design principles of the oracles and specifically the bidirectional trait, an oracle for the base-quote path would be valid for the quote-base path as well.

Impact:

The EulerRouter will report that it does not have an oracle defined for a base and quote pair even though it might actually have registered one.

Example:

src/EulerRouter.sol
43/// @notice Configure a PriceOracle to resolve base/quote.
44/// @param base The address of the base token.
45/// @param quote The address of the quote token.
46/// @param oracle The address of the PriceOracle that resolves base/quote.
47/// @dev Callable only by the governor.
48function govSetConfig(address base, address quote, address oracle) external onlyGovernor {
49 oracles[base][quote] = oracle;
50 emit ConfigSet(base, quote, oracle);
51}

Recommendation:

We advise the code to either assign both oracles[base][quote] and oracles[quote][base], or to order the input base and quote values at the beginning of the EulerRouter::_resolveOracle function.

Alleviation:

The latter of our two recommendations has been incorporated into the codebase, ordering the assets prior to an assignment in the oracles data entry and exposing a EulerRouter::getConfiguredOracle function that will perform this sorting internally to fetch the correct oracle.

ERR-02M: Incorrect Oracle Resolution of EIP-4626 Vaults

Description:

The EulerRouter::_resolveOracle function will incorrectly resolve the oracle path of a quote from any base asset to an EIP-4626 quote asset as it will mutate the input amount as if it were shares of the quote vault.

This vulnerability will manifest itself under the following condition:

  • The EIP-4626 conversion mechanism (i.e. IERC4626::convertToShares) is non-commutative (i.e. for f(x) = wUSDC::convertToShares and g(x) = wwUSDC::convertToShares, f(g(x)) != g(f(x)))

All division operations in Solidity are non-commutative due to truncation, causing all EIP-4626 to produce incorrect results whenever their exchange rate results in a truncation.

Additionally, any non-commutative EIP-4626 implementation would cause significant discrepancies in the converted amounts, and an EIP-4626 may be non-commutative by design as it is not prevented by the standard.

Impact:

The current oracle resolution mechanism will misbehave with a varying degree of severity, depending on the "level" at which the EIP-4626 is non-commutative (i.e. simple truncation vs. more important deviations such as exponential formulae, like a bonding curve).

Example:

src/EulerRouter.sol
89/// @notice Resolve the PriceOracle to call for a given base/quote pair.
90/// @param inAmount The amount of `base` to convert.
91/// @param base The token that is being priced.
92/// @param quote The token that is the unit of account.
93/// @dev Implements the following recursive resolution logic:
94/// 1. Check the base case: `base == quote` and terminate if true.
95/// 2. If a PriceOracle is configured for base/quote in the `oracles` mapping,
96/// return it without transforming the other variables.
97/// 3. If `base` is configured as an ERC4626 vault with internal pricing,
98/// transform inAmount by calling `convertToAssets` and recurse by substituting `asset` for `base`.
99/// 4. If `quote` is configured as an ERC4626 vault with internal pricing,
100/// transform inAmount by calling `convertToAssets` and recurse by substituting `asset` for `quote`.
101/// 5. If there is a fallback oracle, return it without transforming the other variables, else revert.
102/// @return The resolved inAmount.
103/// @return The resolved base.
104/// @return The resolved quote.
105/// @return The resolved PriceOracle to call.
106function _resolveOracle(uint256 inAmount, address base, address quote)
107 internal
108 view
109 returns (uint256, /* inAmount */ address, /* base */ address, /* quote */ address /* oracle */ )
110{
111 // Check the base case
112 if (base == quote) return (inAmount, base, quote, address(0));
113 // 1. Check if base/quote is configured.
114 address oracle = oracles[base][quote];
115 if (oracle != address(0)) return (inAmount, base, quote, oracle);
116 // 2. Recursively resolve `base`.
117 address baseAsset = resolvedVaults[base];
118 if (baseAsset != address(0)) {
119 inAmount = IERC4626(base).convertToAssets(inAmount);
120 return _resolveOracle(inAmount, baseAsset, quote);
121 }
122 // 3. Recursively resolve `quote`.
123 address quoteAsset = resolvedVaults[quote];
124 if (quoteAsset != address(0)) {
125 inAmount = IERC4626(quote).convertToShares(inAmount);
126 return _resolveOracle(inAmount, base, quoteAsset);
127 }
128 // 4. Return the fallback or revert if not configured.
129 oracle = fallbackOracle;
130 if (oracle == address(0)) revert Errors.PriceOracle_NotSupported(base, quote);
131 return (inAmount, base, quote, oracle);
132}

Recommendation:

Mutation of the inAmount value in the current mechanism is misleading, as the code solely behaves properly if the quoteAsset and the base are identical in which case the mutation is essentially applied to the output amount.

As such, we advise the code's recursion to be restructured so as to properly perform the inner-most vault conversion operation first, bubbling the result upwards and converting it on each step to produce the final result.

Alleviation:

The Euler Finance team thoroughly evaluated the ramifications of this exhibit as well as the EulerRouter::_resolveOracle call flows throughout the use cases of the EulerRouter, and opted to omit recursion for the quote asset altogether.

As such, the described misbehaviour is no longer applicable rendering the exhibit alleviated by omission.