diff --git a/.gas-snapshot b/.gas-snapshot index 4f61770..cd436e6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,74 +1,76 @@ FlagsLibTest:testGetDecimalDifference() (gas: 3974) FlagsLibTest:testIsCompositeRoute() (gas: 4341) FlagsLibTest:testPackSimplePrice(int8,uint256) (runs: 256, μ: 7794, ~: 7555) -QueryProcessorTest:testFindNearestSample_CanFindExactValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 67643035, ~: 75383254) -QueryProcessorTest:testFindNearestSample_CanFindIntermediateValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 72673686, ~: 80564855) +QueryProcessorTest:testFindNearestSample_CanFindExactValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 67359104, ~: 75557360) +QueryProcessorTest:testFindNearestSample_CanFindIntermediateValue(uint32,uint256,uint256,uint256) (runs: 256, μ: 72740235, ~: 82229136) QueryProcessorTest:testFindNearestSample_NotInitialized() (gas: 8937393461068805977) -QueryProcessorTest:testFindNearestSample_OneSample(uint256) (runs: 256, μ: 80316, ~: 80360) +QueryProcessorTest:testFindNearestSample_OneSample(uint256) (runs: 256, μ: 80321, ~: 80360) QueryProcessorTest:testGetInstantValue() (gas: 124248) QueryProcessorTest:testGetInstantValue_NotInitialized(uint256) (runs: 256, μ: 19397, ~: 19397) -QueryProcessorTest:testGetInstantValue_NotInitialized_BeyondBufferSize(uint8,uint16) (runs: 256, μ: 68389660, ~: 68389600) -QueryProcessorTest:testGetPastAccumulator_BufferEmpty(uint8) (runs: 256, μ: 27027, ~: 27087) -QueryProcessorTest:testGetPastAccumulator_ExactMatch(uint32,uint256,uint256,uint16) (runs: 256, μ: 71794443, ~: 81233735) -QueryProcessorTest:testGetPastAccumulator_ExactMatch_LatestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 70068127, ~: 79224986) -QueryProcessorTest:testGetPastAccumulator_ExactMatch_OldestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 70098130, ~: 79256586) -QueryProcessorTest:testGetPastAccumulator_ExtrapolatesBeyondLatest(uint32,uint256,uint256,uint256) (runs: 256, μ: 72646619, ~: 80536404) -QueryProcessorTest:testGetPastAccumulator_InterpolatesBetweenPastAccumulators(uint32,uint256,uint256,uint256) (runs: 256, μ: 72681141, ~: 80570863) -QueryProcessorTest:testGetPastAccumulator_InvalidAgo(uint32,uint256,uint256,uint256) (runs: 256, μ: 72638228, ~: 80528115) -QueryProcessorTest:testGetPastAccumulator_QueryTooOld(uint32,uint256,uint256,uint256) (runs: 256, μ: 72649511, ~: 80537999) -QueryProcessorTest:testGetTimeWeightedAverage(uint32,uint256,uint256,uint256,uint256) (runs: 256, μ: 107655745, ~: 114208383) +QueryProcessorTest:testGetInstantValue_NotInitialized_BeyondBufferSize(uint8,uint16) (runs: 256, μ: 68389639, ~: 68389599) +QueryProcessorTest:testGetPastAccumulator_BufferEmpty(uint8) (runs: 256, μ: 27031, ~: 27087) +QueryProcessorTest:testGetPastAccumulator_ExactMatch(uint32,uint256,uint256,uint16) (runs: 256, μ: 68735833, ~: 78421559) +QueryProcessorTest:testGetPastAccumulator_ExactMatch_LatestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 67855253, ~: 76188526) +QueryProcessorTest:testGetPastAccumulator_ExactMatch_OldestAccumulator(uint32,uint256,uint256) (runs: 256, μ: 67885080, ~: 76220126) +QueryProcessorTest:testGetPastAccumulator_ExtrapolatesBeyondLatest(uint32,uint256,uint256,uint256) (runs: 256, μ: 72713324, ~: 82200036) +QueryProcessorTest:testGetPastAccumulator_InterpolatesBetweenPastAccumulators(uint32,uint256,uint256,uint256) (runs: 256, μ: 72747625, ~: 82235144) +QueryProcessorTest:testGetPastAccumulator_InvalidAgo(uint32,uint256,uint256,uint256) (runs: 256, μ: 72704951, ~: 82192090) +QueryProcessorTest:testGetPastAccumulator_QueryTooOld(uint32,uint256,uint256,uint256) (runs: 256, μ: 72716162, ~: 82201974) +QueryProcessorTest:testGetTimeWeightedAverage(uint32,uint256,uint256,uint256,uint256) (runs: 256, μ: 109440354, ~: 113540903) QueryProcessorTest:testGetTimeWeightedAverage_BadSecs() (gas: 10995) -ReservoirPriceOracleTest:testClearRoute() (gas: 52090) -ReservoirPriceOracleTest:testClearRoute_AllWordsCleared() (gas: 155074) -ReservoirPriceOracleTest:testDesignatePair() (gas: 29047) -ReservoirPriceOracleTest:testDesignatePair_IncorrectPair() (gas: 21089) -ReservoirPriceOracleTest:testDesignatePair_NotOwner() (gas: 17487) -ReservoirPriceOracleTest:testDesignatePair_TokenOrderReversed() (gas: 30619) -ReservoirPriceOracleTest:testGasBountyAvailable(uint256) (runs: 256, μ: 9927, ~: 9925) -ReservoirPriceOracleTest:testGasBountyAvailable_Zero() (gas: 8961) -ReservoirPriceOracleTest:testGetLargestSafeQueryWindow() (gas: 8390) -ReservoirPriceOracleTest:testGetLatest(uint32) (runs: 256, μ: 92769, ~: 92726) -ReservoirPriceOracleTest:testGetLatest_Inverted() (gas: 96846) -ReservoirPriceOracleTest:testGetPastAccumulators() (gas: 196354) -ReservoirPriceOracleTest:testGetPastAccumulators_Inverted() (gas: 156772) -ReservoirPriceOracleTest:testGetQuote(uint256,uint256) (runs: 256, μ: 35749, ~: 35862) -ReservoirPriceOracleTest:testGetQuote_AmountInTooLarge() (gas: 13002) -ReservoirPriceOracleTest:testGetQuote_ComplicatedDecimals() (gas: 10352998) -ReservoirPriceOracleTest:testGetQuote_Inverse(uint256,uint256) (runs: 256, μ: 37911, ~: 38085) -ReservoirPriceOracleTest:testGetQuote_MultipleHops() (gas: 114368) -ReservoirPriceOracleTest:testGetQuote_MultipleHops_Inverse() (gas: 114626) -ReservoirPriceOracleTest:testGetQuote_MultipleHops_PriceZero() (gas: 127226) -ReservoirPriceOracleTest:testGetQuote_NoPath() (gas: 11697) -ReservoirPriceOracleTest:testGetQuote_PriceZero() (gas: 16470) -ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_1HopRoute(uint256,uint256,address,address,uint8,uint8) (runs: 256, μ: 5327808, ~: 5327931) -ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_2HopRoute(uint256,uint256,uint256,address,address,address,uint8,uint8,uint8) (runs: 256, μ: 10493751, ~: 10493869) -ReservoirPriceOracleTest:testGetQuote_SameBaseQuote(uint256,address) (runs: 256, μ: 8987, ~: 8987) -ReservoirPriceOracleTest:testGetQuote_ZeroIn() (gas: 39282) -ReservoirPriceOracleTest:testGetQuotes(uint256,uint256) (runs: 256, μ: 33308, ~: 33421) -ReservoirPriceOracleTest:testGetTimeWeightedAverage() (gas: 141982) -ReservoirPriceOracleTest:testGetTimeWeightedAverage_Inverted() (gas: 121063) -ReservoirPriceOracleTest:testSetRoute() (gas: 58782) -ReservoirPriceOracleTest:testSetRoute_InvalidRoute() (gas: 17960) -ReservoirPriceOracleTest:testSetRoute_InvalidRouteLength() (gas: 17567) -ReservoirPriceOracleTest:testSetRoute_MultipleHops() (gas: 196025) -ReservoirPriceOracleTest:testSetRoute_NotSorted() (gas: 12073) -ReservoirPriceOracleTest:testSetRoute_OverwriteExisting() (gas: 162468) -ReservoirPriceOracleTest:testSetRoute_SameToken() (gas: 12026) -ReservoirPriceOracleTest:testUndesignatePair() (gas: 30263) -ReservoirPriceOracleTest:testUndesignatePair_NotOwner() (gas: 15354) -ReservoirPriceOracleTest:testUpdatePriceDeviationThreshold(uint256) (runs: 256, μ: 21397, ~: 21080) -ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold() (gas: 214272) -ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_InsufficientReward(uint256) (runs: 256, μ: 203473, ~: 203684) -ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_ZeroRecipient() (gas: 195975) -ReservoirPriceOracleTest:testUpdatePrice_FirstUpdate() (gas: 203451) -ReservoirPriceOracleTest:testUpdatePrice_IntermediateRoutes() (gas: 15868694) -ReservoirPriceOracleTest:testUpdatePrice_PriceOutOfRange() (gas: 5353128) -ReservoirPriceOracleTest:testUpdatePrice_WithinThreshold() (gas: 204527) -ReservoirPriceOracleTest:testUpdateRewardGasAmount() (gas: 19039) -ReservoirPriceOracleTest:testUpdateRewardGasAmount_NotOwner() (gas: 10984) -ReservoirPriceOracleTest:testUpdateTwapPeriod(uint256) (runs: 256, μ: 21663, ~: 21773) -ReservoirPriceOracleTest:testUpdateTwapPeriod_InvalidTwapPeriod(uint256) (runs: 256, μ: 17830, ~: 18120) -ReservoirPriceOracleTest:testWritePriceCache(uint256) (runs: 256, μ: 29932, ~: 29733) +ReservoirPriceOracleTest:testClearRoute() (gas: 52178) +ReservoirPriceOracleTest:testClearRoute_AllWordsCleared() (gas: 155206) +ReservoirPriceOracleTest:testDesignatePair() (gas: 29135) +ReservoirPriceOracleTest:testDesignatePair_IncorrectPair() (gas: 21200) +ReservoirPriceOracleTest:testDesignatePair_NotOwner() (gas: 17531) +ReservoirPriceOracleTest:testDesignatePair_TokenOrderReversed() (gas: 30796) +ReservoirPriceOracleTest:testGasBountyAvailable(uint256) (runs: 256, μ: 9883, ~: 9881) +ReservoirPriceOracleTest:testGasBountyAvailable_Zero() (gas: 8939) +ReservoirPriceOracleTest:testGetLargestSafeQueryWindow() (gas: 8412) +ReservoirPriceOracleTest:testGetLatest(uint32) (runs: 256, μ: 92794, ~: 92731) +ReservoirPriceOracleTest:testGetLatest_Inverted() (gas: 96786) +ReservoirPriceOracleTest:testGetPastAccumulators() (gas: 196383) +ReservoirPriceOracleTest:testGetPastAccumulators_Inverted() (gas: 156771) +ReservoirPriceOracleTest:testGetQuote(uint256,uint256) (runs: 256, μ: 35814, ~: 35927) +ReservoirPriceOracleTest:testGetQuote_AmountInTooLarge() (gas: 13030) +ReservoirPriceOracleTest:testGetQuote_ComplicatedDecimals() (gas: 10353281) +ReservoirPriceOracleTest:testGetQuote_Inverse(uint256,uint256) (runs: 256, μ: 37970, ~: 38150) +ReservoirPriceOracleTest:testGetQuote_MultipleHops() (gas: 114499) +ReservoirPriceOracleTest:testGetQuote_MultipleHops_Inverse() (gas: 114821) +ReservoirPriceOracleTest:testGetQuote_MultipleHops_PriceZero() (gas: 127407) +ReservoirPriceOracleTest:testGetQuote_NoFallbackOracle() (gas: 13914) +ReservoirPriceOracleTest:testGetQuote_PriceZero() (gas: 16564) +ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_1HopRoute(uint256,uint256,address,address,uint8,uint8) (runs: 256, μ: 5327965, ~: 5328073) +ReservoirPriceOracleTest:testGetQuote_RandomizeAllParam_2HopRoute(uint256,uint256,uint256,address,address,address,uint8,uint8,uint8) (runs: 256, μ: 10493988, ~: 10494081) +ReservoirPriceOracleTest:testGetQuote_SameBaseQuote(uint256,address) (runs: 256, μ: 9030, ~: 9030) +ReservoirPriceOracleTest:testGetQuote_UseFallback() (gas: 35311) +ReservoirPriceOracleTest:testGetQuote_ZeroIn() (gas: 39390) +ReservoirPriceOracleTest:testGetQuotes(uint256,uint256) (runs: 256, μ: 33347, ~: 33460) +ReservoirPriceOracleTest:testGetTimeWeightedAverage() (gas: 141958) +ReservoirPriceOracleTest:testGetTimeWeightedAverage_Inverted() (gas: 121129) +ReservoirPriceOracleTest:testSetFallbackOracle_NotOwner() (gas: 11003) +ReservoirPriceOracleTest:testSetRoute() (gas: 58848) +ReservoirPriceOracleTest:testSetRoute_InvalidRoute() (gas: 17982) +ReservoirPriceOracleTest:testSetRoute_InvalidRouteLength() (gas: 17611) +ReservoirPriceOracleTest:testSetRoute_MultipleHops() (gas: 196135) +ReservoirPriceOracleTest:testSetRoute_NotSorted() (gas: 12095) +ReservoirPriceOracleTest:testSetRoute_OverwriteExisting() (gas: 162578) +ReservoirPriceOracleTest:testSetRoute_SameToken() (gas: 12048) +ReservoirPriceOracleTest:testUndesignatePair() (gas: 30307) +ReservoirPriceOracleTest:testUndesignatePair_NotOwner() (gas: 15288) +ReservoirPriceOracleTest:testUpdatePriceDeviationThreshold(uint256) (runs: 256, μ: 21392, ~: 21107) +ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold() (gas: 216404) +ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_InsufficientReward(uint256) (runs: 256, μ: 205469, ~: 205672) +ReservoirPriceOracleTest:testUpdatePrice_BeyondThreshold_ZeroRecipient() (gas: 197963) +ReservoirPriceOracleTest:testUpdatePrice_FirstUpdate() (gas: 205527) +ReservoirPriceOracleTest:testUpdatePrice_IntermediateRoutes() (gas: 15870947) +ReservoirPriceOracleTest:testUpdatePrice_PriceOutOfRange() (gas: 5355182) +ReservoirPriceOracleTest:testUpdatePrice_WithinThreshold() (gas: 206559) +ReservoirPriceOracleTest:testUpdateRewardGasAmount() (gas: 19055) +ReservoirPriceOracleTest:testUpdateRewardGasAmount_NotOwner() (gas: 11006) +ReservoirPriceOracleTest:testUpdateTwapPeriod(uint256) (runs: 256, μ: 21704, ~: 21806) +ReservoirPriceOracleTest:testUpdateTwapPeriod_InvalidTwapPeriod(uint256) (runs: 256, μ: 17868, ~: 18164) +ReservoirPriceOracleTest:testWritePriceCache(uint256) (runs: 256, μ: 29978, ~: 29777) SamplesTest:testAccumulator() (gas: 3959) SamplesTest:testAccumulator_BadVariableRequest() (gas: 3523) SamplesTest:testInstant() (gas: 3909) diff --git a/src/ReservoirPriceOracle.sol b/src/ReservoirPriceOracle.sol index c965ca3..69f68e5 100644 --- a/src/ReservoirPriceOracle.sol +++ b/src/ReservoirPriceOracle.sol @@ -32,6 +32,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. /////////////////////////////////////////////////////////////////////////////////////////////// event DesignatePair(address token0, address token1, ReservoirPair pair); + event FallbackOracleSet(address fallbackOracle); event PriceDeviationThreshold(uint256 newThreshold); event RewardGasAmount(uint256 newAmount); event Route(address token0, address token1, address[] route); @@ -42,6 +43,10 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. // STORAGE // /////////////////////////////////////////////////////////////////////////////////////////////// + /// @notice The PriceOracle to call if this router is not configured for base/quote. + /// @dev If `address(0)` then there is no fallback. + address public fallbackOracle; + /// @notice percentage change greater than which, a price update may result in a reward payout of native tokens, /// subject to availability of rewards. /// 1e18 == 100% @@ -83,7 +88,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. /// @inheritdoc IPriceOracle function getQuote(uint256 aAmount, address aBase, address aQuote) external view returns (uint256 rOut) { - rOut = _getQuote(aAmount, aBase, aQuote); + (rOut,) = _getQuotes(aAmount, aBase, aQuote, false); } /// @inheritdoc IPriceOracle @@ -92,8 +97,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. view returns (uint256 rBidOut, uint256 rAskOut) { - uint256 lResult = _getQuote(aAmount, aBase, aQuote); - (rBidOut, rAskOut) = (lResult, lResult); + (rBidOut, rAskOut) = _getQuotes(aAmount, aBase, aQuote, true); } // price update related functions @@ -370,19 +374,28 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. } } - function _getQuote(uint256 aAmount, address aBase, address aQuote) internal view returns (uint256 rOut) { - if (aBase == aQuote) return aAmount; + function _getQuotes(uint256 aAmount, address aBase, address aQuote, bool isGetQuotes) + internal + view + returns (uint256 rBidOut, uint256 rAskOut) + { + if (aBase == aQuote) return (aAmount, aAmount); if (aAmount > Constants.MAX_AMOUNT_IN) revert OracleErrors.AmountInTooLarge(); (address lToken0, address lToken1) = aBase.sortTokens(aQuote); (address[] memory lRoute, int256 lDecimalDiff, uint256 lPrice) = _getRouteDecimalDifferencePrice(lToken0, lToken1); + // route does not exist on our oracle, attempt querying the fallback if (lRoute.length == 0) { - revert OracleErrors.NoPath(); + if (fallbackOracle == address(0)) revert OracleErrors.NoPath(); + + // We do not catch errors here so the fallback oracle will revert if it doesn't support the query. + if (isGetQuotes) (rBidOut, rAskOut) = IPriceOracle(fallbackOracle).getQuotes(aAmount, aBase, aQuote); + else rBidOut = rAskOut = IPriceOracle(fallbackOracle).getQuote(aAmount, aBase, aQuote); } else if (lRoute.length == 2) { if (lPrice == 0) revert OracleErrors.PriceZero(); - rOut = _calcAmtOut(aAmount, lPrice, lDecimalDiff, lRoute[0] != aBase); + rBidOut = rAskOut = _calcAmtOut(aAmount, lPrice, lDecimalDiff, lRoute[0] != aBase); } // for composite route, read simple prices to derive composite price else { @@ -400,7 +413,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. if (lPrice == 0) revert OracleErrors.PriceZero(); lIntermediateAmount = _calcAmtOut(lIntermediateAmount, lPrice, lDecimalDiff, lRoute[i] != lLowerToken); } - rOut = lIntermediateAmount; + rBidOut = rAskOut = lIntermediateAmount; } } @@ -437,6 +450,11 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. // ADMIN FUNCTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// + function setFallbackOracle(address aFallbackOracle) public onlyOwner { + fallbackOracle = aFallbackOracle; + emit FallbackOracleSet(aFallbackOracle); + } + function updatePriceDeviationThreshold(uint64 aNewThreshold) public onlyOwner { if (aNewThreshold > Constants.MAX_DEVIATION_THRESHOLD) { revert OracleErrors.PriceDeviationThresholdTooHigh(); diff --git a/test/mock/MockFallbackOracle.sol b/test/mock/MockFallbackOracle.sol new file mode 100644 index 0000000..e75bdf3 --- /dev/null +++ b/test/mock/MockFallbackOracle.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IPriceOracle } from "src/interfaces/IPriceOracle.sol"; + +contract MockFallbackOracle is IPriceOracle { + function name() external view returns (string memory) { + return "MOCK"; + } + + function getQuote(uint256 amount, address, address) external view returns (uint256 out) { + out = amount; + } + + function getQuotes(uint256 amount, address, address) + external + view + returns (uint256 bidOut, uint256 askOut) + { + (bidOut, askOut) = (amount, amount); + } +} diff --git a/test/unit/ReservoirPriceOracle.t.sol b/test/unit/ReservoirPriceOracle.t.sol index 703fadc..2ab7422 100644 --- a/test/unit/ReservoirPriceOracle.t.sol +++ b/test/unit/ReservoirPriceOracle.t.sol @@ -19,6 +19,7 @@ import { import { Bytes32Lib } from "amm-core/libraries/Bytes32.sol"; import { EnumerableSetLib } from "lib/solady/src/utils/EnumerableSetLib.sol"; import { Constants } from "src/libraries/Constants.sol"; +import { MockFallbackOracle } from "test/mock/MockFallbackOracle.sol"; contract ReservoirPriceOracleTest is BaseTest { using Utils for *; @@ -33,10 +34,12 @@ contract ReservoirPriceOracleTest is BaseTest { uint256 private constant WAD = 1e18; + address internal constant ADDRESS_THRESHOLD = address(0x1000); + // to keep track of addresses to ensure no clash for fuzz tests EnumerableSetLib.AddressSet internal _addressSet; - address internal constant ADDRESS_THRESHOLD = address(0x1000); + MockFallbackOracle internal _fallbackOracle = new MockFallbackOracle(); // writes the cached prices, for easy testing function _writePriceCache(address aToken0, address aToken1, uint256 aPrice) internal { @@ -467,6 +470,20 @@ contract ReservoirPriceOracleTest is BaseTest { assertEq(lAmtOut, aAmtIn); } + function testGetQuote_UseFallback() external { + // arrange + _oracle.setFallbackOracle(address(_fallbackOracle)); + + // act + uint256 lAmountOut = _oracle.getQuote(1, address(_tokenC), address(_tokenD)); + (uint256 lBidOut, uint256 lAskOut) = _oracle.getQuotes(1, address(_tokenC), address(_tokenD)); + + // assert + assertGt(lAmountOut, 0); + assertGt(lBidOut, 0); + assertGt(lAskOut, 0); + } + function testUpdatePriceDeviationThreshold(uint256 aNewThreshold) external { // assume uint64 lNewThreshold = uint64(bound(aNewThreshold, 0, 0.1e18)); @@ -961,6 +978,12 @@ contract ReservoirPriceOracleTest is BaseTest { _oracle.getTimeWeightedAverage(lQueries); } + function testSetFallbackOracle_NotOwner() external { + vm.prank(address(123)); + vm.expectRevert("UNAUTHORIZED"); + _oracle.setFallbackOracle(address(456)); + } + function testDesignatePair_IncorrectPair() external { // act & assert vm.expectRevert(); @@ -1097,7 +1120,7 @@ contract ReservoirPriceOracleTest is BaseTest { _oracle.updateRewardGasAmount(111); } - function testGetQuote_NoPath() external { + function testGetQuote_NoFallbackOracle() external { // act & assert vm.expectRevert(OracleErrors.NoPath.selector); _oracle.getQuote(123, address(123), address(456));