diff --git a/src/adapter/erc4626/ERC4626Oracle.sol b/src/adapter/erc4626/ERC4626Oracle.sol new file mode 100644 index 00000000..7dc40eec --- /dev/null +++ b/src/adapter/erc4626/ERC4626Oracle.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {IERC4626} from "forge-std/interfaces/IERC4626.sol"; +import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol"; + +/// @title ERC4626Oracle +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice PriceOracle adapter for ERC4626 vaults. +/// @dev Warning: This adapter may not be suitable for all ERC4626 vaults. +/// By ERC4626 spec `convert*` ignores liquidity restrictions, fees, slippage and per-user restrictions. +/// Therefore the reported price may not be realizable through `redeem` or `withdraw`. +/// @dev Warning: Exercise caution when using this pricing method for borrowable vaults. +/// Ensure that the price cannot be atomically manipulated by a donation attack. +contract ERC4626Oracle is BaseAdapter { + /// @inheritdoc IPriceOracle + string public constant name = "ERC4626Oracle"; + /// @notice The address of the vault. + address public immutable base; + /// @notice The address of the vault's underlying asset. + address public immutable quote; + + /// @notice Deploy an ERC4626Oracle. + /// @param _vault The address of the ERC4626 vault to price. + constructor(address _vault) { + base = _vault; + quote = IERC4626(_vault).asset(); + } + + /// @notice Get the quote from the ERC4626 vault. + /// @param inAmount The amount of `base` to convert. + /// @param _base The token that is being priced. + /// @param _quote The token that is the unit of account. + /// @return The converted amount using the ERC4626 vault. + function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) { + if (_base == base && _quote == quote) { + return IERC4626(base).convertToAssets(inAmount); + } else if (_base == quote && _quote == base) { + return IERC4626(base).convertToShares(inAmount); + } + + revert Errors.PriceOracle_NotSupported(_base, _quote); + } +} diff --git a/src/component/ExchangeRateSentinel.sol b/src/component/ExchangeRateSentinel.sol new file mode 100644 index 00000000..b98a9ca3 --- /dev/null +++ b/src/component/ExchangeRateSentinel.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BaseAdapter, Errors, IPriceOracle} from "../adapter/BaseAdapter.sol"; +import {ScaleUtils, Scale} from "../lib/ScaleUtils.sol"; + +/// @title ExchangeRateSentinel +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice The sentinel is used to clamp the exchange rate and constrain its growth. +/// @dev If out of bounds the rate is saturated (clamped) to the boundary. +contract ExchangeRateSentinel is BaseAdapter { + /// @inheritdoc IPriceOracle + string public constant name = "ExchangeRateSentinel"; + /// @notice The address of the underlying oracle. + address public immutable oracle; + /// @notice The address of the base asset corresponding to the oracle. + address public immutable base; + /// @notice The address of the quote asset corresponding to the oracle. + address public immutable quote; + /// @notice The lower bound for the unit exchange rate of base/quote. + /// @dev Below this value the exchange rate is saturated (returns the floor). + uint256 public immutable floorRate; + /// @notice The upper bound for the unit exchange rate of base/quote. + /// @dev Above this value the exchange rate is saturated (returns the ceil). + uint256 public immutable ceilRate; + /// @notice The maximum per-second growth of the exchange rate. + /// @dev Relative to the snapshotted rate at deployment. + /// If the value is `type(uint256).max` then growth bounds are disabled. + uint256 public immutable maxRateGrowth; + /// @notice The unit exchange rate of base/quote taken at deployment. + uint256 public immutable snapshotRate; + /// @notice The timestamp of the exchange rate snapshot. + uint256 public immutable snapshotAt; + /// @notice The scale factors used for decimal conversions. + Scale internal immutable scale; + + /// @notice Deploy an ExchangeRateSentinel. + /// @param _oracle The address of the underlying oracle. + /// @param _base The address of the base asset corresponding to the oracle. + /// @param _quote The address of the quote asset corresponding to the oracle. + /// @param _floorRate The minimum unit exchange rate of base/quote. + /// @param _ceilRate The maximum unit exchange rate of base/quote. + /// @param _maxRateGrowth The maximum per-second growth of the exchange rate. + /// @dev To use absolute bounds only, set `_maxRateGrowth` to `type(uint256).max`. + /// To use growth bounds only, set `_floorRate` to 0 and `_ceilRate` to `type(uint256).max`. + constructor( + address _oracle, + address _base, + address _quote, + uint256 _floorRate, + uint256 _ceilRate, + uint256 _maxRateGrowth + ) { + if (_floorRate > _ceilRate) revert Errors.PriceOracle_InvalidConfiguration(); + oracle = _oracle; + base = _base; + quote = _quote; + floorRate = _floorRate; + ceilRate = _ceilRate; + maxRateGrowth = _maxRateGrowth; + + uint8 baseDecimals = _getDecimals(base); + uint8 quoteDecimals = _getDecimals(quote); + + // Snapshot the unit exchange rate at deployment. + snapshotRate = IPriceOracle(oracle).getQuote(10 ** baseDecimals, base, quote); + snapshotAt = block.timestamp; + scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, quoteDecimals); + } + + /// @notice Get the upper bound of the unit exchange rate of base/quote. + /// @dev This value is either bound by `maxRate` or `maxRateGrowth`. + /// @return The current maximum exchange rate. + function maxRate() external view returns (uint256) { + return _maxRateAt(block.timestamp); + } + + /// @notice Get the upper bound of the unit exchange rate of base/quote at a timestamp. + /// @param timestamp The timestamp to use. Must not be earlier than `snapshotAt`. + /// @return The maximum unit exchange rate of base/quote at the given timestamp. + function _maxRateAt(uint256 timestamp) internal view returns (uint256) { + // If growth bounds are disabled then only the absolute bounds apply. + if (maxRateGrowth == type(uint256).max) return ceilRate; + // Protect against inconsistent timing on non-standard EVMs. + if (timestamp < snapshotAt) revert Errors.PriceOracle_InvalidAnswer(); + // Return the smaller of the absolute bound and the growth bound. + uint256 secondsElapsed = timestamp - snapshotAt; + uint256 max = snapshotRate + maxRateGrowth * secondsElapsed; + return max < ceilRate ? max : ceilRate; + } + + /// @notice Get the quote from the wrapped oracle and bound it to the range. + /// @param inAmount The amount of `base` to convert. + /// @param _base The token that is being priced. + /// @param _quote The token that is the unit of account. + /// @return The converted amount using the wrapped oracle, bounded to the range. + function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) { + bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote); + + uint256 outAmount = IPriceOracle(oracle).getQuote(inAmount, _base, _quote); + uint256 minAmount = ScaleUtils.calcOutAmount(inAmount, floorRate, scale, inverse); + uint256 maxAmount = ScaleUtils.calcOutAmount(inAmount, _maxRateAt(block.timestamp), scale, inverse); + + // If inverse route then flip the limits because they are specified per unit base/quote by convention. + (minAmount, maxAmount) = inverse ? (maxAmount, minAmount) : (minAmount, maxAmount); + if (outAmount < minAmount) return minAmount; + if (outAmount > maxAmount) return maxAmount; + return outAmount; + } +} diff --git a/test/StubERC4626.sol b/test/StubERC4626.sol index 3a200b4b..8bccc544 100644 --- a/test/StubERC4626.sol +++ b/test/StubERC4626.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; contract StubERC4626 { - address public asset; + address public immutable asset; uint256 private rate; string revertMsg = "oops"; bool doRevert; diff --git a/test/adapter/erc4626/ERC4626Oracle.fork.t.sol b/test/adapter/erc4626/ERC4626Oracle.fork.t.sol new file mode 100644 index 00000000..dd108d3b --- /dev/null +++ b/test/adapter/erc4626/ERC4626Oracle.fork.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {SDAI, DAI, USDM, WUSDM, USDL, WUSDL} from "test/utils/EthereumAddresses.sol"; +import {ForkTest} from "test/utils/ForkTest.sol"; +import {ERC4626Oracle} from "src/adapter/erc4626/ERC4626Oracle.sol"; +import {Errors} from "src/lib/Errors.sol"; + +contract ERC4626OracleForkTest is ForkTest { + uint256 constant ABS_PRECISION = 1; + uint256 constant REL_PRECISION = 0.000001e18; + + function setUp() public { + _setUpFork(); + vm.rollFork(21967153); + } + + function test_Constructor_Integrity() public { + ERC4626Oracle oracle = new ERC4626Oracle(SDAI); + assertEq(oracle.base(), SDAI); + assertEq(oracle.quote(), DAI); + } + + function test_GetQuote_SDAI() public { + uint256 rate = 1.1493126e18; + ERC4626Oracle oracle = new ERC4626Oracle(SDAI); + + uint256 outAmount = oracle.getQuote(1e18, SDAI, DAI); + uint256 outAmount1000 = oracle.getQuote(1000e18, SDAI, DAI); + assertApproxEqRel(outAmount, rate, REL_PRECISION); + assertApproxEqRel(outAmount1000, rate * 1000, REL_PRECISION); + + uint256 outAmountInv = oracle.getQuote(outAmount, DAI, SDAI); + assertApproxEqAbs(outAmountInv, 1e18, ABS_PRECISION); + uint256 outAmountInv1000 = oracle.getQuote(outAmount1000, DAI, SDAI); + assertApproxEqAbs(outAmountInv1000, 1000e18, ABS_PRECISION); + } + + function test_GetQuote_WUSDM() public { + uint256 rate = 1.070076852246772245e18; + ERC4626Oracle oracle = new ERC4626Oracle(WUSDM); + + uint256 outAmount = oracle.getQuote(1e18, WUSDM, USDM); + uint256 outAmount1000 = oracle.getQuote(1000e18, WUSDM, USDM); + assertApproxEqRel(outAmount, rate, REL_PRECISION); + assertApproxEqRel(outAmount1000, rate * 1000, REL_PRECISION); + + uint256 outAmountInv = oracle.getQuote(outAmount, USDM, WUSDM); + assertApproxEqAbs(outAmountInv, 1e18, ABS_PRECISION); + uint256 outAmountInv1000 = oracle.getQuote(outAmount1000, USDM, WUSDM); + assertApproxEqAbs(outAmountInv1000, 1000e18, ABS_PRECISION); + } + + function test_GetQuote_WUSDL() public { + uint256 rate = 1.01639231015737408e18; + ERC4626Oracle oracle = new ERC4626Oracle(WUSDL); + + uint256 outAmount = oracle.getQuote(1e18, WUSDL, USDL); + uint256 outAmount1000 = oracle.getQuote(1000e18, WUSDL, USDL); + assertApproxEqRel(outAmount, rate, REL_PRECISION); + assertApproxEqRel(outAmount1000, rate * 1000, REL_PRECISION); + + uint256 outAmountInv = oracle.getQuote(outAmount, USDL, WUSDL); + assertApproxEqAbs(outAmountInv, 1e18, ABS_PRECISION); + uint256 outAmountInv1000 = oracle.getQuote(outAmount1000, USDL, WUSDL); + assertApproxEqAbs(outAmountInv1000, 1000e18, ABS_PRECISION); + } +} diff --git a/test/adapter/erc4626/ERC4626Oracle.prop.t.sol b/test/adapter/erc4626/ERC4626Oracle.prop.t.sol new file mode 100644 index 00000000..dbee99ce --- /dev/null +++ b/test/adapter/erc4626/ERC4626Oracle.prop.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {AdapterPropTest} from "test/adapter/AdapterPropTest.sol"; +import {ERC4626OracleHelper} from "test/adapter/erc4626/ERC4626OracleHelper.sol"; + +contract ERC4626OraclePropTest is ERC4626OracleHelper, AdapterPropTest { + function testProp_Bidirectional(FuzzableState memory s, Prop_Bidirectional memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_NoOtherPaths(FuzzableState memory s, Prop_NoOtherPaths memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_IdempotentQuoteAndQuotes(FuzzableState memory s, Prop_IdempotentQuoteAndQuotes memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_SupportsZero(FuzzableState memory s, Prop_SupportsZero memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_ContinuousDomain(FuzzableState memory s, Prop_ContinuousDomain memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_OutAmountIncreasing(FuzzableState memory s, Prop_OutAmountIncreasing memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function setUpPropTest(FuzzableState memory s) internal { + setUpState(s); + adapter = address(oracle); + base = s.base; + quote = s.quote; + } +} diff --git a/test/adapter/erc4626/ERC4626Oracle.unit.t.sol b/test/adapter/erc4626/ERC4626Oracle.unit.t.sol new file mode 100644 index 00000000..4e995e27 --- /dev/null +++ b/test/adapter/erc4626/ERC4626Oracle.unit.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {ERC4626OracleHelper} from "test/adapter/erc4626/ERC4626OracleHelper.sol"; +import {boundAddr} from "test/utils/TestUtils.sol"; +import {ERC4626Oracle} from "src/adapter/erc4626/ERC4626Oracle.sol"; +import {Errors} from "src/lib/Errors.sol"; + +contract ERC4626OracleTest is ERC4626OracleHelper { + function test_Constructor_Integrity(FuzzableState memory s) public { + setUpState(s); + assertEq(ERC4626Oracle(oracle).name(), "ERC4626Oracle"); + assertEq(ERC4626Oracle(oracle).base(), s.base); + assertEq(ERC4626Oracle(oracle).quote(), s.quote); + } + + function test_Quote_RevertsWhen_InvalidTokens(FuzzableState memory s, address otherA, address otherB) public { + setUpState(s); + otherA = boundAddr(otherA); + otherB = boundAddr(otherB); + vm.assume(otherA != s.base && otherA != s.quote); + vm.assume(otherB != s.base && otherB != s.quote); + expectNotSupported(s.inAmount, s.base, s.base); + expectNotSupported(s.inAmount, s.quote, s.quote); + expectNotSupported(s.inAmount, s.base, otherA); + expectNotSupported(s.inAmount, otherA, s.base); + expectNotSupported(s.inAmount, s.quote, otherA); + expectNotSupported(s.inAmount, otherA, s.quote); + expectNotSupported(s.inAmount, otherA, otherA); + expectNotSupported(s.inAmount, otherA, otherB); + } + + function test_Quote_Integrity(FuzzableState memory s) public { + setUpState(s); + ERC4626Oracle(oracle).getQuote(s.inAmount, s.base, s.quote); + ERC4626Oracle(oracle).getQuote(s.inAmount, s.quote, s.base); + } + + function test_Quotes_Integrity(FuzzableState memory s) public { + setUpState(s); + uint256 outAmount = ERC4626Oracle(oracle).getQuote(s.inAmount, s.base, s.quote); + (uint256 bidOutAmount, uint256 askOutAmount) = ERC4626Oracle(oracle).getQuotes(s.inAmount, s.base, s.quote); + assertEq(bidOutAmount, outAmount); + assertEq(askOutAmount, outAmount); + uint256 outAmountInv = ERC4626Oracle(oracle).getQuote(s.inAmount, s.quote, s.base); + (uint256 bidOutAmountInv, uint256 askOutAmountInv) = + ERC4626Oracle(oracle).getQuotes(s.inAmount, s.quote, s.base); + assertEq(bidOutAmountInv, outAmountInv); + assertEq(askOutAmountInv, outAmountInv); + } +} diff --git a/test/adapter/erc4626/ERC4626OracleHelper.sol b/test/adapter/erc4626/ERC4626OracleHelper.sol new file mode 100644 index 00000000..9e00d563 --- /dev/null +++ b/test/adapter/erc4626/ERC4626OracleHelper.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {IERC4626} from "forge-std/interfaces/IERC4626.sol"; +import {AdapterHelper} from "test/adapter/AdapterHelper.sol"; +import {boundAddr} from "test/utils/TestUtils.sol"; +import {StubERC4626} from "test/StubERC4626.sol"; +import {ERC4626Oracle} from "src/adapter/erc4626/ERC4626Oracle.sol"; + +contract ERC4626OracleHelper is AdapterHelper { + struct FuzzableState { + // Config + address base; + address quote; + // Oracle State + uint256 rate; + // Environment + uint256 inAmount; + } + + function setUpState(FuzzableState memory s) internal { + s.base = boundAddr(s.base); + s.quote = boundAddr(s.quote); + + vm.assume(s.base != s.quote); + + s.rate = bound(s.rate, 1e9, 1e27); + + vm.etch(s.base, address(new StubERC4626(s.quote, 0)).code); + + StubERC4626(s.base).setRate(s.rate); + StubERC4626(s.base).setRevert(behaviors[Behavior.FeedReverts]); + + oracle = address(new ERC4626Oracle(s.base)); + s.inAmount = bound(s.inAmount, 0, type(uint128).max); + } +} diff --git a/test/component/ExchangeRateSentinel.fork.t.sol b/test/component/ExchangeRateSentinel.fork.t.sol new file mode 100644 index 00000000..30439003 --- /dev/null +++ b/test/component/ExchangeRateSentinel.fork.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.23; + +import {BALANCER_RETH_RATE_PROVIDER, BALANCER_WEETH_RATE_PROVIDER} from "test/adapter/rate/RateProviderAddresses.sol"; +import {RETH, WEETH, WETH, WSTETH} from "test/utils/EthereumAddresses.sol"; +import {ForkTest} from "test/utils/ForkTest.sol"; +import {LidoFundamentalOracle} from "src/adapter/lido/LidoFundamentalOracle.sol"; +import {RateProviderOracle} from "src/adapter/rate/RateProviderOracle.sol"; +import {ExchangeRateSentinel} from "src/component/ExchangeRateSentinel.sol"; + +contract ExchangeRateSentinelForkTest is ForkTest { + function setUp() public { + _setUpFork(20893573); + } + + function test_wstETH() public { + vm.rollFork(12000000); + LidoFundamentalOracle adapter = new LidoFundamentalOracle(); + uint256 maxRateGrowth = uint256(0.08e18) / 365 days; + ExchangeRateSentinel sentinel = + new ExchangeRateSentinel(address(adapter), WSTETH, WETH, 0.9e18, 1.5e18, maxRateGrowth); + + vm.rollFork(20893573); + uint256 adapterOutAmount = adapter.getQuote(1e18, WSTETH, WETH); + uint256 sentinelOutAmount = sentinel.getQuote(1e18, WSTETH, WETH); + assertEq(sentinelOutAmount, adapterOutAmount); + } + + function test_rETH() public { + vm.rollFork(13846103); + RateProviderOracle adapter = new RateProviderOracle(RETH, WETH, BALANCER_RETH_RATE_PROVIDER); + uint256 maxRateGrowth = uint256(0.08e18) / 365 days; + ExchangeRateSentinel sentinel = + new ExchangeRateSentinel(address(adapter), RETH, WETH, 0.9e18, 1.5e18, maxRateGrowth); + + vm.rollFork(20893573); + uint256 adapterOutAmount = adapter.getQuote(1e18, RETH, WETH); + uint256 sentinelOutAmount = sentinel.getQuote(1e18, RETH, WETH); + assertEq(sentinelOutAmount, adapterOutAmount); + } + + function test_weETH() public { + vm.rollFork(18550000); + RateProviderOracle adapter = new RateProviderOracle(WEETH, WETH, BALANCER_WEETH_RATE_PROVIDER); + uint256 maxRateGrowth = uint256(0.08e18) / 365 days; + ExchangeRateSentinel sentinel = + new ExchangeRateSentinel(address(adapter), WEETH, WETH, 0.9e18, 1.5e18, maxRateGrowth); + + vm.rollFork(20893573); + uint256 adapterOutAmount = adapter.getQuote(1e18, WEETH, WETH); + uint256 sentinelOutAmount = sentinel.getQuote(1e18, WEETH, WETH); + assertEq(sentinelOutAmount, adapterOutAmount); + } +} diff --git a/test/component/ExchangeRateSentinel.t.sol b/test/component/ExchangeRateSentinel.t.sol new file mode 100644 index 00000000..c2e130a0 --- /dev/null +++ b/test/component/ExchangeRateSentinel.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.23; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {StubPriceOracle} from "test/adapter/StubPriceOracle.sol"; +import {ExchangeRateSentinel} from "src/component/ExchangeRateSentinel.sol"; + +contract ExchangeRateSentinelTest is Test { + address base = makeAddr("BASE"); + address quote = makeAddr("QUOTE"); + uint256 INITIAL_RATE = 2e18; + uint256 MAX_RATE_GROWTH = 0.1e18; + uint256 FLOOR_RATE = 1e18; + uint256 CEIL_RATE = 5e18; + StubPriceOracle wrappedAdapter; + ExchangeRateSentinel sentinel; + + function setUp() public { + vm.warp(0); + wrappedAdapter = new StubPriceOracle(); + vm.mockCall(base, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(18)); + vm.mockCall(quote, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(18)); + setPrice(INITIAL_RATE); + sentinel = + new ExchangeRateSentinel(address(wrappedAdapter), base, quote, FLOOR_RATE, CEIL_RATE, MAX_RATE_GROWTH); + } + + function test_Quote_GrowthBoundsDisabled() public { + sentinel = + new ExchangeRateSentinel(address(wrappedAdapter), base, quote, FLOOR_RATE, CEIL_RATE, type(uint256).max); + + vm.warp(10); + setPrice(CEIL_RATE); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + assertEq(sentinelOutAmount, CEIL_RATE); + assertEq(adapterOutAmount, CEIL_RATE); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / CEIL_RATE); + assertEq(adapterOutAmount, 1e36 / CEIL_RATE); + } + + function test_Quote_AtInitial() public { + vm.warp(10); + setPrice(INITIAL_RATE); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + assertEq(sentinelOutAmount, INITIAL_RATE); + assertEq(adapterOutAmount, INITIAL_RATE); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / INITIAL_RATE); + assertEq(adapterOutAmount, 1e36 / INITIAL_RATE); + } + + function test_Quote_AtFloor() public { + vm.warp(10); + setPrice(FLOOR_RATE); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + assertEq(sentinelOutAmount, FLOOR_RATE); + assertEq(adapterOutAmount, FLOOR_RATE); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / FLOOR_RATE); + assertEq(adapterOutAmount, 1e36 / FLOOR_RATE); + } + + function test_Quote_BelowFloor() public { + vm.warp(10); + setPrice(0.5e18); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + assertEq(sentinelOutAmount, FLOOR_RATE); + assertEq(adapterOutAmount, 0.5e18); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / FLOOR_RATE); + assertEq(adapterOutAmount, 1e36 / 0.5e18); + } + + function test_Quote_IncreaseAtMaxGrowth() public { + vm.warp(10); + uint256 price = INITIAL_RATE + MAX_RATE_GROWTH * 10; + setPrice(price); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + assertEq(sentinelOutAmount, price); + assertEq(adapterOutAmount, price); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / price); + assertEq(adapterOutAmount, 1e36 / price); + } + + function test_Quote_IncreaseOverMaxGrowthUnderCeil() public { + vm.warp(20); + uint256 price = 4e18; + setPrice(price); + uint256 maxPrice = INITIAL_RATE + MAX_RATE_GROWTH * 20; + assertEq(sentinel.maxRate(), maxPrice); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + + assertEq(sentinelOutAmount, maxPrice); + assertEq(adapterOutAmount, price); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / maxPrice); + assertEq(adapterOutAmount, 1e36 / price); + } + + function test_Quote_IncreaseOverMaxGrowthAtCeil() public { + vm.warp(30); + uint256 price = 5e18; + setPrice(price); + uint256 maxPrice = INITIAL_RATE + MAX_RATE_GROWTH * 30; + assertEq(sentinel.maxRate(), maxPrice); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + + assertEq(sentinelOutAmount, maxPrice); + assertEq(adapterOutAmount, price); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / maxPrice); + assertEq(adapterOutAmount, 1e36 / price); + } + + function test_Quote_IncreaseOverMaxGrowthOverCeil() public { + vm.warp(40); + uint256 price = 6e18; + setPrice(price); + assertEq(sentinel.maxRate(), CEIL_RATE); + + uint256 sentinelOutAmount = sentinel.getQuote(1e18, base, quote); + uint256 adapterOutAmount = wrappedAdapter.getQuote(1e18, base, quote); + + assertEq(sentinelOutAmount, CEIL_RATE); + assertEq(adapterOutAmount, price); + + sentinelOutAmount = sentinel.getQuote(1e18, quote, base); + adapterOutAmount = wrappedAdapter.getQuote(1e18, quote, base); + assertEq(sentinelOutAmount, 1e36 / CEIL_RATE); + assertEq(adapterOutAmount, 1e36 / price); + } + + function setPrice(uint256 price) internal { + wrappedAdapter.setPrice(base, quote, price); + wrappedAdapter.setPrice(quote, base, 1e36 / price); + } +} diff --git a/test/utils/EthereumAddresses.sol b/test/utils/EthereumAddresses.sol index bb9034f0..adc227d4 100644 --- a/test/utils/EthereumAddresses.sol +++ b/test/utils/EthereumAddresses.sol @@ -83,6 +83,8 @@ address constant UNIETH = 0xF1376bceF0f78459C0Ed0ba5ddce976F1ddF51F4; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant USDD = 0x0C10bF8FcB7Bf5412187A595ab97a3609160b5c6; address constant USDE = 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3; +address constant USDL = 0xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD; +address constant USDM = 0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C; address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address constant USDP = 0x8E870D67F660D95d5be530380D0eC0bd388289E1; address constant USDV = 0x0E573Ce2736Dd9637A0b21058352e1667925C7a8; @@ -93,6 +95,8 @@ address constant WEETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant WSOL = 0xD31a59c85aE9D8edEFeC411D448f90841571b89c; address constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; +address constant WUSDL = 0x7751E2F4b8ae93EF6B79d86419d42FE3295A4559; +address constant WUSDM = 0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812; address constant XAUT = 0x68749665FF8D2d112Fa859AA293F07A622782F38; address constant XCN = 0xA2cd3D43c775978A96BdBf12d733D5A1ED94fb18; address constant YFI = 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e;