From ebe983e4ff076c91cfe08c2570ce01e4b7aabf2c Mon Sep 17 00:00:00 2001 From: "A.L." Date: Sun, 9 Jun 2024 20:06:29 +0200 Subject: [PATCH] feat: resolve base asset --- src/ReservoirPriceOracle.sol | 31 ++++++++++++++++++++++----- test/mock/StubERC4626.sol | 32 ++++++++++++++++++++++++++++ test/unit/ReservoirPriceOracle.t.sol | 18 ++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 test/mock/StubERC4626.sol diff --git a/src/ReservoirPriceOracle.sol b/src/ReservoirPriceOracle.sol index 414a8f3..c1c05e0 100644 --- a/src/ReservoirPriceOracle.sol +++ b/src/ReservoirPriceOracle.sol @@ -35,6 +35,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 ResolvedVaultSet(address vault, address asset); event RewardGasAmount(uint256 newAmount); event Route(address token0, address token1, address[] route); event Price(address token0, address token1, uint256 price); @@ -69,6 +70,9 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. /// @notice Designated pairs to serve as price feed for a certain token0 and token1 mapping(address token0 => mapping(address token1 => ReservoirPair pair)) public pairs; + /// @notice ERC4626 vaults resolved using internal pricing (`convertToAssets`). + mapping(address vault => address asset) public resolvedVaults; + /////////////////////////////////////////////////////////////////////////////////////////////// // CONSTRUCTOR, FALLBACKS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -382,7 +386,7 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. } } - function _getQuotes(uint256 aAmount, address aBase, address aQuote, bool isGetQuotes) + function _getQuotes(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes) internal view returns (uint256 rBidOut, uint256 rAskOut) @@ -396,11 +400,14 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. // route does not exist on our oracle, attempt querying the fallback if (lRoute.length == 0) { - if (fallbackOracle == address(0)) revert OracleErrors.NoPath(); + address lBaseAsset = resolvedVaults[aBase]; + + if (lBaseAsset != address(0)) { + uint256 lResolvedAmountIn = IERC4626(aBase).convertToAssets(aAmount); + return _getQuotes(lResolvedAmountIn, lBaseAsset, aQuote, aIsGetQuotes); + } - // 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); + return _useFallbackOracle(aAmount, aBase, aQuote, aIsGetQuotes); } else if (lRoute.length == 2) { if (lPrice == 0) revert OracleErrors.PriceZero(); rBidOut = rAskOut = _calcAmtOut(aAmount, lPrice, lDecimalDiff, lRoute[0] != aBase); @@ -454,6 +461,14 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. } } + function _useFallbackOracle(uint256 aAmount, address aBase, address aQuote, bool aIsGetQuotes) internal view returns (uint256 rBidOut, uint256 rAskOut) { + 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 (aIsGetQuotes) (rBidOut, rAskOut) = IPriceOracle(fallbackOracle).getQuotes(aAmount, aBase, aQuote); + else rBidOut = rAskOut = IPriceOracle(fallbackOracle).getQuote(aAmount, aBase, aQuote); + } + /////////////////////////////////////////////////////////////////////////////////////////////// // ADMIN FUNCTIONS // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -506,6 +521,12 @@ contract ReservoirPriceOracle is IPriceOracle, IReservoirPriceOracle, Owned(msg. emit SetPriceType(aType); } + function setResolvedVault(address aVault, bool aSet) external onlyOwner { + address lAsset = aSet ? IERC4626(aVault).asset() : address(0); + resolvedVaults[aVault] = lAsset; + emit ResolvedVaultSet(aVault, lAsset); + } + /// @notice Sets the price route between aToken0 and aToken1, and also intermediate routes if previously undefined /// @param aToken0 Address of the lower token /// @param aToken1 Address of the higher token diff --git a/test/mock/StubERC4626.sol b/test/mock/StubERC4626.sol new file mode 100644 index 0000000..3a200b4 --- /dev/null +++ b/test/mock/StubERC4626.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +contract StubERC4626 { + address public asset; + uint256 private rate; + string revertMsg = "oops"; + bool doRevert; + + constructor(address _asset, uint256 _rate) { + asset = _asset; + rate = _rate; + } + + function setRevert(bool _doRevert) external { + doRevert = _doRevert; + } + + function setRate(uint256 _rate) external { + rate = _rate; + } + + function convertToAssets(uint256 shares) external view returns (uint256) { + if (doRevert) revert(revertMsg); + return shares * rate / 1e18; + } + + function convertToShares(uint256 assets) external view returns (uint256) { + if (doRevert) revert(revertMsg); + return assets * 1e18 / rate; + } +} diff --git a/test/unit/ReservoirPriceOracle.t.sol b/test/unit/ReservoirPriceOracle.t.sol index 4e20f79..fbfd4e3 100644 --- a/test/unit/ReservoirPriceOracle.t.sol +++ b/test/unit/ReservoirPriceOracle.t.sol @@ -20,6 +20,7 @@ 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"; +import { StubERC4626 } from "test/mock/StubERC4626.sol"; contract ReservoirPriceOracleTest is BaseTest { using Utils for *; @@ -486,6 +487,23 @@ contract ReservoirPriceOracleTest is BaseTest { assertGt(lAskOut, 0); } + function testGetQuote_BaseIsVault(uint256 aRate) external { + // assume + uint256 lRate = bound(aRate, 1, 1e36); + + // arrange + uint256 lAmtIn = 5e18; + StubERC4626 lVault = new StubERC4626(address(_tokenA), lRate); + _oracle.setResolvedVault(address(lVault), true); + _writePriceCache(address(_tokenA), address(_tokenB), 1e18); + + // act + uint256 lAmtOut = _oracle.getQuote(lAmtIn, address(lVault), address(_tokenB)); + + // assert + assertEq(lAmtOut / 1e12, lAmtIn * lRate / 1e18); + } + function testUpdatePriceDeviationThreshold(uint256 aNewThreshold) external { // assume uint64 lNewThreshold = uint64(bound(aNewThreshold, 0, 0.1e18));