Omniscia Euler Finance Audit

UniswapV3Oracle Manual Review Findings

UniswapV3Oracle Manual Review Findings

UVO-01M: Inexistent Validation of Observation Cardinality Length

Description:

The Uniswap V3 TWAP system relies on a set of observations made within a finite-length array that is initialized at 1. This means that a TWAP of a Uniswap V3 pool is insecure to utilize unless its cardinality has been expanded to accommodate for the TWAP window that the caller wishes to query for.

As detailed extensively in the Uniswap V3 documentation, the cardinality of a pool should be increased the longer the TWAP window is desired to be by invoking the IUniswapV3Pool::increaseObservationCardinalityNext function.

Impact:

A UniswapV3Oracle may fail to execute properly out-of-the-box due to not validating the cardinality of the pool that it is deployed for.

Example:

src/adapter/uniswap/UniswapV3Oracle.sol
44/// @notice Get a quote by calling the pool's TWAP oracle.
45/// @param inAmount The amount of `base` to convert.
46/// @param base The token that is being priced. Either `tokenA` or `tokenB`.
47/// @param quote The token that is the unit of account. Either `tokenB` or `tokenA`.
48/// @return The converted amount.
49function _getQuote(uint256 inAmount, address base, address quote) internal view override returns (uint256) {
50 if (!((base == tokenA && quote == tokenB) || (base == tokenB && quote == tokenA))) {
51 revert Errors.PriceOracle_NotSupported(base, quote);
52 }
53 // Size limitation enforced by the pool.
54 if (inAmount > type(uint128).max) revert Errors.PriceOracle_Overflow();
55
56 uint32[] memory secondsAgos = new uint32[](2);
57 secondsAgos[0] = twapWindow;
58
59 // Calculate the mean tick over the twap window.
60 (int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
61 int24 tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int32(twapWindow));
62 return OracleLibrary.getQuoteAtTick(tick, uint128(inAmount), base, quote);
63}

Recommendation:

The cardinality required per minute varies from chain-to-chain, and represents the number of samples that should be taken per minute for a particular token to be considered accurate.

For example, in the Ethereum mainnet, a block is generated at a median of 12 seconds meaning that each minute has 5 blocks within. As such, a cardinality of 4-5 is considered acceptable per minute.

The cardinality at which the pool is set to should be evaluated during its UniswapV3Oracle::constructor and as such, we advise an IUniswapV3Pool::increaseObservationCardinalityNext function call to be introduced to it with an argument generated via a _cardinaltyPerMinute input argument.

Alleviation:

The Euler Finance team opted to acknowledge this exhibit as they do not consider it a vulnerability given that the UniswapV3Oracle requires further preparation beyond cardinality before it is deployed and utilized.

As such, we consider this exhibit as a safely acknowledged usability enhancement.

UVO-02M: Insecure Typecasting Operation (TWAP)

Description:

The UniswapV3Oracle::_getQuote function will cast its configured twapWindow to an int32 data type which the uint32 data type may not fit into.

Impact:

An incorrectly configured twapWindow would result in an incorrect calculation of the arithmetic mean tick. To note, a severity of informational has been assigned as the likelihood of such an incident in a production environment is low due to the impossibly-high required number of observations necessary for the IUniswapV3Pool::observe lookup to succeed.

Example:

src/adapter/uniswap/UniswapV3Oracle.sol
44/// @notice Get a quote by calling the pool's TWAP oracle.
45/// @param inAmount The amount of `base` to convert.
46/// @param base The token that is being priced. Either `tokenA` or `tokenB`.
47/// @param quote The token that is the unit of account. Either `tokenB` or `tokenA`.
48/// @return The converted amount.
49function _getQuote(uint256 inAmount, address base, address quote) internal view override returns (uint256) {
50 if (!((base == tokenA && quote == tokenB) || (base == tokenB && quote == tokenA))) {
51 revert Errors.PriceOracle_NotSupported(base, quote);
52 }
53 // Size limitation enforced by the pool.
54 if (inAmount > type(uint128).max) revert Errors.PriceOracle_Overflow();
55
56 uint32[] memory secondsAgos = new uint32[](2);
57 secondsAgos[0] = twapWindow;
58
59 // Calculate the mean tick over the twap window.
60 (int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
61 int24 tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int32(twapWindow));
62 return OracleLibrary.getQuoteAtTick(tick, uint128(inAmount), base, quote);
63}

