Omniscia Euler Finance Audit
EulerRouter Manual Review Findings
EulerRouter Manual Review Findings
ERR-01M: Improper Oracle Resolution Mechanism
Type | Severity | Location |
---|---|---|
Logical Fault | EulerRouter.sol:L49, L114 |
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:
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
Type | Severity | Location |
---|---|---|
Logical Fault | EulerRouter.sol:L125 |
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. forf(x) = wUSDC::convertToShares
andg(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:
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 internal108 view109 returns (uint256, /* inAmount */ address, /* base */ address, /* quote */ address /* oracle */ )110{111 // Check the base case112 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.