Recommendation:

We advise the UniswapV3Oracle::constructor to ensure that the _twapWindow being configured is less-than-or-equal-to the value of type(int32).max which is less than the maximum value of a uint32 variable.

Alleviation:

The UniswapV3Oracle::constructor was updated to further sanitize the _twapWindow as being a value that does not exceed the maximum representable by the int32 data type, effectively alleviating this exhibit.

UVO-03M: Insecure Calculation of Mean Tick

Description:

In the original Uniswap V3 implementation of the OracleLibrary::consult function, the delta between two cumulative tick values is permitted to underflow. Additionally, the median tick calculated should always round towards negative infinity as otherwise a rounding error may result in a rounding operation that would over-evaluate the tick and thus potential quote.

Impact:

Observations by the UniswapV3Oracle::_getQuote may fail to execute under extreme cumulative tick circumstances, and will incorrectly calculate the arithmetic mean tick when the delta between the cumulative values is negative.

Example:

src/adapter/uniswap/UniswapV3Oracle.sol
44/// @notice Get a quote by calling the pool's TWAP oracle.
45/// @param inAmount The amount of `base` to convert.
46/// @param base The token that is being priced. Either `tokenA` or `tokenB`.
47/// @param quote The token that is the unit of account. Either `tokenB` or `tokenA`.
48/// @return The converted amount.
49function _getQuote(uint256 inAmount, address base, address quote) internal view override returns (uint256) {
50 if (!((base == tokenA && quote == tokenB) || (base == tokenB && quote == tokenA))) {
51 revert Errors.PriceOracle_NotSupported(base, quote);
52 }
53 // Size limitation enforced by the pool.
54 if (inAmount > type(uint128).max) revert Errors.PriceOracle_Overflow();
55
56 uint32[] memory secondsAgos = new uint32[](2);
57 secondsAgos[0] = twapWindow;
58
59 // Calculate the mean tick over the twap window.
60 (int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
61 int24 tick = int24((tickCumulatives[1] - tickCumulatives[0]) / int32(twapWindow));
62 return OracleLibrary.getQuoteAtTick(tick, uint128(inAmount), base, quote);
63}

Recommendation:

We advise the code to be made functionally identical to its original Uniswap V3 counterpart, wrapping the subtraction in an unchecked code block and ensuring that the resulting tick rounds towards negative infinity by performing the conditional and subtraction referenced by this exhibit.

Alleviation:

The downward-rounding mechanism has been copied based on the OracleLibrary of Uniswap V3 as advised, however, the unchecked statement advised was not introduced as the official Uniswap V3 implementation deviates between its v0.8 and non-v0.8 variants.

We consider both the v0.8 and the non-v0.8 variants as correct with checked arithmetic providing additional peace of mind rendering this exhibit fully addressed.

UVO-04M: Potentially Insecure TWAP Window

Description:

The minimum TWAP window enforced by the UniswapV3Oracle is insecure for low liquidity pools and from a Proof-of-Stake perspective as it is likely for a single validator to generate all blocks within the minimum TWAP window.

Impact:

The present minimum TWAP window recommendation may be insufficient for certain use cases and under a Proof-of-Stake scheme.

Example:

src/adapter/uniswap/UniswapV3Oracle.sol
14/// @dev The minimum length of the TWAP window is 1 minute.
15uint32 internal constant MIN_TWAP_WINDOW = 60 seconds;

Recommendation:

We advise the minimum to be increased to a figure of 30 minutes, ensuring a sufficient order book depth is observed in the AMM pair and minimizing the risk of a PoS validator manipulating the transactions within the blocks they are mining to execute a TWAP manipulation attack with no downside.

Alleviation:

The MIN_TWAP_WINDOW was updated to 5 minutes and PoS related warnings were introduced to the contract's documentation, rendering this exhibit as addressed to the greatest extent it can be.