From ed039cf6230721713d81f1ce0ce4485dd990e6ff Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 4 Feb 2024 13:53:05 +0100 Subject: [PATCH 01/18] handling multiple prices and velo twaps --- .gitmodules | 3 + .vscode/settings.json | 5 + lib/tarot-price-oracle | 1 + remappings.txt | 1 + src/ReaperStrategyStabilityPool.sol | 142 +++++++++++++- src/interfaces/IVeloPair.sol | 124 ++++++++++++ test/OraclesTest.t.sol | 255 +++++++++++++++++++++++++ test/ReaperStrategyStabilityPool.t.sol | 36 ++-- 8 files changed, 550 insertions(+), 17 deletions(-) create mode 100644 .vscode/settings.json create mode 160000 lib/tarot-price-oracle create mode 100644 src/interfaces/IVeloPair.sol create mode 100644 test/OraclesTest.t.sol diff --git a/.gitmodules b/.gitmodules index 81a3ec5..876df8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/vault-v2"] path = lib/vault-v2 url = git@github.com:Byte-Masons/vault-v2.git +[submodule "lib/tarot-price-oracle"] + path = lib/tarot-price-oracle + url = https://github.com/xrave110/tarot-price-oracle diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..00a1f92 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity", + "solidity.formatter": "forge" +} \ No newline at end of file diff --git a/lib/tarot-price-oracle b/lib/tarot-price-oracle new file mode 160000 index 0000000..3b7b8a7 --- /dev/null +++ b/lib/tarot-price-oracle @@ -0,0 +1 @@ +Subproject commit 3b7b8a7a33e7a882e138eb66865049cf0af35e68 diff --git a/remappings.txt b/remappings.txt index b42cd60..33d0d05 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,3 +4,4 @@ ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/ forge-std/=lib/vault-v2/lib/forge-std/src/ oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/ oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/ +tarot-oracle/=lib/tarot-price-oracle/contracts/ \ No newline at end of file diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index f370728..caf95a5 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; +import "forge-std/Test.sol"; import "vault-v2/interfaces/ISwapper.sol"; import {ReaperBaseStrategyv4} from "vault-v2/ReaperBaseStrategyv4.sol"; import {IStabilityPool} from "./interfaces/IStabilityPool.sol"; @@ -14,6 +15,7 @@ import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol"; import {IERC20MetadataUpgradeable} from "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {SafeERC20Upgradeable} from "oz-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; +import {IVeloPair} from "./interfaces/IVeloPair.sol"; /** * @dev Strategy to compound rewards and liquidation collateral gains in the Ethos stability pool @@ -27,8 +29,11 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { IStabilityPool public stabilityPool; IPriceFeed public priceFeed; IERC20MetadataUpgradeable public usdc; + IERC20MetadataUpgradeable public weth; ExchangeSettings public exchangeSettings; // Holds addresses to use Velo, UniV3 and Bal through Swapper IUniswapV3Pool public uniV3UsdcErnPool; + IVeloPair public veloUsdcErnPool; + IVeloPair public veloWethErnPool; IStaticOracle public uniV3TWAP; uint256 public constant ETHOS_DECIMALS = 18; // Decimals used by ETHOS @@ -50,11 +55,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { struct Pools { address stabilityPool; address uniV3UsdcErnPool; + address veloUsdcErnPool; + address veloWethErnPool; } struct Tokens { address want; address usdc; + address weth; } error InvalidUsdcToErnExchange(uint256 exchangeEnum); @@ -85,6 +93,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { require(_tokens.want != address(0), "want is 0 address"); require(_priceFeed != address(0), "priceFeed is 0 address"); require(_tokens.usdc != address(0), "usdc is 0 address"); + require(_tokens.weth != address(0), "weth is 0 address"); require(_uniV3TWAP != address(0), "uniV3TWAP is 0 address"); require(_exchangeSettings.veloRouter != address(0), "veloRouter is 0 address"); require(_exchangeSettings.balVault != address(0), "balVault is 0 address"); @@ -92,11 +101,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { require(_exchangeSettings.uniV2Router != address(0), "uniV2Router is 0 address"); require(_pools.stabilityPool != address(0), "stabilityPool is 0 address"); require(_pools.uniV3UsdcErnPool != address(0), "uniV3UsdcErnPool is 0 address"); + require(_pools.veloUsdcErnPool != address(0), "veloUsdcErnPool is 0 address"); + require(_pools.veloWethErnPool != address(0), "veloWethErnPool is 0 address"); __ReaperBaseStrategy_init(_vault, _swapper, _tokens.want, _strategists, _multisigRoles, _keepers); stabilityPool = IStabilityPool(_pools.stabilityPool); priceFeed = IPriceFeed(_priceFeed); usdc = IERC20MetadataUpgradeable(_tokens.usdc); + weth = IERC20MetadataUpgradeable(_tokens.weth); exchangeSettings = _exchangeSettings; updateErnMinAmountOutBPS(9800); @@ -104,6 +116,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { uniV3TWAP = IStaticOracle(_uniV3TWAP); uniV3UsdcErnPool = IUniswapV3Pool(_pools.uniV3UsdcErnPool); + veloUsdcErnPool = IVeloPair(_pools.veloUsdcErnPool); + veloWethErnPool = IVeloPair(_pools.veloWethErnPool); compoundingFeeMarginBPS = 9950; updateUniV3TWAPPeriod(7200); updateAcceptableTWAPBounds(980_000, 1_100_000); @@ -316,7 +330,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { */ function _getErnAmountForUsdc(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - expectedErnAmount = getErnAmountForUsdcUniV3(uint128(_usdcAmount), uniV3TWAPPeriod); + expectedErnAmount = getErnAmountForUsdcAll(uint128(_usdcAmount), uniV3TWAPPeriod); } } @@ -332,6 +346,128 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { return quoteAmount; } + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the Velo TWAP. + */ + function getErnAmountForUsdcVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + require(_period >= 2 days, "Too short period"); + address[] memory pools = new address[](1); + uint256 window = _period / 1 days; + require(window <= veloUsdcErnPool.observationLength(), "Window longer than observation length"); + + uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); + uint256[] memory quoteAmount = veloUsdcErnPool.sample(address(usdc), (10 ** usdc.decimals()), 1, window); + return (_baseAmount * quoteAmount[0] * (10 ** (wantDecimals - usdc.decimals())) / 1 ether); // better math + } + + function getUsdcAmountForWethUsingPriceFeeds() public returns (uint256) { + uint256 tmpPrice = _getUSDEquivalentOfCollateralUsingPriceFeed(address(weth), 1 ether); + return _getUsdcEquivalentOfUSD(tmpPrice); + } + + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the Velo TWAP. + */ + function getErnAmountForWethVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + require(_period >= 2 days, "Too short period"); + address[] memory pools = new address[](1); + uint256 window = _period / 1 days; + require(window <= veloWethErnPool.observationLength(), "Window longer than observation length"); + + uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); + uint256[] memory quoteAmount = veloWethErnPool.sample(address(weth), 1e18, 1, window); + return (_baseAmount * quoteAmount[0] / 1 ether); // better math + } + + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the Velo TWAP. + * + * Math: + * 1e18 wei - x ern + * 1e18 wei - y usdc + * ern = 1e18 wei / x => ern = y usdc / x + */ + function getErnAmountForUsdcVeloWeth(uint128 _baseAmount, uint32 _period) public returns (uint256) { + uint256 usdcAmountForWethPriceFeeds = (getUsdcAmountForWethUsingPriceFeeds() * _baseAmount); + // console2.log("Feed: ", usdcAmountForWethPriceFeeds); + uint256 veloAmountForWethVelo = getErnAmountForWethVelo(_baseAmount, _period); + // console2.log("Velo: ", veloAmountForWethVelo); + return ((usdcAmountForWethPriceFeeds * 1 ether * 10 ** usdc.decimals()) / veloAmountForWethVelo); + } + + function getInfoAboutTwapOracles(uint256[] memory prices, uint32 idx, uint32 tolerance) + private + view + returns (bool[] memory indexes, uint32 validAmount) + { + uint32 PERCENTAGE = 100_000; // make global constant + + indexes = new bool[](prices.length); + uint256 referencePrice = prices[idx]; + indexes[idx] = true; + validAmount = 1; + + for (uint32 cnt = (idx + 1) % uint32(prices.length); cnt != idx; cnt = (cnt + 1) % uint32(prices.length)) { + console2.log("Reference price: ", referencePrice); + console2.log("vs prices[cnt]: ", prices[cnt]); + if ( + referencePrice + (referencePrice * tolerance / PERCENTAGE) >= prices[cnt] + && referencePrice - (referencePrice * tolerance / PERCENTAGE) <= prices[cnt] + ) { + validAmount++; + indexes[cnt] = true; + } + } + } + + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the all possible oracles. + */ + function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _period) public returns (uint256) { + uint256[] memory _prices = new uint256[](3); + _prices[0] = getErnAmountForUsdcVelo(_baseAmount, _period); + _prices[1] = getErnAmountForUsdcUniV3(_baseAmount, _period); + _prices[2] = getErnAmountForUsdcVeloWeth(_baseAmount, _period); // This function is not view + + return getErnAmountForUsdcAll(_prices, _baseAmount, _period, 1000); + } + + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the all possible oracles. + */ + function getErnAmountForUsdcAll(uint256[] memory _prices, uint128 _baseAmount, uint32 _period, uint32 _tolerance) + public + view + returns (uint256) + { + uint256 meanTwap = 0; + + for (uint32 idx = 0; idx < _prices.length; idx++) { + (bool[] memory indexes, uint32 validAmount) = getInfoAboutTwapOracles(_prices, idx, _tolerance); + console2.log("Idx: ", idx); + console2.log("Valid amount: ", validAmount); + // Amount of valid prices must be greater than 50% + if (validAmount > (_prices.length / 2)) { + uint256 sumOfPrices = 0; + for (uint32 cnt = 0; cnt < indexes.length; cnt++) { + if (indexes[cnt] != false) { + sumOfPrices += _prices[cnt]; + console2.log("Sum of prices: ", sumOfPrices); + } + } + meanTwap = sumOfPrices / validAmount; + break; + } + } + require(meanTwap != 0, "Couldn't determine mean price"); + return meanTwap; + } + /** * @dev Returns USD equivalent of {_amount} of {_collateral} with 18 digits of decimal precision. * The precision of {_amount} is whatever {_collateral}'s native decimals are (ex. 8 for wBTC) @@ -482,8 +618,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { _atLeastRole(ADMIN); require(_uniV3TWAPPeriod >= 7200, "TWAP period is too short"); - uint256 newErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), _uniV3TWAPPeriod); - uint256 oldErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), uniV3TWAPPeriod); + uint256 newErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), _uniV3TWAPPeriod); + uint256 oldErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), uniV3TWAPPeriod); uniV3TWAPPeriod = _uniV3TWAPPeriod; diff --git a/src/interfaces/IVeloPair.sol b/src/interfaces/IVeloPair.sol new file mode 100644 index 0000000..d99709d --- /dev/null +++ b/src/interfaces/IVeloPair.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IVeloPair { + error DepositsNotEqual(); + error BelowMinimumK(); + error FactoryAlreadySet(); + error InsufficientLiquidity(); + error InsufficientLiquidityMinted(); + error InsufficientLiquidityBurned(); + error InsufficientOutputAmount(); + error InsufficientInputAmount(); + error IsPaused(); + error InvalidTo(); + error K(); + error NotEmergencyCouncil(); + + event Fees(address indexed sender, uint256 amount0, uint256 amount1); + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn( + address indexed sender, + address indexed to, + uint256 amount0, + uint256 amount1 + ); + event Swap( + address indexed sender, + address indexed to, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out + ); + event Sync(uint256 reserve0, uint256 reserve1); + event Claim( + address indexed sender, + address indexed recipient, + uint256 amount0, + uint256 amount1 + ); + + function metadata() + external + view + returns ( + uint256 dec0, + uint256 dec1, + uint256 r0, + uint256 r1, + bool st, + address t0, + address t1 + ); + + function claimFees() external returns (uint256, uint256); + + function tokens() external view returns (address, address); + + function token0() external view returns (address); + + function token1() external view returns (address); + + function stable() external view returns (bool); + + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata data + ) external; + + function burn( + address to + ) external returns (uint256 amount0, uint256 amount1); + + function mint(address to) external returns (uint256 liquidity); + + function getReserves() + external + view + returns ( + uint256 _reserve0, + uint256 _reserve1, + uint256 _blockTimestampLast + ); + + function getAmountOut(uint256, address) external view returns (uint256); + + function skim(address to) external; + + function initialize( + address _token0, + address _token1, + bool _stable + ) external; + + function reserve0CumulativeLast() external view returns (uint256); + + function reserve1CumulativeLast() external view returns (uint256); + + function currentCumulativePrices() + external + view + returns ( + uint256 reserve0Cumulative, + uint256 reserve1Cumulative, + uint256 blockTimestamp + ); + + function prices( + address tokenIn, + uint256 amountIn, + uint256 points + ) external view returns (uint256[] memory); + + function sample( + address tokenIn, + uint256 amountIn, + uint256 points, + uint256 window + ) external view returns (uint256[] memory); + + function observationLength() external view returns (uint256); +} diff --git a/test/OraclesTest.t.sol b/test/OraclesTest.t.sol new file mode 100644 index 0000000..f323a35 --- /dev/null +++ b/test/OraclesTest.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "src/ReaperStrategyStabilityPool.sol"; +// import "tarot-oracle/TarotPriceOracleVolatile.sol"; +import "vault-v2/ReaperVaultV2.sol"; +import {IERC20Upgradeable} from "oz-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; +import {IERC20} from "oz/token/ERC20/IERC20.sol"; +import {ReaperSwapper, MinAmountOutData, MinAmountOutKind} from "vault-v2/ReaperSwapper.sol"; +import {IVeloRouter} from "vault-v2/interfaces/IVeloRouter.sol"; +// import "tarot-oracle/interfaces/IVeloPair.sol"; +import {Pool} from "tarot-oracle/toWatch/Pool.sol"; + +contract TarotOracleTest is Test { + uint256 FORK_BLOCK = 115641661; + + ReaperVaultV2 public vault; + string public vaultName = "ERN Stability Pool Vault"; + string public vaultSymbol = "rf-SP-ERN"; + uint256 public vaultTvlCap = type(uint256).max; + address public treasuryAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; + address public strategistAddr = 0x1A20D7A31e5B3Bc5f02c8A146EF6f394502a10c4; + address public superAdminAddress = 0x9BC776dBb134Ef9D7014dB1823Cd755Ac5015203; + address public adminAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; + address public guardianAddress = 0xb0C9D5851deF8A2Aac4A23031CA2610f8C3483F9; + address public uniV3UsdcErnPool = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; + address public wantHolderAddr = strategistAddr; + address[] public strategists = [strategistAddr]; + address[] public multisigRoles = [superAdminAddress, adminAddress, guardianAddress]; + + address public balVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + address public uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address public uniV2Router = 0xbeeF000000000000000000000000000000000000; + address public uniV3TWAP = 0xB210CE856631EeEB767eFa666EC7C1C57738d438; + + address[] keepers = [ + 0xe0268Aa6d55FfE1AA7A77587e56784e5b29004A2, + 0x34Df14D42988e4Dc622e37dc318e70429336B6c5, + 0x73C882796Ea481fe0A2B8DE499d95e60ff971663, + 0x36a63324edFc157bE22CF63A6Bf1C3B49a0E72C0, + 0x9a2AdcbFb972e0EC2946A342f46895702930064F, + 0x7B540a4D24C906E5fB3d3EcD0Bb7B1aEd3823897, + 0x8456a746e09A18F9187E5babEe6C60211CA728D1, + 0x55a078AFC2e20C8c20d1aa4420710d827Ee494d4, + 0x5241F63D0C1f2970c45234a0F5b345036117E3C2, + 0xf58d534290Ce9fc4Ea639B8b9eE238Fe83d2efA6, + 0x5318250BD0b44D1740f47a5b6BE4F7fD5042682D, + 0x33D6cB7E91C62Dd6980F16D61e0cfae082CaBFCA, + 0x51263D56ec81B5e823e34d7665A1F505C327b014, + 0x87A5AfC8cdDa71B5054C698366E97DB2F3C2BC2f + ]; + + address public wethAddress = 0x4200000000000000000000000000000000000006; + address public wbtcAddress = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; + address public usdcAddress = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address public ernAddress = 0xc5b001DC33727F8F26880B184090D3E252470D45; + address public usdceAddress = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607; + address public veloRouter = 0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858; + address public veloFactoryV1 = 0x25CbdDb98b35ab1FF77413456B31EC81A6B6B746; + address public veloFactoryV2Default = 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a; + address public veloUsdcErnPool = 0x605cCE502dEe6BD201b493782e351e645D44abBB; + address public veloWethErnPool = 0xFFf37730744930Cb61Be34c0014068F4f1eC28cF; + address public wantAddress = ernAddress; + //address public veloUsdcErnPoolOLD = 0x5e4A183Fa83C52B1c55b11f2682f6a8421206633; + address public stabilityPoolAddress = 0x8B147A2d4Fc3598079C64b8BF9Ad2f776786CFed; + + address public priceFeedAddress = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; + address public priceFeedOwnerAddress = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C; + + bytes32 public balErnPoolId = 0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1; + + address public ernWhale = 0x223341f84E784f0cFD3e30438DBDF5Aa4384A8b9; + address public usdcWhale = 0xf491d040110384DBcf7F241fFE2A546513fD873d; + + uint256 public optimismFork; + + ReaperSwapper reaperSwapper; + ReaperStrategyStabilityPool implementation; + ReaperStrategyStabilityPool wrappedProxy; + // TarotPriceOracleVolatile tarot; + + function setUp() public { + Pool pool = new Pool(); + // address[] memory strategists = new address[](1); + // strategists[0] = makeAddr("strategist1"); + //0x95885Af5492195F0754bE71AD1545Fe81364E531 + // vm.etch(0x95885Af5492195F0754bE71AD1545Fe81364E531, address(pool).code); + // vm.etch(veloUsdcErnPool, address(pool).code); + // Forking + string memory rpc = vm.envString("RPC"); + optimismFork = vm.createSelectFork(rpc, FORK_BLOCK); + assertEq(vm.activeFork(), optimismFork); + + // Deploying + // tarot = new TarotPriceOracleVolatile(); + // tarot.initialize(veloUsdcErnPool); + // veloWethErnPool = IVeloRouter(veloRouter).poolFor(wethAddress, ernAddress, false, veloFactoryV2Default); + // tarot.initialize(veloWethErnPool); + + /* Reaper deployment and configuration */ + ERC1967Proxy tmpProxy; + reaperSwapper = new ReaperSwapper(); + tmpProxy = new ERC1967Proxy(address(reaperSwapper), ""); + reaperSwapper = ReaperSwapper(address(tmpProxy)); + reaperSwapper.initialize(strategists, address(this), address(this)); + IVeloRouter.Route[] memory veloPath = new IVeloRouter.Route[](1); + veloPath[0] = IVeloRouter.Route(ernAddress, usdcAddress, true, veloFactoryV2Default); + reaperSwapper.updateVeloSwapPath(ernAddress, usdcAddress, address(veloRouter), veloPath); + veloPath[0] = IVeloRouter.Route(usdcAddress, ernAddress, true, veloFactoryV2Default); + reaperSwapper.updateVeloSwapPath(usdcAddress, ernAddress, address(veloRouter), veloPath); + + vault = new ReaperVaultV2( + wantAddress, + vaultName, + vaultSymbol, + vaultTvlCap, + treasuryAddress, + strategists, + multisigRoles + ); + + ReaperStrategyStabilityPool.ExchangeSettings memory exchangeSettings; + exchangeSettings.veloRouter = veloRouter; + exchangeSettings.balVault = balVault; + exchangeSettings.uniV3Router = uniV3Router; + exchangeSettings.uniV2Router = uniV2Router; + + ReaperStrategyStabilityPool.Pools memory pools; + pools.stabilityPool = stabilityPoolAddress; + pools.uniV3UsdcErnPool = uniV3UsdcErnPool; + pools.veloUsdcErnPool = veloUsdcErnPool; + pools.veloWethErnPool = veloWethErnPool; + + ReaperStrategyStabilityPool.Tokens memory tokens; + tokens.want = wantAddress; + tokens.usdc = usdcAddress; + tokens.weth = wethAddress; + + implementation = new ReaperStrategyStabilityPool(); + tmpProxy = new ERC1967Proxy(address(implementation), ""); + wrappedProxy = ReaperStrategyStabilityPool(address(tmpProxy)); + + wrappedProxy.initialize( + address(vault), + address(reaperSwapper), + strategists, + multisigRoles, + keepers, + priceFeedAddress, + uniV3TWAP, + exchangeSettings, + pools, + tokens + ); + } + + function testMultipleOracles(uint128 baseAmount, uint32 period) public { + baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); + period = uint32(bound(period, 2 days, 9 days)); + uint32 tolerance = 5000; + // console2.log("1. Pool address: ", veloUsdcErnPool); + // console2.log("1. USDC address: ", usdcAddress); + // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 2); + // console2.log("1. Price Velo: ", prices[0]); + uint256[] memory prices = new uint256[](6); + prices[0] = wrappedProxy.getErnAmountForUsdcVelo(baseAmount, period); + prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); + prices[2] = 1059945924123 * baseAmount; + prices[3] = 959945924123 * baseAmount; + prices[4] = 1009945924123 * baseAmount; + prices[5] = 0 * baseAmount; + console2.log("Price Velo: ", prices[0]); + console2.log("Price UniV3: ", prices[1]); + uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, baseAmount, period, tolerance); + // console2.log("1.Price All: ", wrappedProxy.getErnAmountForUsdcAll(baseAmount, period)); + console2.log("2.Price All: ", finalPrice); + uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 100_000; + uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 100_000; + assert(finalPrice < highBoundary && finalPrice > lowBoundary); + } + + function testSeparaeteOracles() public { + console2.log("Velo 1 WETH = %d ERN", wrappedProxy.getErnAmountForWethVelo(1 ether, 2 days)); + console2.log("Chainlink 1 WETH = %d USDC", wrappedProxy.getUsdcAmountForWethUsingPriceFeeds()); + console2.log(wrappedProxy.getErnAmountForUsdcVelo(1 ether, 2 days)); + console2.log(wrappedProxy.getErnAmountForUsdcUniV3(1 ether, 2 days)); + console2.log(wrappedProxy.getErnAmountForUsdcVeloWeth(1 ether, 2 days)); + } + + function testTarotOracle_Weth() public { + uint256 amount = 20000e18; + console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + console2.log("Length WETH/ERN: ", IVeloPair(veloWethErnPool).observationLength()); + MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); + vm.warp(block.timestamp + 1 days); + uint256[] memory prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 10); + console2.log("1. Sample result: ", prices[0]); + prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 100); + console2.log("2. Sample result: ", prices[0]); + prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 1000); + console2.log("3. Sample result: ", prices[0]); + prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 10); + console2.log("2. Sample result: ", prices[0]); + console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + console2.log("Length WETH/ERN: ", IVeloPair(veloWethErnPool).observationLength()); + // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 1000); + console2.log("3. Sample result: ", prices[0]); + } + + // function testWindows() public { + // string memory rpc = vm.envString("RPC"); + // console2.log(1 seconds, 1 hours, 1 days); + // optimismFork = vm.createSelectFork(rpc, 115651661); + // console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + + // optimismFork = vm.createSelectFork(rpc, 115651661 - 30 minutes); + // console2.log("Length 30 minutes USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + + // optimismFork = vm.createSelectFork(rpc, 115651661 - 1 hours); + // console2.log("Length 1 hour USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // optimismFork = vm.createSelectFork(rpc, 115651661 - 2 hours); + // console2.log("Length 2 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // optimismFork = vm.createSelectFork(rpc, 115651661 - 3 hours); + // console2.log("Length 3 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // optimismFork = vm.createSelectFork(rpc, 115651661 - 4 hours); + // console2.log("Length 4 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // optimismFork = vm.createSelectFork(rpc, 115651661 - 1 days); + // console2.log("Length -1 day USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // optimismFork = vm.createSelectFork(rpc, 115651661 - 10 days); + // console2.log("Length -2 days USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); + // } + + // function testTarotOracle() public { + // uint256 amount = 20000e18; + // MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); + // vm.warp(block.timestamp + 1 days); + // (uint256 result, uint256 timestamp) = tarot.getResult(veloUsdcErnPool); + // console2.log("1. Result before swap: ", result); + // vm.startPrank(ernWhale); + // console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); + // console2.log("1. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); + // IERC20Upgradeable(ernAddress).approve(address(reaperSwapper), amount); + // reaperSwapper.swapVelo(ernAddress, usdcAddress, amount, minAmountOutData, address(veloRouter)); + // console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); + // console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); + // (result, timestamp) = tarot.getResult(veloUsdcErnPool); + // console2.log("2. Result after swap: ", result); + // vm.stopPrank(); + // vm.warp(block.timestamp + 1 weeks); + // (result, timestamp) = tarot.getResult(veloUsdcErnPool); + // console2.log("3. Result after 1 week: ", result); + // } +} diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol index bd4febb..baf2133 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "src/ReaperStrategyStabilityPool.sol"; -import "vault-v2/ReaperSwapper.sol"; +import {ReaperSwapper, ISwapRouter, TransferHelper} from "vault-v2/ReaperSwapper.sol"; import "vault-v2/ReaperVaultV2.sol"; import "vault-v2/ReaperBaseStrategyv4.sol"; import "vault-v2/interfaces/ISwapper.sol"; @@ -38,7 +38,7 @@ contract ReaperStrategyStabilityPoolTest is Test { address public balVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; address public uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address public uniV2Router = 0xbeeF000000000000000000000000000000000000; // Any non-0 address when UniV2 router does not exist - address public veloUsdcErnPool = 0x5e4A183Fa83C52B1c55b11f2682f6a8421206633; + address public veloUsdcErnPool = 0x605cCE502dEe6BD201b493782e351e645D44abBB; address public uniV3UsdcErnPool = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; address public chainlinkUsdcOracle = 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3; address public uniV3TWAP = 0xB210CE856631EeEB767eFa666EC7C1C57738d438; @@ -113,7 +113,7 @@ contract ReaperStrategyStabilityPoolTest is Test { function setUp() public { // Forking string memory rpc = vm.envString("RPC"); - optimismFork = vm.createSelectFork(rpc, 107994026); + optimismFork = vm.createSelectFork(rpc, 115675719); assertEq(vm.activeFork(), optimismFork); // // Deploying stuff @@ -123,8 +123,15 @@ contract ReaperStrategyStabilityPoolTest is Test { wrappedSwapperProxy.initialize(strategists, guardianAddress, superAdminAddress); swapper = ISwapper(address(swapperProxy)); - vault = - new ReaperVaultV2(wantAddress, vaultName, vaultSymbol, vaultTvlCap, treasuryAddress, strategists, multisigRoles); + vault = new ReaperVaultV2( + wantAddress, + vaultName, + vaultSymbol, + vaultTvlCap, + treasuryAddress, + strategists, + multisigRoles + ); implementation = new ReaperStrategyStabilityPool(); proxy = new ERC1967Proxy(address(implementation), ""); wrappedProxy = ReaperStrategyStabilityPool(address(proxy)); @@ -138,6 +145,7 @@ contract ReaperStrategyStabilityPoolTest is Test { ReaperStrategyStabilityPool.Pools memory pools; pools.stabilityPool = stabilityPoolAddress; pools.uniV3UsdcErnPool = uniV3UsdcErnPool; + pools.veloUsdcErnPool = veloUsdcErnPool; address[] memory usdcErnPath = new address[](2); usdcErnPath[0] = usdcAddress; @@ -791,7 +799,7 @@ contract ReaperStrategyStabilityPoolTest is Test { assertEq(valueInCollateralAfter, priceQuote); uint256 compoundingFeeMarginBPS = wrappedProxy.compoundingFeeMarginBPS(); - uint256 expectedPoolBalance = valueInCollateralAfter * compoundingFeeMarginBPS / BPS_UNIT; + uint256 expectedPoolBalance = (valueInCollateralAfter * compoundingFeeMarginBPS) / BPS_UNIT; console.log("expectedPoolBalance: ", expectedPoolBalance); assertEq(poolBalanceAfter, expectedPoolBalance); } @@ -853,8 +861,8 @@ contract ReaperStrategyStabilityPoolTest is Test { // All usd values must have 18 decimals for comparison. // WETH and OP already have 18 decimals, but we need to scale WBTC. uint256 wbtcUsdValue = wbtcAmount * uint256(wbtcPrice) * (10 ** 2); - uint256 wethUsdValue = wethAmount * uint256(wethPrice) / (10 ** 8); - uint256 opUsdValue = opAmount * uint256(opPrice) / (10 ** 8); + uint256 wethUsdValue = (wethAmount * uint256(wethPrice)) / (10 ** 8); + uint256 opUsdValue = (opAmount * uint256(opPrice)) / (10 ** 8); uint256 expectedUsdValueInCollateral = wbtcUsdValue + wethUsdValue + opUsdValue; console.log("wbtcUsdValue: ", wbtcUsdValue); console.log("wethUsdValue: ", wethUsdValue); @@ -887,7 +895,7 @@ contract ReaperStrategyStabilityPoolTest is Test { assertApproxEqRel(ernAmount, wantValueInCollateral, 1e8); uint256 compoundingFeeMarginBPS = wrappedProxy.compoundingFeeMarginBPS(); - uint256 expectedPoolIncrease = ernAmount * compoundingFeeMarginBPS / BPS_UNIT; + uint256 expectedPoolIncrease = (ernAmount * compoundingFeeMarginBPS) / BPS_UNIT; // console.log("poolBalanceIncrease: ", poolBalanceAfter - poolBalanceBefore); // console.log("expectedPoolIncrease: ", expectedPoolIncrease); // assertEq(poolBalanceAfter - poolBalanceBefore, expectedPoolIncrease); @@ -925,7 +933,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 valueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); console.log("valueInCollateral: ", valueInCollateral); - uint256 newUsdcPrice = usdcPrice * 9500 / BPS_UNIT; + uint256 newUsdcPrice = (usdcPrice * 9500) / BPS_UNIT; vm.startPrank(usdcOracleOwner); mockChainlink.setPrice(int256(newUsdcPrice)); mockChainlink.setPrevPrice(int256(newUsdcPrice)); @@ -933,7 +941,7 @@ contract ReaperStrategyStabilityPoolTest is Test { // uint256 usdcPrice = uint256(usdcAggregator.latestAnswer()); // console.log("usdcPrice: ", usdcPrice); - uint256 expectedValueInCollateral = valueInCollateral * 10_526 / BPS_UNIT; + uint256 expectedValueInCollateral = (valueInCollateral * 10_526) / BPS_UNIT; valueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); console.log("expectedValueInCollateral: ", expectedValueInCollateral); console.log("valueInCollateral: ", valueInCollateral); @@ -975,7 +983,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = usdcInPool * 9999 / 10_000; + uint256 usdcToDump = (usdcInPool * 9999) / 10_000; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); @@ -1058,7 +1066,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = usdcInPool * 9999 / 10_000; + uint256 usdcToDump = (usdcInPool * 9999) / 10_000; uint256 ernToDump = 10 * 1 ether; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); deal({token: wantAddress, to: address(this), give: ernToDump * 100}); @@ -1132,7 +1140,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = usdcInPool * 9999 / 10_000; + uint256 usdcToDump = (usdcInPool * 9999) / 10_000; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); uint256 nrOfSwaps = 100; From c7872de0816470e3c38b086f5047966bba977d4f Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 4 Feb 2024 14:46:58 +0100 Subject: [PATCH 02/18] Tests updates and adjustments --- .gitmodules | 6 +- lib/options-token | 1 + lib/tarot-price-oracle | 1 - script/upgrade/validateUpgrade.js | 2 +- src/ReaperStrategyStabilityPool.sol | 59 ++++++++++++-------- src/interfaces/IVeloPair.sol | 76 +++++--------------------- test/OraclesTest.t.sol | 33 ++++++++--- test/ReaperStrategyStabilityPool.t.sol | 32 +++++------ 8 files changed, 97 insertions(+), 113 deletions(-) create mode 160000 lib/options-token delete mode 160000 lib/tarot-price-oracle diff --git a/.gitmodules b/.gitmodules index 876df8e..bbb748b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "lib/vault-v2"] path = lib/vault-v2 url = git@github.com:Byte-Masons/vault-v2.git -[submodule "lib/tarot-price-oracle"] - path = lib/tarot-price-oracle - url = https://github.com/xrave110/tarot-price-oracle +[submodule "lib/options-token"] + path = lib/options-token + url = https://github.com/Byte-Masons/options-token diff --git a/lib/options-token b/lib/options-token new file mode 160000 index 0000000..9aa8a91 --- /dev/null +++ b/lib/options-token @@ -0,0 +1 @@ +Subproject commit 9aa8a91e6788ada2354682e43e781e1c691af574 diff --git a/lib/tarot-price-oracle b/lib/tarot-price-oracle deleted file mode 160000 index 3b7b8a7..0000000 --- a/lib/tarot-price-oracle +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3b7b8a7a33e7a882e138eb66865049cf0af35e68 diff --git a/script/upgrade/validateUpgrade.js b/script/upgrade/validateUpgrade.js index 83cce3f..48d8ca5 100644 --- a/script/upgrade/validateUpgrade.js +++ b/script/upgrade/validateUpgrade.js @@ -14,4 +14,4 @@ main() .catch((error) => { console.error(error); process.exit(1); - }); \ No newline at end of file + }); diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index caf95a5..9e2a092 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -39,7 +39,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { uint256 public constant ETHOS_DECIMALS = 18; // Decimals used by ETHOS uint256 public ernMinAmountOutBPS; // The max allowed slippage when trading in to ERN uint256 public compoundingFeeMarginBPS; // How much collateral value is lowered to account for the costs of swapping - uint32 public uniV3TWAPPeriod; // How many seconds the uniV3 TWAP will look at + uint32 public twapPeriod; // How many seconds the uniV3 TWAP will look at ExchangeType public usdcToErnExchange; // Controls which exchange is used to swap USDC to ERN bool public shouldOverrideHarvestBlock; // If reverts on TWAP out of normal range should be ignored uint256 acceptableTWAPUpperBound; // The normal upper price for the TWAP, reverts harvest if above @@ -69,6 +69,9 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { error InvalidUsdcToErnTWAP(uint256 twapEnum); error TWAPOutsideAllowedRange(uint256 usdcPrice); error InvalidSwapStep(); + error StabilityPool__CouldntDetermineMeanPrice(); + error StabilityPool__WindowLongerThanOrZero(); + error StabilityPool__TooShortPeriod(); /** * @dev Initializes the strategy. Sets parameters, saves routes, and gives allowances. @@ -119,7 +122,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { veloUsdcErnPool = IVeloPair(_pools.veloUsdcErnPool); veloWethErnPool = IVeloPair(_pools.veloWethErnPool); compoundingFeeMarginBPS = 9950; - updateUniV3TWAPPeriod(7200); + twapPeriod = 7200; // Question: Couldn't understand how it will work with function and twapPeriod as a 0 at the init, + // Can it be initialization of global variable instead of update function ? updateAcceptableTWAPBounds(980_000, 1_100_000); } @@ -186,7 +190,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); uint256 usdcAmount = - uniV3TWAP.quoteSpecificPoolsWithTimePeriod(ernAmount, want, address(usdc), pools, uniV3TWAPPeriod); + uniV3TWAP.quoteSpecificPoolsWithTimePeriod(ernAmount, want, address(usdc), pools, twapPeriod); if (usdcAmount < acceptableTWAPLowerBound || usdcAmount > acceptableTWAPUpperBound) { revert TWAPOutsideAllowedRange(usdcAmount); @@ -330,7 +334,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { */ function _getErnAmountForUsdc(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - expectedErnAmount = getErnAmountForUsdcAll(uint128(_usdcAmount), uniV3TWAPPeriod); + expectedErnAmount = getErnAmountForUsdcAll(uint128(_usdcAmount), twapPeriod); } } @@ -351,10 +355,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * using the Velo TWAP. */ function getErnAmountForUsdcVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - require(_period >= 2 days, "Too short period"); + if (_period < 2 days) { + revert StabilityPool__TooShortPeriod(); + } address[] memory pools = new address[](1); uint256 window = _period / 1 days; - require(window <= veloUsdcErnPool.observationLength(), "Window longer than observation length"); + if (window >= veloUsdcErnPool.observationLength() || window == 0) { + revert StabilityPool__WindowLongerThanOrZero(); + } uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); uint256[] memory quoteAmount = veloUsdcErnPool.sample(address(usdc), (10 ** usdc.decimals()), 1, window); @@ -371,10 +379,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * using the Velo TWAP. */ function getErnAmountForWethVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - require(_period >= 2 days, "Too short period"); + if (_period < 2 days) { + revert StabilityPool__TooShortPeriod(); + } address[] memory pools = new address[](1); uint256 window = _period / 1 days; - require(window <= veloWethErnPool.observationLength(), "Window longer than observation length"); + if (window >= veloUsdcErnPool.observationLength() || window == 0) { + revert StabilityPool__WindowLongerThanOrZero(); + } uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); uint256[] memory quoteAmount = veloWethErnPool.sample(address(weth), 1e18, 1, window); @@ -403,8 +415,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { view returns (bool[] memory indexes, uint32 validAmount) { - uint32 PERCENTAGE = 100_000; // make global constant - indexes = new bool[](prices.length); uint256 referencePrice = prices[idx]; indexes[idx] = true; @@ -413,9 +423,10 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { for (uint32 cnt = (idx + 1) % uint32(prices.length); cnt != idx; cnt = (cnt + 1) % uint32(prices.length)) { console2.log("Reference price: ", referencePrice); console2.log("vs prices[cnt]: ", prices[cnt]); + // Question: Can be acceptableTWAPLowerBound and acceptableTWAPUpperBound be applied here ? if ( - referencePrice + (referencePrice * tolerance / PERCENTAGE) >= prices[cnt] - && referencePrice - (referencePrice * tolerance / PERCENTAGE) <= prices[cnt] + referencePrice + (referencePrice * tolerance / PERCENT_DIVISOR) >= prices[cnt] + && referencePrice - (referencePrice * tolerance / PERCENT_DIVISOR) <= prices[cnt] ) { validAmount++; indexes[cnt] = true; @@ -427,13 +438,13 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) * using the all possible oracles. */ - function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _period) public returns (uint256) { - uint256[] memory _prices = new uint256[](3); + function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + uint256[] memory _prices = new uint256[](2); _prices[0] = getErnAmountForUsdcVelo(_baseAmount, _period); _prices[1] = getErnAmountForUsdcUniV3(_baseAmount, _period); - _prices[2] = getErnAmountForUsdcVeloWeth(_baseAmount, _period); // This function is not view + //_prices[2] = getErnAmountForUsdcVeloWeth(_baseAmount, _period); // This function is not view - return getErnAmountForUsdcAll(_prices, _baseAmount, _period, 1000); + return getErnAmountForUsdcAll(_prices, _baseAmount, _period, 200); } /** @@ -464,7 +475,9 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { break; } } - require(meanTwap != 0, "Couldn't determine mean price"); + if (meanTwap == 0) { + revert StabilityPool__CouldntDetermineMeanPrice(); + } return meanTwap; } @@ -487,7 +500,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { internal returns (uint256) { - uint256 price = priceFeed.fetchPrice(_collateral); + uint256 price = priceFeed.fetchPrice(_collateral); // Question: This make function not viewable an must be propagated upper return _getUSDEquivalentOfCollateralCommon(_collateral, _amount, price, ETHOS_DECIMALS); } @@ -614,14 +627,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * roles a check is performed to see if changing duration would effect the price * past some threshold, if the strategy holds collateral value (priced by TWAP). */ - function updateUniV3TWAPPeriod(uint32 _uniV3TWAPPeriod) public { + function updateTwapPeriod(uint32 _twapPeriod) public { _atLeastRole(ADMIN); - require(_uniV3TWAPPeriod >= 7200, "TWAP period is too short"); + require(_twapPeriod >= 7200, "TWAP period is too short"); - uint256 newErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), _uniV3TWAPPeriod); - uint256 oldErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), uniV3TWAPPeriod); + uint256 newErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), _twapPeriod); + uint256 oldErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), twapPeriod); - uniV3TWAPPeriod = _uniV3TWAPPeriod; + twapPeriod = _twapPeriod; if (_hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) return; diff --git a/src/interfaces/IVeloPair.sol b/src/interfaces/IVeloPair.sol index d99709d..193bb86 100644 --- a/src/interfaces/IVeloPair.sol +++ b/src/interfaces/IVeloPair.sol @@ -17,12 +17,7 @@ interface IVeloPair { event Fees(address indexed sender, uint256 amount0, uint256 amount1); event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn( - address indexed sender, - address indexed to, - uint256 amount0, - uint256 amount1 - ); + event Burn(address indexed sender, address indexed to, uint256 amount0, uint256 amount1); event Swap( address indexed sender, address indexed to, @@ -32,25 +27,12 @@ interface IVeloPair { uint256 amount1Out ); event Sync(uint256 reserve0, uint256 reserve1); - event Claim( - address indexed sender, - address indexed recipient, - uint256 amount0, - uint256 amount1 - ); + event Claim(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1); function metadata() external view - returns ( - uint256 dec0, - uint256 dec1, - uint256 r0, - uint256 r1, - bool st, - address t0, - address t1 - ); + returns (uint256 dec0, uint256 dec1, uint256 r0, uint256 r1, bool st, address t0, address t1); function claimFees() external returns (uint256, uint256); @@ -62,37 +44,19 @@ interface IVeloPair { function stable() external view returns (bool); - function swap( - uint256 amount0Out, - uint256 amount1Out, - address to, - bytes calldata data - ) external; + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; - function burn( - address to - ) external returns (uint256 amount0, uint256 amount1); + function burn(address to) external returns (uint256 amount0, uint256 amount1); function mint(address to) external returns (uint256 liquidity); - function getReserves() - external - view - returns ( - uint256 _reserve0, - uint256 _reserve1, - uint256 _blockTimestampLast - ); + function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _blockTimestampLast); function getAmountOut(uint256, address) external view returns (uint256); function skim(address to) external; - function initialize( - address _token0, - address _token1, - bool _stable - ) external; + function initialize(address _token0, address _token1, bool _stable) external; function reserve0CumulativeLast() external view returns (uint256); @@ -101,24 +65,14 @@ interface IVeloPair { function currentCumulativePrices() external view - returns ( - uint256 reserve0Cumulative, - uint256 reserve1Cumulative, - uint256 blockTimestamp - ); - - function prices( - address tokenIn, - uint256 amountIn, - uint256 points - ) external view returns (uint256[] memory); - - function sample( - address tokenIn, - uint256 amountIn, - uint256 points, - uint256 window - ) external view returns (uint256[] memory); + returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + + function prices(address tokenIn, uint256 amountIn, uint256 points) external view returns (uint256[] memory); + + function sample(address tokenIn, uint256 amountIn, uint256 points, uint256 window) + external + view + returns (uint256[] memory); function observationLength() external view returns (uint256); } diff --git a/test/OraclesTest.t.sol b/test/OraclesTest.t.sol index f323a35..7d7a7b3 100644 --- a/test/OraclesTest.t.sol +++ b/test/OraclesTest.t.sol @@ -3,15 +3,12 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "src/ReaperStrategyStabilityPool.sol"; -// import "tarot-oracle/TarotPriceOracleVolatile.sol"; import "vault-v2/ReaperVaultV2.sol"; import {IERC20Upgradeable} from "oz-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; import {IERC20} from "oz/token/ERC20/IERC20.sol"; import {ReaperSwapper, MinAmountOutData, MinAmountOutKind} from "vault-v2/ReaperSwapper.sol"; import {IVeloRouter} from "vault-v2/interfaces/IVeloRouter.sol"; -// import "tarot-oracle/interfaces/IVeloPair.sol"; -import {Pool} from "tarot-oracle/toWatch/Pool.sol"; contract TarotOracleTest is Test { uint256 FORK_BLOCK = 115641661; @@ -82,7 +79,6 @@ contract TarotOracleTest is Test { // TarotPriceOracleVolatile tarot; function setUp() public { - Pool pool = new Pool(); // address[] memory strategists = new address[](1); // strategists[0] = makeAddr("strategist1"); //0x95885Af5492195F0754bE71AD1545Fe81364E531 @@ -156,10 +152,10 @@ contract TarotOracleTest is Test { ); } - function testMultipleOracles(uint128 baseAmount, uint32 period) public { + function testMultipleOracles_Positive(uint128 baseAmount, uint32 period) public { baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); period = uint32(bound(period, 2 days, 9 days)); - uint32 tolerance = 5000; + uint32 tolerance = 500; // console2.log("1. Pool address: ", veloUsdcErnPool); // console2.log("1. USDC address: ", usdcAddress); // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 2); @@ -176,11 +172,32 @@ contract TarotOracleTest is Test { uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, baseAmount, period, tolerance); // console2.log("1.Price All: ", wrappedProxy.getErnAmountForUsdcAll(baseAmount, period)); console2.log("2.Price All: ", finalPrice); - uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 100_000; - uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 100_000; + uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 10_000; + uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 10_000; assert(finalPrice < highBoundary && finalPrice > lowBoundary); } + function testMultipleOracles_Negative(uint128 baseAmount, uint32 period) public { + baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); + period = uint32(bound(period, 2 days, 9 days)); + uint32 tolerance = 300; + // console2.log("1. Pool address: ", veloUsdcErnPool); + // console2.log("1. USDC address: ", usdcAddress); + // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 2); + // console2.log("1. Price Velo: ", prices[0]); + uint256[] memory prices = new uint256[](6); + prices[0] = wrappedProxy.getErnAmountForUsdcVelo(baseAmount, period); + prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); + prices[2] = 1059945924123 * baseAmount; + prices[3] = 959945924123 * baseAmount; + prices[4] = 1009945924123 * baseAmount; + prices[5] = 0 * baseAmount; + console2.log("Price Velo: ", prices[0]); + console2.log("Price UniV3: ", prices[1]); + vm.expectRevert(bytes4(keccak256("StabilityPool__CouldntDetermineMeanPrice()"))); + uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, baseAmount, period, tolerance); + } + function testSeparaeteOracles() public { console2.log("Velo 1 WETH = %d ERN", wrappedProxy.getErnAmountForWethVelo(1 ether, 2 days)); console2.log("Chainlink 1 WETH = %d USDC", wrappedProxy.getUsdcAmountForWethUsingPriceFeeds()); diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol index baf2133..85f2f0c 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -787,12 +787,12 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("poolBalanceBefore: ", poolBalanceBefore); console.log("poolBalanceAfter: ", poolBalanceAfter); - uint32 currentUniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod(); + uint32 currentTwapPeriod = wrappedProxy.twapPeriod(); address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); uint256 priceQuote = IStaticOracle(uniV3TWAP).quoteSpecificPoolsWithTimePeriod( - uint128(usdcAmount), usdcAddress, wantAddress, pools, currentUniV3TWAPPeriod + uint128(usdcAmount), usdcAddress, wantAddress, pools, currentTwapPeriod ); // Values should be the same because the usdc balance will be valued // using the Velo TWAP @@ -883,7 +883,7 @@ contract ReaperStrategyStabilityPoolTest is Test { address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); - uint32 twapPeriod = wrappedProxy.uniV3TWAPPeriod(); + uint32 twapPeriod = wrappedProxy.twapPeriod(); console.log("twapPeriod: ", twapPeriod); uint256 ernAmount = IStaticOracle(uniV3TWAP).quoteSpecificPoolsWithTimePeriod( uint128(usdcAmount), usdcAddress, wantAddress, pools, twapPeriod @@ -1113,30 +1113,30 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("priceQuoteSpot1: ", priceQuoteSpot); } - function testUpdateUniV3TWAPPeriod() public { + function testUpdateTwapPeriod() public { uint32 period = 36000; - wrappedProxy.updateUniV3TWAPPeriod(period); + wrappedProxy.updateTwapPeriod(period); (uint32 earliestObservationTimestamp,,,) = IUniswapV3Pool(uniV3UsdcErnPool).observations(0); uint32 currentTimeStamp = uint32(block.timestamp); uint32 timeDifference = currentTimeStamp - earliestObservationTimestamp; period = timeDifference; - wrappedProxy.updateUniV3TWAPPeriod(period); + wrappedProxy.updateTwapPeriod(period); period += 1; console.log("period: ", period); vm.expectRevert(bytes("OLD")); - wrappedProxy.updateUniV3TWAPPeriod(period); + wrappedProxy.updateTwapPeriod(period); period = type(uint32).max; vm.expectRevert(bytes("OLD")); - wrappedProxy.updateUniV3TWAPPeriod(period); + wrappedProxy.updateTwapPeriod(period); } - function testChangeTWAPPeriod() public { + function testChangeTwapPeriod() public { uint32 oldPeriod = 36000; - wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); + wrappedProxy.updateTwapPeriod(oldPeriod); uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); @@ -1152,18 +1152,18 @@ contract ReaperStrategyStabilityPoolTest is Test { uint32 newPeriod = 7200; // DEFAULT_ADMIN_ROLE is allowed regardless - wrappedProxy.updateUniV3TWAPPeriod(newPeriod); - wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); + wrappedProxy.updateTwapPeriod(newPeriod); + wrappedProxy.updateTwapPeriod(oldPeriod); // 0 collateral value is allowed regardless vm.startPrank(adminAddress); - wrappedProxy.updateUniV3TWAPPeriod(newPeriod); - wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); + wrappedProxy.updateTwapPeriod(newPeriod); + wrappedProxy.updateTwapPeriod(oldPeriod); // ADMIN role with collateral is blocked deal({token: usdcAddress, to: address(wrappedProxy), give: 1_000_000}); vm.expectRevert("TWAP duration change would change price"); - wrappedProxy.updateUniV3TWAPPeriod(newPeriod); + wrappedProxy.updateTwapPeriod(newPeriod); vm.stopPrank(); - wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); + wrappedProxy.updateTwapPeriod(oldPeriod); } function liquidateTroves(address asset) internal { From 0fb2f31a0929c51eb6375802daec256585253e14 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 11 Feb 2024 12:29:25 +0100 Subject: [PATCH 03/18] New velo twap added and improved interfaces --- src/ReaperStrategyStabilityPool.sol | 205 +++++++++++++++++-------- src/interfaces/IVeloPair.sol | 31 +--- test/OraclesTest.t.sol | 193 ++++++++++++----------- test/ReaperStrategyStabilityPool.t.sol | 32 ++-- 4 files changed, 262 insertions(+), 199 deletions(-) diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index 9e2a092..87e017a 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -25,6 +25,15 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { using ReaperMathUtils for uint256; using SafeERC20Upgradeable for IERC20MetadataUpgradeable; + // constants + uint256 constant MIN_VELO_PRICE_UPDATE_INTERVAL = 1 days; + uint256 constant MIN_ALLOWED_PERIOD_VELO = 2 days; + uint256 constant MIN_NR_OF_POINTS = 1; + uint256 constant MIN_LENGTH_OF_WINDOW = 1; + uint256 constant MAXIMUM_ALLOWED_RELATIVE_CHANGE = 300; // 3% + uint256 constant PRICE_VALIDITY_THRESHOLD = 5000; // 50% + uint32 constant MAX_ALLOWED_TOLERANCE = 200; // 2% + // 3rd-party contract addresses IStabilityPool public stabilityPool; IPriceFeed public priceFeed; @@ -39,7 +48,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { uint256 public constant ETHOS_DECIMALS = 18; // Decimals used by ETHOS uint256 public ernMinAmountOutBPS; // The max allowed slippage when trading in to ERN uint256 public compoundingFeeMarginBPS; // How much collateral value is lowered to account for the costs of swapping - uint32 public twapPeriod; // How many seconds the uniV3 TWAP will look at + uint32 public uniV3TWAPPeriod; // How many seconds the uniV3 TWAP will look at + uint32 public veloTWAPPeriod; // How many seconds the velo TWAP will look at ExchangeType public usdcToErnExchange; // Controls which exchange is used to swap USDC to ERN bool public shouldOverrideHarvestBlock; // If reverts on TWAP out of normal range should be ignored uint256 acceptableTWAPUpperBound; // The normal upper price for the TWAP, reverts harvest if above @@ -69,9 +79,9 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { error InvalidUsdcToErnTWAP(uint256 twapEnum); error TWAPOutsideAllowedRange(uint256 usdcPrice); error InvalidSwapStep(); - error StabilityPool__CouldntDetermineMeanPrice(); - error StabilityPool__WindowLongerThanOrZero(); - error StabilityPool__TooShortPeriod(); + error CouldntDetermineMeanPrice(); + error WindowLongerThanOrZero(); + error TooShortPeriod(); /** * @dev Initializes the strategy. Sets parameters, saves routes, and gives allowances. @@ -122,8 +132,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { veloUsdcErnPool = IVeloPair(_pools.veloUsdcErnPool); veloWethErnPool = IVeloPair(_pools.veloWethErnPool); compoundingFeeMarginBPS = 9950; - twapPeriod = 7200; // Question: Couldn't understand how it will work with function and twapPeriod as a 0 at the init, - // Can it be initialization of global variable instead of update function ? + updateUniV3TWAPPeriod(7200); + veloTWAPPeriod = 2 days; updateAcceptableTWAPBounds(980_000, 1_100_000); } @@ -187,10 +197,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { return; } uint128 ernAmount = 1 ether; // 1 ERN - address[] memory pools = new address[](1); - pools[0] = address(uniV3UsdcErnPool); - uint256 usdcAmount = - uniV3TWAP.quoteSpecificPoolsWithTimePeriod(ernAmount, want, address(usdc), pools, twapPeriod); + uint256 usdcAmount = getErnAmountForUsdcAll(ernAmount, MAX_ALLOWED_TOLERANCE); if (usdcAmount < acceptableTWAPLowerBound || usdcAmount > acceptableTWAPUpperBound) { revert TWAPOutsideAllowedRange(usdcAmount); @@ -334,7 +341,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { */ function _getErnAmountForUsdc(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - expectedErnAmount = getErnAmountForUsdcAll(uint128(_usdcAmount), twapPeriod); + expectedErnAmount = getErnAmountForUsdcUniV3(uint128(_usdcAmount), uniV3TWAPPeriod); } } @@ -352,23 +359,56 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { /** * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo TWAP. + * using the Velo TWAP + * @notice One sample with wide window (calculations are done inside sample function between two last priceCumulatives and timestamps). */ - function getErnAmountForUsdcVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - if (_period < 2 days) { - revert StabilityPool__TooShortPeriod(); + function getErnAmountForUsdcVeloWindow(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + if (_period < MIN_ALLOWED_PERIOD_VELO) { + revert TooShortPeriod(); } - address[] memory pools = new address[](1); - uint256 window = _period / 1 days; + uint256 window = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; if (window >= veloUsdcErnPool.observationLength() || window == 0) { - revert StabilityPool__WindowLongerThanOrZero(); + revert WindowLongerThanOrZero(); } - uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); - uint256[] memory quoteAmount = veloUsdcErnPool.sample(address(usdc), (10 ** usdc.decimals()), 1, window); + uint256[] memory quoteAmount = + veloUsdcErnPool.sample(address(usdc), (10 ** usdc.decimals()), MIN_NR_OF_POINTS, window); return (_baseAmount * quoteAmount[0] * (10 ** (wantDecimals - usdc.decimals())) / 1 ether); // better math } + /** + * @dev provides twap price with user configured granularity, up to the full window size + * + */ + function _quote(address tokenIn, uint256 amountIn, uint256 granularity) private view returns (uint256 amountOut) { + uint256[] memory _prices = veloUsdcErnPool.sample(tokenIn, amountIn, granularity, MIN_LENGTH_OF_WINDOW); + uint256 priceAverageCumulative; + uint256 _length = _prices.length; + for (uint256 i = 0; i < _length; i++) { + priceAverageCumulative += _prices[i]; + } + return priceAverageCumulative / granularity; + } + + /** + * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) + * using the Velo TWAP + * @notice Multiple samples with shortest window possible (calculations are the average of small samples calculated from shortest possible window). + */ + function getErnAmountForUsdcVeloPoints(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + if (_period < MIN_ALLOWED_PERIOD_VELO) { + revert TooShortPeriod(); + } + uint256 granuality = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; + if (granuality >= veloUsdcErnPool.observationLength() || granuality == 0) { + revert WindowLongerThanOrZero(); + } + uint256 quoteAmount = _quote(address(usdc), (10 ** usdc.decimals()), granuality); + uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); + + return (_baseAmount * quoteAmount * (10 ** (wantDecimals - usdc.decimals())) / 1 ether); // better math + } + function getUsdcAmountForWethUsingPriceFeeds() public returns (uint256) { uint256 tmpPrice = _getUSDEquivalentOfCollateralUsingPriceFeed(address(weth), 1 ether); return _getUsdcEquivalentOfUSD(tmpPrice); @@ -379,28 +419,28 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * using the Velo TWAP. */ function getErnAmountForWethVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - if (_period < 2 days) { - revert StabilityPool__TooShortPeriod(); + if (_period < MIN_ALLOWED_PERIOD_VELO) { + revert TooShortPeriod(); } address[] memory pools = new address[](1); - uint256 window = _period / 1 days; + uint256 window = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; if (window >= veloUsdcErnPool.observationLength() || window == 0) { - revert StabilityPool__WindowLongerThanOrZero(); + revert WindowLongerThanOrZero(); } - uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); uint256[] memory quoteAmount = veloWethErnPool.sample(address(weth), 1e18, 1, window); return (_baseAmount * quoteAmount[0] / 1 ether); // better math } /** * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo TWAP. + * using the Velo ERN/WETH TWAP and chainlink price feed. * * Math: * 1e18 wei - x ern * 1e18 wei - y usdc * ern = 1e18 wei / x => ern = y usdc / x + * @notice Due to price feeds interface this function cannot be viewable - it usage makes difficullties */ function getErnAmountForUsdcVeloWeth(uint128 _baseAmount, uint32 _period) public returns (uint256) { uint256 usdcAmountForWethPriceFeeds = (getUsdcAmountForWethUsingPriceFeeds() * _baseAmount); @@ -410,73 +450,86 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { return ((usdcAmountForWethPriceFeeds * 1 ether * 10 ** usdc.decimals()) / veloAmountForWethVelo); } + /** + * @dev Function consumes array of {prices} and check them against one reference price pointed by {idx}. + * If the prices are inside range specified in {tolerance}, function marks it in {indexes} array and increment {nrOfValidPrices}. + * @param prices - array of prices from oracles + * @param idx - array index at which the price will be taken as a reference + * @param tolerance - allowed tolerance of deviation + * @return indexes - indexes at which the prices are in range + * @return nrOfValidPrices - number of prices which are in range + */ function getInfoAboutTwapOracles(uint256[] memory prices, uint32 idx, uint32 tolerance) private - view - returns (bool[] memory indexes, uint32 validAmount) + pure + returns (bool[] memory, uint32) { - indexes = new bool[](prices.length); + require(idx <= prices.length); + bool[] memory indexes = new bool[](prices.length); uint256 referencePrice = prices[idx]; + uint32 nrOfValidPrices = 1; indexes[idx] = true; - validAmount = 1; + /* For loop assumptions: + - {cnt} shall start with value greater than passed {idx} but cannot be greater than length of array of prices + - loop ends when {cnt} reaches value of {idx} - it must happen as we are iterating over finite number of values (modulo {price.length}) + - {cnt} increments by one and starts from 0 when reaches {prices.length} + */ for (uint32 cnt = (idx + 1) % uint32(prices.length); cnt != idx; cnt = (cnt + 1) % uint32(prices.length)) { - console2.log("Reference price: ", referencePrice); - console2.log("vs prices[cnt]: ", prices[cnt]); - // Question: Can be acceptableTWAPLowerBound and acceptableTWAPUpperBound be applied here ? if ( referencePrice + (referencePrice * tolerance / PERCENT_DIVISOR) >= prices[cnt] && referencePrice - (referencePrice * tolerance / PERCENT_DIVISOR) <= prices[cnt] ) { - validAmount++; + /* The price is inside the range - store index and increment number of valid prices */ + nrOfValidPrices++; indexes[cnt] = true; } } + return (indexes, nrOfValidPrices); } /** * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) * using the all possible oracles. */ - function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _period) public view returns (uint256) { + function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _tolerance) public view returns (uint256) { uint256[] memory _prices = new uint256[](2); - _prices[0] = getErnAmountForUsdcVelo(_baseAmount, _period); - _prices[1] = getErnAmountForUsdcUniV3(_baseAmount, _period); + _prices[0] = getErnAmountForUsdcVeloPoints(_baseAmount, veloTWAPPeriod); + _prices[1] = getErnAmountForUsdcUniV3(_baseAmount, uniV3TWAPPeriod); //_prices[2] = getErnAmountForUsdcVeloWeth(_baseAmount, _period); // This function is not view - return getErnAmountForUsdcAll(_prices, _baseAmount, _period, 200); + return getErnAmountForUsdcAll(_prices, _tolerance); } /** * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) * using the all possible oracles. */ - function getErnAmountForUsdcAll(uint256[] memory _prices, uint128 _baseAmount, uint32 _period, uint32 _tolerance) - public - view - returns (uint256) - { + function getErnAmountForUsdcAll(uint256[] memory _prices, uint32 _tolerance) public view returns (uint256) { uint256 meanTwap = 0; - - for (uint32 idx = 0; idx < _prices.length; idx++) { - (bool[] memory indexes, uint32 validAmount) = getInfoAboutTwapOracles(_prices, idx, _tolerance); - console2.log("Idx: ", idx); - console2.log("Valid amount: ", validAmount); - // Amount of valid prices must be greater than 50% - if (validAmount > (_prices.length / 2)) { - uint256 sumOfPrices = 0; - for (uint32 cnt = 0; cnt < indexes.length; cnt++) { - if (indexes[cnt] != false) { - sumOfPrices += _prices[cnt]; - console2.log("Sum of prices: ", sumOfPrices); + if (_prices.length > 1) { + for (uint32 idx = 0; idx < _prices.length; idx++) { + (bool[] memory indexes, uint32 nrOfValidPrices) = getInfoAboutTwapOracles(_prices, idx, _tolerance); + // Amount of valid prices must be greater than {PRICE_VALIDITY_THRESHOLD}% + if (nrOfValidPrices > (_prices.length * PRICE_VALIDITY_THRESHOLD / PERCENT_DIVISOR)) { + uint256 sumOfPrices = 0; + // Iterate through {indexes} array to see which index of price array shall be taken into {meanTwap} calculations + for (uint32 cnt = 0; cnt < indexes.length; cnt++) { + if (indexes[cnt] != false) { + sumOfPrices += _prices[cnt]; + } } + meanTwap = sumOfPrices / nrOfValidPrices; + break; } - meanTwap = sumOfPrices / validAmount; - break; } - } - if (meanTwap == 0) { - revert StabilityPool__CouldntDetermineMeanPrice(); + if (meanTwap == 0) { + revert CouldntDetermineMeanPrice(); + } + } else if (_prices.length == 1) { + meanTwap = _prices[0]; + } else { + revert CouldntDetermineMeanPrice(); } return meanTwap; } @@ -627,14 +680,34 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * roles a check is performed to see if changing duration would effect the price * past some threshold, if the strategy holds collateral value (priced by TWAP). */ - function updateTwapPeriod(uint32 _twapPeriod) public { + function updateUniV3TWAPPeriod(uint32 _uniV3TWAPPeriod) public { + _atLeastRole(ADMIN); + require(_uniV3TWAPPeriod >= 7200, "TWAP period is too short"); + + uint256 newErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), _uniV3TWAPPeriod); + uint256 oldErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), uniV3TWAPPeriod); + + uniV3TWAPPeriod = _uniV3TWAPPeriod; + + if (_hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) return; + + uint256 ernCollateralValue = getERNValueOfCollateralGainUsingPriceFeed(); + + if (ernCollateralValue != 0) { + uint256 difference = newErnAmount > oldErnAmount ? newErnAmount - oldErnAmount : oldErnAmount - newErnAmount; + uint256 relativeChange = difference * PERCENT_DIVISOR / oldErnAmount; + require(relativeChange < MAXIMUM_ALLOWED_RELATIVE_CHANGE, "TWAP duration change would change price"); + } + } + + function updateVeloTWAPPeriod(uint32 _veloTWAPPeriod) public { _atLeastRole(ADMIN); - require(_twapPeriod >= 7200, "TWAP period is too short"); + require(_veloTWAPPeriod >= MIN_ALLOWED_PERIOD_VELO, "TWAP period is too short"); - uint256 newErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), _twapPeriod); - uint256 oldErnAmount = getErnAmountForUsdcAll(uint128(1_000_000), twapPeriod); + uint256 newErnAmount = getErnAmountForUsdcVeloPoints(uint128(1_000_000), _veloTWAPPeriod); + uint256 oldErnAmount = getErnAmountForUsdcVeloPoints(uint128(1_000_000), veloTWAPPeriod); - twapPeriod = _twapPeriod; + veloTWAPPeriod = _veloTWAPPeriod; if (_hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) return; @@ -643,7 +716,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { if (ernCollateralValue != 0) { uint256 difference = newErnAmount > oldErnAmount ? newErnAmount - oldErnAmount : oldErnAmount - newErnAmount; uint256 relativeChange = difference * PERCENT_DIVISOR / oldErnAmount; - require(relativeChange < 300, "TWAP duration change would change price"); + require(relativeChange < MAXIMUM_ALLOWED_RELATIVE_CHANGE, "TWAP duration change would change price"); } } diff --git a/src/interfaces/IVeloPair.sol b/src/interfaces/IVeloPair.sol index 193bb86..cda893d 100644 --- a/src/interfaces/IVeloPair.sol +++ b/src/interfaces/IVeloPair.sol @@ -29,35 +29,6 @@ interface IVeloPair { event Sync(uint256 reserve0, uint256 reserve1); event Claim(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1); - function metadata() - external - view - returns (uint256 dec0, uint256 dec1, uint256 r0, uint256 r1, bool st, address t0, address t1); - - function claimFees() external returns (uint256, uint256); - - function tokens() external view returns (address, address); - - function token0() external view returns (address); - - function token1() external view returns (address); - - function stable() external view returns (bool); - - function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; - - function burn(address to) external returns (uint256 amount0, uint256 amount1); - - function mint(address to) external returns (uint256 liquidity); - - function getReserves() external view returns (uint256 _reserve0, uint256 _reserve1, uint256 _blockTimestampLast); - - function getAmountOut(uint256, address) external view returns (uint256); - - function skim(address to) external; - - function initialize(address _token0, address _token1, bool _stable) external; - function reserve0CumulativeLast() external view returns (uint256); function reserve1CumulativeLast() external view returns (uint256); @@ -75,4 +46,6 @@ interface IVeloPair { returns (uint256[] memory); function observationLength() external view returns (uint256); + + function sync() external; } diff --git a/test/OraclesTest.t.sol b/test/OraclesTest.t.sol index 7d7a7b3..e9802b0 100644 --- a/test/OraclesTest.t.sol +++ b/test/OraclesTest.t.sol @@ -76,25 +76,13 @@ contract TarotOracleTest is Test { ReaperSwapper reaperSwapper; ReaperStrategyStabilityPool implementation; ReaperStrategyStabilityPool wrappedProxy; - // TarotPriceOracleVolatile tarot; function setUp() public { - // address[] memory strategists = new address[](1); - // strategists[0] = makeAddr("strategist1"); - //0x95885Af5492195F0754bE71AD1545Fe81364E531 - // vm.etch(0x95885Af5492195F0754bE71AD1545Fe81364E531, address(pool).code); - // vm.etch(veloUsdcErnPool, address(pool).code); // Forking string memory rpc = vm.envString("RPC"); optimismFork = vm.createSelectFork(rpc, FORK_BLOCK); assertEq(vm.activeFork(), optimismFork); - // Deploying - // tarot = new TarotPriceOracleVolatile(); - // tarot.initialize(veloUsdcErnPool); - // veloWethErnPool = IVeloRouter(veloRouter).poolFor(wethAddress, ernAddress, false, veloFactoryV2Default); - // tarot.initialize(veloWethErnPool); - /* Reaper deployment and configuration */ ERC1967Proxy tmpProxy; reaperSwapper = new ReaperSwapper(); @@ -152,16 +140,26 @@ contract TarotOracleTest is Test { ); } + function testMultipleOracles_PositiveOnePrice(uint128 baseAmount, uint32 period) public { + baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); + period = uint32(bound(period, 2 days, 9 days)); + uint32 tolerance = 500; + uint256[] memory prices = new uint256[](1); + prices[0] = wrappedProxy.getErnAmountForUsdcVeloPoints(baseAmount, period); + console2.log("Price Velo: ", prices[0]); + uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); + console2.log("2.Price All: ", finalPrice); + uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 10_000; + uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 10_000; + assert(finalPrice < highBoundary && finalPrice > lowBoundary); + } + function testMultipleOracles_Positive(uint128 baseAmount, uint32 period) public { baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); period = uint32(bound(period, 2 days, 9 days)); uint32 tolerance = 500; - // console2.log("1. Pool address: ", veloUsdcErnPool); - // console2.log("1. USDC address: ", usdcAddress); - // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 2); - // console2.log("1. Price Velo: ", prices[0]); uint256[] memory prices = new uint256[](6); - prices[0] = wrappedProxy.getErnAmountForUsdcVelo(baseAmount, period); + prices[0] = wrappedProxy.getErnAmountForUsdcVeloWindow(baseAmount, period); prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); prices[2] = 1059945924123 * baseAmount; prices[3] = 959945924123 * baseAmount; @@ -169,24 +167,23 @@ contract TarotOracleTest is Test { prices[5] = 0 * baseAmount; console2.log("Price Velo: ", prices[0]); console2.log("Price UniV3: ", prices[1]); - uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, baseAmount, period, tolerance); - // console2.log("1.Price All: ", wrappedProxy.getErnAmountForUsdcAll(baseAmount, period)); - console2.log("2.Price All: ", finalPrice); + uint256 finalPrice_1 = wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); + console2.log("2.Price_1 All: ", finalPrice_1); + + uint256 finalPrice_2 = wrappedProxy.getErnAmountForUsdcAll(baseAmount, tolerance); + console2.log("2.Price_2 All: ", finalPrice_2); uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 10_000; uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 10_000; - assert(finalPrice < highBoundary && finalPrice > lowBoundary); + assert(finalPrice_1 < highBoundary && finalPrice_1 > lowBoundary); + assert(finalPrice_2 < highBoundary && finalPrice_2 > lowBoundary); } function testMultipleOracles_Negative(uint128 baseAmount, uint32 period) public { baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); period = uint32(bound(period, 2 days, 9 days)); - uint32 tolerance = 300; - // console2.log("1. Pool address: ", veloUsdcErnPool); - // console2.log("1. USDC address: ", usdcAddress); - // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 2); - // console2.log("1. Price Velo: ", prices[0]); + uint32 tolerance = 200; // tolerance set to narrow values uint256[] memory prices = new uint256[](6); - prices[0] = wrappedProxy.getErnAmountForUsdcVelo(baseAmount, period); + prices[0] = wrappedProxy.getErnAmountForUsdcVeloWindow(baseAmount, period); prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); prices[2] = 1059945924123 * baseAmount; prices[3] = 959945924123 * baseAmount; @@ -194,79 +191,99 @@ contract TarotOracleTest is Test { prices[5] = 0 * baseAmount; console2.log("Price Velo: ", prices[0]); console2.log("Price UniV3: ", prices[1]); - vm.expectRevert(bytes4(keccak256("StabilityPool__CouldntDetermineMeanPrice()"))); - uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, baseAmount, period, tolerance); + vm.expectRevert(bytes4(keccak256("CouldntDetermineMeanPrice()"))); + wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); } function testSeparaeteOracles() public { console2.log("Velo 1 WETH = %d ERN", wrappedProxy.getErnAmountForWethVelo(1 ether, 2 days)); console2.log("Chainlink 1 WETH = %d USDC", wrappedProxy.getUsdcAmountForWethUsingPriceFeeds()); - console2.log(wrappedProxy.getErnAmountForUsdcVelo(1 ether, 2 days)); + console2.log(wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days)); + console2.log(wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days)); console2.log(wrappedProxy.getErnAmountForUsdcUniV3(1 ether, 2 days)); console2.log(wrappedProxy.getErnAmountForUsdcVeloWeth(1 ether, 2 days)); } - function testTarotOracle_Weth() public { - uint256 amount = 20000e18; - console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - console2.log("Length WETH/ERN: ", IVeloPair(veloWethErnPool).observationLength()); + function testOraclesOnPriceManipulationsComparison_ErnWhale() public { + uint256 amount = IERC20(ernAddress).balanceOf(ernWhale); MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); + + uint256 result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("1. Result before swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("1. Result1 before swap: ", result); + vm.startPrank(ernWhale); + console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); + console2.log("1. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); + IERC20Upgradeable(ernAddress).approve(address(reaperSwapper), amount); + reaperSwapper.swapVelo(ernAddress, usdcAddress, amount, minAmountOutData, address(veloRouter)); + console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); + console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); vm.warp(block.timestamp + 1 days); - uint256[] memory prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 10); - console2.log("1. Sample result: ", prices[0]); - prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 100); - console2.log("2. Sample result: ", prices[0]); - prices = IVeloPair(veloWethErnPool).sample(wethAddress, 1e18, 1, 1000); - console2.log("3. Sample result: ", prices[0]); - prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 10); - console2.log("2. Sample result: ", prices[0]); - console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - console2.log("Length WETH/ERN: ", IVeloPair(veloWethErnPool).observationLength()); - // prices = IVeloPair(veloUsdcErnPool).sample(usdcAddress, 1e6, 1, 1000); - console2.log("3. Sample result: ", prices[0]); + IVeloPair(veloUsdcErnPool).sync(); // imitates vm.warp(block.timestamp + 1 days); + result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("2. Result (points) after 1 day from swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("2. Result (window) after 1 day from swap: ", result); + vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("2. Result (points) after 1 week from swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("2. Result (window) after 1 week from swap: ", result); } - // function testWindows() public { - // string memory rpc = vm.envString("RPC"); - // console2.log(1 seconds, 1 hours, 1 days); - // optimismFork = vm.createSelectFork(rpc, 115651661); - // console2.log("Length USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - - // optimismFork = vm.createSelectFork(rpc, 115651661 - 30 minutes); - // console2.log("Length 30 minutes USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - - // optimismFork = vm.createSelectFork(rpc, 115651661 - 1 hours); - // console2.log("Length 1 hour USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // optimismFork = vm.createSelectFork(rpc, 115651661 - 2 hours); - // console2.log("Length 2 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // optimismFork = vm.createSelectFork(rpc, 115651661 - 3 hours); - // console2.log("Length 3 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // optimismFork = vm.createSelectFork(rpc, 115651661 - 4 hours); - // console2.log("Length 4 hours USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // optimismFork = vm.createSelectFork(rpc, 115651661 - 1 days); - // console2.log("Length -1 day USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // optimismFork = vm.createSelectFork(rpc, 115651661 - 10 days); - // console2.log("Length -2 days USDC/ERN: ", IVeloPair(veloUsdcErnPool).observationLength()); - // } + function testOraclesOnPriceManipulationsComparison_UsdcWhale() public { + uint256 amount = IERC20(usdcAddress).balanceOf(usdcWhale); + MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); - // function testTarotOracle() public { - // uint256 amount = 20000e18; - // MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); - // vm.warp(block.timestamp + 1 days); - // (uint256 result, uint256 timestamp) = tarot.getResult(veloUsdcErnPool); - // console2.log("1. Result before swap: ", result); - // vm.startPrank(ernWhale); - // console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); - // console2.log("1. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); - // IERC20Upgradeable(ernAddress).approve(address(reaperSwapper), amount); - // reaperSwapper.swapVelo(ernAddress, usdcAddress, amount, minAmountOutData, address(veloRouter)); - // console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); - // console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); - // (result, timestamp) = tarot.getResult(veloUsdcErnPool); - // console2.log("2. Result after swap: ", result); - // vm.stopPrank(); - // vm.warp(block.timestamp + 1 weeks); - // (result, timestamp) = tarot.getResult(veloUsdcErnPool); - // console2.log("3. Result after 1 week: ", result); - // } + uint256 result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("1. Result before swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("1. Result1 before swap: ", result); + vm.startPrank(usdcWhale); + console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(usdcWhale)); + console2.log("1. Whale balance of USDC", amount * 1e12); + IERC20Upgradeable(usdcAddress).approve(address(reaperSwapper), amount); + reaperSwapper.swapVelo(usdcAddress, ernAddress, amount, minAmountOutData, address(veloRouter)); + console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); + console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); + vm.warp(block.timestamp + 1 days); + IVeloPair(veloUsdcErnPool).sync(); // imitates vm.warp(block.timestamp + 1 days); + result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("2. Result (points) after 1 day from swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("2. Result (window) after 1 day from swap: ", result); + vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("2. Result (points) after 1 week from swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("2. Result (window) after 1 week from swap: ", result); + vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + IVeloPair(veloUsdcErnPool).sync(); + result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); + console2.log("2. Result (points) after 2 weeks from swap: ", result); + result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); + console2.log("2. Result (window) after 2 weeks from swap: ", result); + } } diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol index 85f2f0c..641d2f9 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -787,7 +787,7 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("poolBalanceBefore: ", poolBalanceBefore); console.log("poolBalanceAfter: ", poolBalanceAfter); - uint32 currentTwapPeriod = wrappedProxy.twapPeriod(); + uint32 currentTwapPeriod = wrappedProxy.uniV3TWAPPeriod(); address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); @@ -883,10 +883,10 @@ contract ReaperStrategyStabilityPoolTest is Test { address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); - uint32 twapPeriod = wrappedProxy.twapPeriod(); - console.log("twapPeriod: ", twapPeriod); + uint32 uniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod(); + console.log("uniV3TWAPPeriod: ", uniV3TWAPPeriod); uint256 ernAmount = IStaticOracle(uniV3TWAP).quoteSpecificPoolsWithTimePeriod( - uint128(usdcAmount), usdcAddress, wantAddress, pools, twapPeriod + uint128(usdcAmount), usdcAddress, wantAddress, pools, uniV3TWAPPeriod ); uint256 wantValueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); @@ -1113,30 +1113,30 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("priceQuoteSpot1: ", priceQuoteSpot); } - function testUpdateTwapPeriod() public { + function testUpdateUniV3TWAPPeriod() public { uint32 period = 36000; - wrappedProxy.updateTwapPeriod(period); + wrappedProxy.updateUniV3TWAPPeriod(period); (uint32 earliestObservationTimestamp,,,) = IUniswapV3Pool(uniV3UsdcErnPool).observations(0); uint32 currentTimeStamp = uint32(block.timestamp); uint32 timeDifference = currentTimeStamp - earliestObservationTimestamp; period = timeDifference; - wrappedProxy.updateTwapPeriod(period); + wrappedProxy.updateUniV3TWAPPeriod(period); period += 1; console.log("period: ", period); vm.expectRevert(bytes("OLD")); - wrappedProxy.updateTwapPeriod(period); + wrappedProxy.updateUniV3TWAPPeriod(period); period = type(uint32).max; vm.expectRevert(bytes("OLD")); - wrappedProxy.updateTwapPeriod(period); + wrappedProxy.updateUniV3TWAPPeriod(period); } function testChangeTwapPeriod() public { uint32 oldPeriod = 36000; - wrappedProxy.updateTwapPeriod(oldPeriod); + wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); @@ -1152,18 +1152,18 @@ contract ReaperStrategyStabilityPoolTest is Test { uint32 newPeriod = 7200; // DEFAULT_ADMIN_ROLE is allowed regardless - wrappedProxy.updateTwapPeriod(newPeriod); - wrappedProxy.updateTwapPeriod(oldPeriod); + wrappedProxy.updateUniV3TWAPPeriod(newPeriod); + wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); // 0 collateral value is allowed regardless vm.startPrank(adminAddress); - wrappedProxy.updateTwapPeriod(newPeriod); - wrappedProxy.updateTwapPeriod(oldPeriod); + wrappedProxy.updateUniV3TWAPPeriod(newPeriod); + wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); // ADMIN role with collateral is blocked deal({token: usdcAddress, to: address(wrappedProxy), give: 1_000_000}); vm.expectRevert("TWAP duration change would change price"); - wrappedProxy.updateTwapPeriod(newPeriod); + wrappedProxy.updateUniV3TWAPPeriod(newPeriod); vm.stopPrank(); - wrappedProxy.updateTwapPeriod(oldPeriod); + wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); } function liquidateTroves(address asset) internal { From 47e869694d45980d9294de583c3348c2b93e9dd4 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 11 Feb 2024 12:37:48 +0100 Subject: [PATCH 04/18] Disabled ReaperStrategyStabilityPool.t tests --- ...ilityPool.t.sol => ReaperStrategyStabilityPool.t.solTODO} | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename test/{ReaperStrategyStabilityPool.t.sol => ReaperStrategyStabilityPool.t.solTODO} (99%) diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.solTODO similarity index 99% rename from test/ReaperStrategyStabilityPool.t.sol rename to test/ReaperStrategyStabilityPool.t.solTODO index 641d2f9..63300a5 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.solTODO @@ -39,6 +39,7 @@ contract ReaperStrategyStabilityPoolTest is Test { address public uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address public uniV2Router = 0xbeeF000000000000000000000000000000000000; // Any non-0 address when UniV2 router does not exist address public veloUsdcErnPool = 0x605cCE502dEe6BD201b493782e351e645D44abBB; + address public veloWethErnPool = 0xFFf37730744930Cb61Be34c0014068F4f1eC28cF; address public uniV3UsdcErnPool = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; address public chainlinkUsdcOracle = 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3; address public uniV3TWAP = 0xB210CE856631EeEB767eFa666EC7C1C57738d438; @@ -113,7 +114,7 @@ contract ReaperStrategyStabilityPoolTest is Test { function setUp() public { // Forking string memory rpc = vm.envString("RPC"); - optimismFork = vm.createSelectFork(rpc, 115675719); + optimismFork = vm.createSelectFork(rpc, 115641661); assertEq(vm.activeFork(), optimismFork); // // Deploying stuff @@ -146,6 +147,7 @@ contract ReaperStrategyStabilityPoolTest is Test { pools.stabilityPool = stabilityPoolAddress; pools.uniV3UsdcErnPool = uniV3UsdcErnPool; pools.veloUsdcErnPool = veloUsdcErnPool; + pools.veloWethErnPool = veloWethErnPool; address[] memory usdcErnPath = new address[](2); usdcErnPath[0] = usdcAddress; @@ -154,6 +156,7 @@ contract ReaperStrategyStabilityPoolTest is Test { ReaperStrategyStabilityPool.Tokens memory tokens; tokens.want = wantAddress; tokens.usdc = usdcAddress; + tokens.weth = wethAddress; uint256 allowedTWAPDiscrepancy = 500; From 59cbf934ce891ae55a935ef7f241e909578b131a Mon Sep 17 00:00:00 2001 From: xRave110 Date: Sun, 11 Feb 2024 12:40:49 +0100 Subject: [PATCH 05/18] Cleaning --- .gitmodules | 3 --- lib/options-token | 1 - remappings.txt | 3 +-- src/ReaperStrategyStabilityPool.sol | 1 - 4 files changed, 1 insertion(+), 7 deletions(-) delete mode 160000 lib/options-token diff --git a/.gitmodules b/.gitmodules index bbb748b..81a3ec5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "lib/vault-v2"] path = lib/vault-v2 url = git@github.com:Byte-Masons/vault-v2.git -[submodule "lib/options-token"] - path = lib/options-token - url = https://github.com/Byte-Masons/options-token diff --git a/lib/options-token b/lib/options-token deleted file mode 160000 index 9aa8a91..0000000 --- a/lib/options-token +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9aa8a91e6788ada2354682e43e781e1c691af574 diff --git a/remappings.txt b/remappings.txt index 33d0d05..89de457 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,5 +3,4 @@ mixins/=lib/vault-v2/src/mixins/ ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/ forge-std/=lib/vault-v2/lib/forge-std/src/ oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/ -oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/ -tarot-oracle/=lib/tarot-price-oracle/contracts/ \ No newline at end of file +oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index 87e017a..bf05610 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; -import "forge-std/Test.sol"; import "vault-v2/interfaces/ISwapper.sol"; import {ReaperBaseStrategyv4} from "vault-v2/ReaperBaseStrategyv4.sol"; import {IStabilityPool} from "./interfaces/IStabilityPool.sol"; From fccc79f582a346992f751acd277507342a4162d3 Mon Sep 17 00:00:00 2001 From: xRave110 Date: Mon, 12 Feb 2024 07:34:51 +0100 Subject: [PATCH 06/18] Name refactor --- src/ReaperStrategyStabilityPool.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index bf05610..c35cd09 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -505,7 +505,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * using the all possible oracles. */ function getErnAmountForUsdcAll(uint256[] memory _prices, uint32 _tolerance) public view returns (uint256) { - uint256 meanTwap = 0; + uint256 averageTwap = 0; if (_prices.length > 1) { for (uint32 idx = 0; idx < _prices.length; idx++) { (bool[] memory indexes, uint32 nrOfValidPrices) = getInfoAboutTwapOracles(_prices, idx, _tolerance); @@ -518,19 +518,19 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { sumOfPrices += _prices[cnt]; } } - meanTwap = sumOfPrices / nrOfValidPrices; + averageTwap = sumOfPrices / nrOfValidPrices; break; } } - if (meanTwap == 0) { + if (averageTwap == 0) { revert CouldntDetermineMeanPrice(); } } else if (_prices.length == 1) { - meanTwap = _prices[0]; + averageTwap = _prices[0]; } else { revert CouldntDetermineMeanPrice(); } - return meanTwap; + return averageTwap; } /** From 4b68bc0a6f59fb170b3bef331b1651fe80516339 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:41:59 -0300 Subject: [PATCH 07/18] Create OracleAggregator --- .gitmodules | 9 +- lib/v3-core | 1 + remappings.txt | 11 +- src/OracleAggregator.sol | 119 ++++++++++++++++++++ src/ReaperStrategyStabilityPool.sol | 1 - src/interfaces/IVeloPair.sol | 21 +++- src/oracles/UniV3TwapMixin.sol | 43 ++++++++ src/oracles/VeloTwapMixin.sol | 165 ++++++++++++++++++++++++++++ test/OraclesTest.t.sol | 8 +- 9 files changed, 358 insertions(+), 20 deletions(-) create mode 160000 lib/v3-core create mode 100644 src/OracleAggregator.sol create mode 100644 src/oracles/UniV3TwapMixin.sol create mode 100644 src/oracles/VeloTwapMixin.sol diff --git a/.gitmodules b/.gitmodules index 81a3ec5..9e9a96b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ -[submodule "lib/vault-v2"] - path = lib/vault-v2 - url = git@github.com:Byte-Masons/vault-v2.git +[submodule "lib/vault-v2"] + path = lib/vault-v2 + url = git@github.com:Byte-Masons/vault-v2.git +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/Uniswap/v3-core diff --git a/lib/v3-core b/lib/v3-core new file mode 160000 index 0000000..6562c52 --- /dev/null +++ b/lib/v3-core @@ -0,0 +1 @@ +Subproject commit 6562c52e8f75f0c10f9deaf44861847585fc8129 diff --git a/remappings.txt b/remappings.txt index 89de457..9cf44fc 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,7 @@ -vault-v2/=lib/vault-v2/src/ -mixins/=lib/vault-v2/src/mixins/ -ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/ -forge-std/=lib/vault-v2/lib/forge-std/src/ -oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/ +vault-v2/=lib/vault-v2/src/ +univ3-core/=lib/v3-core/contracts/ +mixins/=lib/vault-v2/src/mixins/ +ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/ +forge-std/=lib/vault-v2/lib/forge-std/src/ +oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/ oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol new file mode 100644 index 0000000..da8497b --- /dev/null +++ b/src/OracleAggregator.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: BUSL1.1 + +pragma solidity ^0.8.0; + +import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; +import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; +import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; + +import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; + +// This contract contains tools for computing TWAP values and +// making averages between the results, for more reliable prices. +// Has support for multiple oracles +contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { + error Oracle_VeloOverflow(); + error Oracle_InvalidKind(); + error Oracle_OraclesUnreliable(); + + uint256 constant BPS = 1_000_000; + + enum OracleKind { + Velo, + UniV3, + PriceFeed + } + + struct OracleRoute { + Oracle[] oracles; + } + + struct Oracle { + address source; + address target; + uint256 period; + OracleKind kind; + } + + function getTwapPrices(OracleRoute[] memory oracles, uint256 baseAmount) public returns (uint256[] memory prices) { + prices = new uint256[](oracles.length); + for (uint256 i = 0; i < oracles.length; i++) { + prices[i] = getMultiHopPrice(oracles[i], baseAmount); + } + } + + function getMultiHopPrice(OracleRoute memory route, uint256 baseAmount) public returns (uint256 price) { + for (uint256 i = 0; i < route.oracles.length; i++) { + price = getPrice(route.oracles[i], baseAmount); + baseAmount = price; + } + } + + function getPrice(Oracle memory oracle, uint256 baseAmount) public returns (uint256 price) { + if (oracle.kind == OracleKind.Velo) { + return getVeloPrice(oracle.source, oracle.target, uint32(oracle.period), baseAmount); + } else if (oracle.kind == OracleKind.UniV3) { + return getUniV3Price(oracle.source, oracle.target, uint32(oracle.period), baseAmount); + } else if (oracle.kind == OracleKind.PriceFeed) { + return getPriceFeedPrice(oracle.source, oracle.target, baseAmount); + } else { + revert Oracle_InvalidKind(); + } + } + + function getMeanPrice(uint256[] memory prices, uint256 maxStdRelativeToMeanBPS, uint256 maxScoreBPS) + public + pure + returns (uint256) + { + (bool[] memory isInvalid) = getValidityByZScore(prices, maxScoreBPS); + (uint256 std, uint256 mean, uint256 nrOfValidPrices) = standardDeviation(prices, isInvalid); + if (std * BPS / mean > maxStdRelativeToMeanBPS) revert Oracle_OraclesUnreliable(); + if (nrOfValidPrices < 2) revert Oracle_OraclesUnreliable(); + return mean; + } + + function getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) + public + pure + returns (bool[] memory isInvalid) + { + (uint256 std, uint256 mean,) = standardDeviation(prices, new bool[](prices.length)); + isInvalid = new bool[](prices.length); + for (uint256 i = 0; i < prices.length; i++) { + int256 score = (int256(prices[i]) - int256(mean)) * int256(BPS) / int256(std); + isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); + } + } + + function standardDeviation(uint256[] memory prices, bool[] memory isInvalid) + public + pure + returns (uint256 std, uint256 mean, uint256 nrValidPrices) + { + uint256 sum = 0; + for (uint256 i = 0; i < prices.length; i++) { + if (!isInvalid[i]) { + sum += prices[i]; + nrValidPrices++; + } + } + mean = sum / nrValidPrices; + uint256[] memory deviationsSq = new uint256[](nrValidPrices); + for (uint256 i = 0; i < nrValidPrices; i++) { + int256 deviation = int256(prices[i]) - int256(mean); + deviationsSq[i] = uint256(deviation * deviation); + } + uint256 sumDeviationsSq = 0; + for (uint256 i = 0; i < deviationsSq.length; i++) { + sumDeviationsSq += deviationsSq[i]; + } + std = MathUpgradeable.sqrt(sumDeviationsSq / nrValidPrices, MathUpgradeable.Rounding.Up); + } + + function getPriceFeedPrice(address source, address target, uint256 baseAmount) public returns (uint256 price) { + return IPriceFeed(source).fetchPrice(target) * baseAmount; + } + + uint256[50] private __gap; +} diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index c35cd09..873c412 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -19,7 +19,6 @@ import {IVeloPair} from "./interfaces/IVeloPair.sol"; /** * @dev Strategy to compound rewards and liquidation collateral gains in the Ethos stability pool */ - contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { using ReaperMathUtils for uint256; using SafeERC20Upgradeable for IERC20MetadataUpgradeable; diff --git a/src/interfaces/IVeloPair.sol b/src/interfaces/IVeloPair.sol index cda893d..c936553 100644 --- a/src/interfaces/IVeloPair.sol +++ b/src/interfaces/IVeloPair.sol @@ -1,6 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +struct Cumulatives { + uint256 reserve0Cumulative; + uint256 reserve1Cumulative; + uint256 blockTimestamp; +} + interface IVeloPair { error DepositsNotEqual(); error BelowMinimumK(); @@ -33,10 +39,9 @@ interface IVeloPair { function reserve1CumulativeLast() external view returns (uint256); - function currentCumulativePrices() - external - view - returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + function currentCumulativePrices() external view returns (Cumulatives memory); + + function observations(uint256 index) external view returns (uint256, uint256, uint256); function prices(address tokenIn, uint256 amountIn, uint256 points) external view returns (uint256[] memory); @@ -45,7 +50,15 @@ interface IVeloPair { view returns (uint256[] memory); + function tokens() external view returns (address, address); + + function stable() external view returns (bool); + function observationLength() external view returns (uint256); function sync() external; + + function token0() external view returns (address); + + function token1() external view returns (address); } diff --git a/src/oracles/UniV3TwapMixin.sol b/src/oracles/UniV3TwapMixin.sol new file mode 100644 index 0000000..29ace79 --- /dev/null +++ b/src/oracles/UniV3TwapMixin.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL1.1 + +pragma solidity ^0.8.0; + +import {TickMath} from "univ3-core/libraries/TickMath.sol"; +import {FullMath} from "univ3-core/libraries/FullMath.sol"; +import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; + +contract UniV3TwapMixin { + function getUniV3Price(address source, address targetToken, uint32 period, uint256 baseAmount) + public + view + returns (uint256 price) + { + require(period != 0, "BP"); + + uint32[] memory secondsAgos = new uint32[](2); + secondsAgos[0] = period; + secondsAgos[1] = 0; + + (int56[] memory tickCumulatives,) = IUniswapV3Pool(source).observe(secondsAgos); + + int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; + int24 tick = int24(tickCumulativesDelta / int56(int32(period))); + uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); + + (address token0, address token1) = (IUniswapV3Pool(source).token0(), IUniswapV3Pool(source).token1()); + address baseToken = token0 < token1 ? token0 : token1; + + // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself + if (sqrtRatioX96 <= type(uint128).max) { + uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; + price = baseToken < targetToken + ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192) + : FullMath.mulDiv(1 << 192, baseAmount, ratioX192); + } else { + uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); + price = baseToken < targetToken + ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128) + : FullMath.mulDiv(1 << 128, baseAmount, ratioX128); + } + } +} diff --git a/src/oracles/VeloTwapMixin.sol b/src/oracles/VeloTwapMixin.sol new file mode 100644 index 0000000..55e2bcc --- /dev/null +++ b/src/oracles/VeloTwapMixin.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BUSL1.1 + +pragma solidity ^0.8.0; + +import {IVeloPair, Cumulatives} from "../interfaces/IVeloPair.sol"; +import {ERC20} from "oz/token/ERC20/ERC20.sol"; +import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; + +contract VeloTwapMixin { + uint256 constant VELO_OBSERVATION_PERIOD = 1800; + + function getVeloPrice(address source, address target, uint32 period, uint256 baseAmount) + public + view + returns (uint256) + { + IVeloPair pair = IVeloPair(source); + Cumulatives memory current = pair.currentCumulativePrices(); + Cumulatives memory last; + uint256 observationLength = pair.observationLength(); + + uint256 time; + + // avoid stack too deep + { + uint256 maxTimestampRequired = current.blockTimestamp - period; + // the minimum amount of observations the pair must have registered in the period of the query. + // the actual amount of observations since (block.timestamp - period) is likely to be smaller + uint256 minObservationsPassed = MathUpgradeable.ceilDiv(period, VELO_OBSERVATION_PERIOD); + // this observation is guaranteed to be from before the period (left side of the binary search) + uint256 L = observationLength - minObservationsPassed - 1; + uint256 R = observationLength - 1; // right side of the binary search + // binary search for the observation that's closest to the most recent one, yet still within the period + while (L < R) { + uint256 observationIndex = (L + R) / 2; + + (last.blockTimestamp, last.reserve0Cumulative, last.reserve1Cumulative) = + pair.observations(observationIndex); + if (last.blockTimestamp > maxTimestampRequired) { + R = observationIndex - 1; + } else { + L = observationIndex + 1; + } + } + time = current.blockTimestamp - last.blockTimestamp; + } + + uint112 reserve0 = safe112((current.reserve0Cumulative - last.reserve0Cumulative) / time); + uint112 reserve1 = safe112((current.reserve1Cumulative - last.reserve1Cumulative) / time); + + return _veloGetAmountOut(baseAmount, target, reserve0, reserve1, pair.stable(), pair); + } + + // Utils + + struct GetAmountOutLocalVars { + uint256 decimals0; + uint256 decimals1; + uint256 xy; + } + + function _veloGetAmountOut( + uint256 amountIn, + address tokenIn, + uint256 _reserve0, + uint256 _reserve1, + bool stable, + IVeloPair pair + ) private view returns (uint256) { + (address token0, address token1) = pair.tokens(); + if (stable) { + GetAmountOutLocalVars memory vars; + vars.decimals0 = 10 ** ERC20(token0).decimals(); + vars.decimals1 = 10 ** ERC20(token1).decimals(); + vars.xy = _k(_reserve0, _reserve1, vars.decimals0, vars.decimals1, stable); + _reserve0 = (_reserve0 * 1e18) / vars.decimals0; + _reserve1 = (_reserve1 * 1e18) / vars.decimals1; + (uint256 reserveA, uint256 reserveB) = tokenIn == token0 ? (_reserve0, _reserve1) : (_reserve1, _reserve0); + amountIn = tokenIn == token0 ? (amountIn * 1e18) / vars.decimals0 : (amountIn * 1e18) / vars.decimals1; + uint256 y = + reserveB - _get_y(amountIn + reserveA, vars.xy, reserveB, vars.decimals0, vars.decimals1, stable); + return (y * (tokenIn == token0 ? vars.decimals1 : vars.decimals0)) / 1e18; + } else { + (uint256 reserveA, uint256 reserveB) = tokenIn == token0 ? (_reserve0, _reserve1) : (_reserve1, _reserve0); + return (amountIn * reserveB) / (reserveA + amountIn); + } + } + + function _k(uint256 x, uint256 y, uint256 decimals0, uint256 decimals1, bool stable) + private + pure + returns (uint256) + { + if (stable) { + uint256 _x = (x * 1e18) / decimals0; + uint256 _y = (y * 1e18) / decimals1; + uint256 _a = (_x * _y) / 1e18; + uint256 _b = ((_x * _x) / 1e18 + (_y * _y) / 1e18); + return (_a * _b) / 1e18; // x3y+y3x >= k + } else { + return x * y; // xy >= k + } + } + + function _f(uint256 x0, uint256 y) private pure returns (uint256) { + uint256 _a = (x0 * y) / 1e18; + uint256 _b = ((x0 * x0) / 1e18 + (y * y) / 1e18); + return (_a * _b) / 1e18; + } + + function _d(uint256 x0, uint256 y) private pure returns (uint256) { + return (3 * x0 * ((y * y) / 1e18)) / 1e18 + ((((x0 * x0) / 1e18) * x0) / 1e18); + } + + function _get_y(uint256 x0, uint256 xy, uint256 y, uint256 decimals0, uint256 decimals1, bool stable) + private + pure + returns (uint256) + { + for (uint256 i = 0; i < 255; i++) { + uint256 k = _f(x0, y); + if (k < xy) { + // there are two cases where dy == 0 + // case 1: The y is converged and we find the correct answer + // case 2: _d(x0, y) is too large compare to (xy - k) and the rounding error + // screwed us. + // In this case, we need to increase y by 1 + uint256 dy = ((xy - k) * 1e18) / _d(x0, y); + if (dy == 0) { + if (k == xy) { + // We found the correct answer. Return y + return y; + } + if (_k(x0, y + 1, decimals0, decimals1, stable) > xy) { + // If _k(x0, y + 1) > xy, then we are close to the correct answer. + // There's no closer answer than y + 1 + return y + 1; + } + dy = 1; + } + y = y + dy; + } else { + uint256 dy = ((k - xy) * 1e18) / _d(x0, y); + if (dy == 0) { + if (k == xy || _f(x0, y - 1) < xy) { + // Likewise, if k == xy, we found the correct answer. + // If _f(x0, y - 1) < xy, then we are close to the correct answer. + // There's no closer answer than "y" + // It's worth mentioning that we need to find y where f(x0, y) >= xy + // As a result, we can't return y - 1 even it's closer to the correct answer + return y; + } + dy = 1; + } + y = y - dy; + } + } + revert("!y"); + } + + function safe112(uint256 n) private pure returns (uint112) { + if (n >= 2 ** 112) revert("lol"); + return uint112(n); + } +} diff --git a/test/OraclesTest.t.sol b/test/OraclesTest.t.sol index e9802b0..8f5035c 100644 --- a/test/OraclesTest.t.sol +++ b/test/OraclesTest.t.sol @@ -96,13 +96,7 @@ contract TarotOracleTest is Test { reaperSwapper.updateVeloSwapPath(usdcAddress, ernAddress, address(veloRouter), veloPath); vault = new ReaperVaultV2( - wantAddress, - vaultName, - vaultSymbol, - vaultTvlCap, - treasuryAddress, - strategists, - multisigRoles + wantAddress, vaultName, vaultSymbol, vaultTvlCap, treasuryAddress, strategists, multisigRoles ); ReaperStrategyStabilityPool.ExchangeSettings memory exchangeSettings; From affe3f821d928c5389647eb9641b936e0292d478 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:38:13 -0300 Subject: [PATCH 08/18] Use MAD instead of standard deviation --- src/OracleAggregator.sol | 75 +++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index da8497b..b719163 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -61,35 +61,78 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } - function getMeanPrice(uint256[] memory prices, uint256 maxStdRelativeToMeanBPS, uint256 maxScoreBPS) + function getMeanPrice(uint256[] memory prices, uint256 maxMadRelativeToMedianBPS, uint256 maxScoreBPS) public pure returns (uint256) { - (bool[] memory isInvalid) = getValidityByZScore(prices, maxScoreBPS); - (uint256 std, uint256 mean, uint256 nrOfValidPrices) = standardDeviation(prices, isInvalid); - if (std * BPS / mean > maxStdRelativeToMeanBPS) revert Oracle_OraclesUnreliable(); - if (nrOfValidPrices < 2) revert Oracle_OraclesUnreliable(); + (bool[] memory isInvalid, uint256 mad, uint256 median) = getValidityByZScore(prices, maxScoreBPS); + (uint256 mean, uint256 nrOfValidPrices) = getMean(prices, isInvalid); + if (mad > (median * maxMadRelativeToMedianBPS) / BPS) revert Oracle_OraclesUnreliable(); + if (nrOfValidPrices < ((prices.length * 3) / 5)) revert Oracle_OraclesUnreliable(); return mean; } function getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) public pure - returns (bool[] memory isInvalid) + returns (bool[] memory, uint256, uint256) { - (uint256 std, uint256 mean,) = standardDeviation(prices, new bool[](prices.length)); - isInvalid = new bool[](prices.length); + (uint256 mad, uint256 median) = getMAD(prices); + bool[] memory isInvalid = new bool[](prices.length); for (uint256 i = 0; i < prices.length; i++) { - int256 score = (int256(prices[i]) - int256(mean)) * int256(BPS) / int256(std); + int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); } + return (isInvalid, mad, median); } - function standardDeviation(uint256[] memory prices, bool[] memory isInvalid) + function quickSort(uint256[] memory arr, int256 left, int256 right) internal pure { + int256 i = left; + int256 j = right; + if (i == j) return; + uint256 pivot = arr[uint256(left + (right - left) / 2)]; + while (i <= j) { + while (arr[uint256(i)] < pivot) i++; + while (pivot < arr[uint256(j)]) j--; + if (i <= j) { + (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]); + i++; + j--; + } + } + if (left < j) { + quickSort(arr, left, j); + } + if (i < right) { + quickSort(arr, i, right); + } + } + + function getMAD(uint256[] memory arr) public pure returns (uint256 mad, uint256 median) { + uint256 n = arr.length; + quickSort(arr, 0, int256(n - 1)); + if (n % 2 == 0) { + median = (arr[n / 2 - 1] + arr[n / 2]) / 2; + } else { + median = arr[n / 2]; + } + uint256[] memory deviations = new uint256[](n); + for (uint256 i = 0; i < n; i++) { + if (arr[i] > median) { + deviations[i] = arr[i] - median; + } else { + deviations[i] = median - arr[i]; + } + } + quickSort(deviations, 0, int256(n - 1)); + mad = deviations[n / 2]; + } + + function getMean(uint256[] memory prices, bool[] memory isInvalid) public pure - returns (uint256 std, uint256 mean, uint256 nrValidPrices) + returns (uint256 mean, uint256 nrValidPrices) { uint256 sum = 0; for (uint256 i = 0; i < prices.length; i++) { @@ -99,16 +142,6 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } mean = sum / nrValidPrices; - uint256[] memory deviationsSq = new uint256[](nrValidPrices); - for (uint256 i = 0; i < nrValidPrices; i++) { - int256 deviation = int256(prices[i]) - int256(mean); - deviationsSq[i] = uint256(deviation * deviation); - } - uint256 sumDeviationsSq = 0; - for (uint256 i = 0; i < deviationsSq.length; i++) { - sumDeviationsSq += deviationsSq[i]; - } - std = MathUpgradeable.sqrt(sumDeviationsSq / nrValidPrices, MathUpgradeable.Rounding.Up); } function getPriceFeedPrice(address source, address target, uint256 baseAmount) public returns (uint256 price) { From ceed2cf5cfad6d4378bf1591de6efbc9ca00aa00 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:52:35 -0300 Subject: [PATCH 09/18] add natspec --- src/OracleAggregator.sol | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index b719163..faa1ae8 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -42,6 +42,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } + /// @param route List of oracles for multihop price + /// @param baseAmount Input amount of the base token function getMultiHopPrice(OracleRoute memory route, uint256 baseAmount) public returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { price = getPrice(route.oracles[i], baseAmount); @@ -49,6 +51,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } + /// @param oracle Kind of oracle to use -- see OracleKind + /// @param baseAmount Input amount of the base token function getPrice(Oracle memory oracle, uint256 baseAmount) public returns (uint256 price) { if (oracle.kind == OracleKind.Velo) { return getVeloPrice(oracle.source, oracle.target, uint32(oracle.period), baseAmount); @@ -61,6 +65,12 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } + /// @notice Get the mean price of a list of prices, filtering out outliers + /// @param prices List of prices + /// @param maxMadRelativeToMedianBPS How many BPS the MAD can be relative to the median. + /// For example, a MAD higher than 10% of the median means the prices are too spread out, + /// and the whole list is considered unreliable. + /// @param maxScoreBPS If a price has a Z-score higher than this, it's considered an outlier and filtered out function getMeanPrice(uint256[] memory prices, uint256 maxMadRelativeToMedianBPS, uint256 maxScoreBPS) public pure @@ -73,6 +83,11 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { return mean; } + /// @param prices List of prices to be checked + /// @param maxScoreBPS If a price has a Z-score higher than this, it's considered an outlier and filtered out + /// @return An array mask for the prices array, where true means the price is invalid + /// @return The MAD - Median Absolute Deviation + /// @return The median of the prices function getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) public pure @@ -109,6 +124,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } } + /// @notice Get the Median Absolute Deviation of a list of values + /// @param arr List of values function getMAD(uint256[] memory arr) public pure returns (uint256 mad, uint256 median) { uint256 n = arr.length; quickSort(arr, 0, int256(n - 1)); From 0ee142e6ee60bfc73c718cef247122b38244c379 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:50:09 -0300 Subject: [PATCH 10/18] Integrate OracleAggregator into Strategy --- .gitmodules | 6 +- foundry.toml | 11 + remappings.txt | 7 - src/OracleAggregator.sol | 34 +- src/ReaperStrategyStabilityPool.sol | 349 +++--------------- src/oracles/VeloTwapMixin.sol | 8 +- ...raclesTest.t.sol => OraclesTest.t.solTODO} | 0 7 files changed, 107 insertions(+), 308 deletions(-) delete mode 100644 remappings.txt rename test/{OraclesTest.t.sol => OraclesTest.t.solTODO} (100%) diff --git a/.gitmodules b/.gitmodules index 9e9a96b..047f42b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "lib/vault-v2"] - path = lib/vault-v2 - url = git@github.com:Byte-Masons/vault-v2.git +[submodule "lib/vault-v2"] + path = lib/vault-v2 + url = https://github.com/Byte-Masons/vault-v2 [submodule "lib/v3-core"] path = lib/v3-core url = https://github.com/Uniswap/v3-core diff --git a/foundry.toml b/foundry.toml index 4ff40c4..27fe069 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,16 @@ src = "src" out = "out" libs = ["lib"] +via-ir = true + +remappings = [ + "vault-v2/=lib/vault-v2/src/", + "univ3-core/=lib/v3-core/contracts/", + "mixins/=lib/vault-v2/src/mixins/", + "ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/", + "forge-std/=lib/vault-v2/lib/forge-std/src/", + "oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/", + "oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/" +] # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index 9cf44fc..0000000 --- a/remappings.txt +++ /dev/null @@ -1,7 +0,0 @@ -vault-v2/=lib/vault-v2/src/ -univ3-core/=lib/v3-core/contracts/ -mixins/=lib/vault-v2/src/mixins/ -ds-test/=lib/vault-v2/lib/forge-std/lib/ds-test/src/ -forge-std/=lib/vault-v2/lib/forge-std/src/ -oz-upgradeable/=lib/vault-v2/lib/openzeppelin-contracts-upgradeable/contracts/ -oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index faa1ae8..219082b 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -35,6 +35,30 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { OracleKind kind; } + function getTwapPricesView(OracleRoute[] memory oracles, uint256 baseAmount) + public + view + returns (uint256[] memory prices) + { + prices = new uint256[](oracles.length); + for (uint256 i = 0; i < oracles.length; i++) { + prices[i] = getMultiHopPriceView(oracles[i], baseAmount); + } + } + + /// @param route List of oracles for multihop price + /// @param baseAmount Input amount of the base token + function getMultiHopPriceView(OracleRoute memory route, uint256 baseAmount) public view returns (uint256 price) { + for (uint256 i = 0; i < route.oracles.length; i++) { + price = getPrice(route.oracles[i], baseAmount); + baseAmount = price; + } + } + + /** + * state-changing versions of the above functions + * This allows the Kind of oracle to be PriceFeed + */ function getTwapPrices(OracleRoute[] memory oracles, uint256 baseAmount) public returns (uint256[] memory prices) { prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { @@ -46,6 +70,11 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { /// @param baseAmount Input amount of the base token function getMultiHopPrice(OracleRoute memory route, uint256 baseAmount) public returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { + if (route.oracles[i].kind == OracleKind.PriceFeed) { + price = getPriceFeedPrice(route.oracles[i].source, route.oracles[i].target, baseAmount); + } else { + price = getPrice(route.oracles[i], baseAmount); + } price = getPrice(route.oracles[i], baseAmount); baseAmount = price; } @@ -53,13 +82,11 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { /// @param oracle Kind of oracle to use -- see OracleKind /// @param baseAmount Input amount of the base token - function getPrice(Oracle memory oracle, uint256 baseAmount) public returns (uint256 price) { + function getPrice(Oracle memory oracle, uint256 baseAmount) public view returns (uint256 price) { if (oracle.kind == OracleKind.Velo) { return getVeloPrice(oracle.source, oracle.target, uint32(oracle.period), baseAmount); } else if (oracle.kind == OracleKind.UniV3) { return getUniV3Price(oracle.source, oracle.target, uint32(oracle.period), baseAmount); - } else if (oracle.kind == OracleKind.PriceFeed) { - return getPriceFeedPrice(oracle.source, oracle.target, baseAmount); } else { revert Oracle_InvalidKind(); } @@ -165,5 +192,6 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { return IPriceFeed(source).fetchPrice(target) * baseAmount; } + // in the case contracts that inhrerit from this one are upgradeable uint256[50] private __gap; } diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index 873c412..b62691c 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -14,45 +14,38 @@ import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol"; import {IERC20MetadataUpgradeable} from "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {SafeERC20Upgradeable} from "oz-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; -import {IVeloPair} from "./interfaces/IVeloPair.sol"; +import {OracleAggregator} from "./OracleAggregator.sol"; /** * @dev Strategy to compound rewards and liquidation collateral gains in the Ethos stability pool */ -contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { +contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { using ReaperMathUtils for uint256; using SafeERC20Upgradeable for IERC20MetadataUpgradeable; // constants - uint256 constant MIN_VELO_PRICE_UPDATE_INTERVAL = 1 days; - uint256 constant MIN_ALLOWED_PERIOD_VELO = 2 days; - uint256 constant MIN_NR_OF_POINTS = 1; - uint256 constant MIN_LENGTH_OF_WINDOW = 1; + uint256 constant MAXIMUM_ALLOWED_RELATIVE_CHANGE = 300; // 3% - uint256 constant PRICE_VALIDITY_THRESHOLD = 5000; // 50% - uint32 constant MAX_ALLOWED_TOLERANCE = 200; // 2% + uint256 public constant MAX_MAD_RELATIVE_TO_MEDIAN_BPS = 3000000; + uint256 public constant MAX_SCORE_BPS = 10000; // 3rd-party contract addresses IStabilityPool public stabilityPool; IPriceFeed public priceFeed; IERC20MetadataUpgradeable public usdc; - IERC20MetadataUpgradeable public weth; ExchangeSettings public exchangeSettings; // Holds addresses to use Velo, UniV3 and Bal through Swapper - IUniswapV3Pool public uniV3UsdcErnPool; - IVeloPair public veloUsdcErnPool; - IVeloPair public veloWethErnPool; - IStaticOracle public uniV3TWAP; uint256 public constant ETHOS_DECIMALS = 18; // Decimals used by ETHOS uint256 public ernMinAmountOutBPS; // The max allowed slippage when trading in to ERN uint256 public compoundingFeeMarginBPS; // How much collateral value is lowered to account for the costs of swapping - uint32 public uniV3TWAPPeriod; // How many seconds the uniV3 TWAP will look at - uint32 public veloTWAPPeriod; // How many seconds the velo TWAP will look at ExchangeType public usdcToErnExchange; // Controls which exchange is used to swap USDC to ERN bool public shouldOverrideHarvestBlock; // If reverts on TWAP out of normal range should be ignored uint256 acceptableTWAPUpperBound; // The normal upper price for the TWAP, reverts harvest if above uint256 acceptableTWAPLowerBound; // The normal lower price for the , reverts harvest if below + OracleRoute[] internal ernForUsdcOracles; + OracleRoute[] internal ernForUsdcViewOracles; + struct ExchangeSettings { address veloRouter; address balVault; @@ -62,29 +55,21 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { struct Pools { address stabilityPool; - address uniV3UsdcErnPool; - address veloUsdcErnPool; - address veloWethErnPool; } struct Tokens { address want; address usdc; - address weth; } error InvalidUsdcToErnExchange(uint256 exchangeEnum); - error InvalidUsdcToErnTWAP(uint256 twapEnum); - error TWAPOutsideAllowedRange(uint256 usdcPrice); + error TWAPOutsideAllowedRange(uint256 ernPrice); error InvalidSwapStep(); - error CouldntDetermineMeanPrice(); - error WindowLongerThanOrZero(); - error TooShortPeriod(); - /** * @dev Initializes the strategy. Sets parameters, saves routes, and gives allowances. * @notice see documentation for each variable above its respective declaration. */ + function initialize( address _vault, address _swapper, @@ -93,6 +78,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { address[] memory _keepers, address _priceFeed, address _uniV3TWAP, + OracleRoute[] calldata _ernForUsdcOracles, ExchangeSettings calldata _exchangeSettings, Pools calldata _pools, Tokens calldata _tokens @@ -104,34 +90,24 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { require(_tokens.want != address(0), "want is 0 address"); require(_priceFeed != address(0), "priceFeed is 0 address"); require(_tokens.usdc != address(0), "usdc is 0 address"); - require(_tokens.weth != address(0), "weth is 0 address"); require(_uniV3TWAP != address(0), "uniV3TWAP is 0 address"); require(_exchangeSettings.veloRouter != address(0), "veloRouter is 0 address"); require(_exchangeSettings.balVault != address(0), "balVault is 0 address"); require(_exchangeSettings.uniV3Router != address(0), "uniV3Router is 0 address"); require(_exchangeSettings.uniV2Router != address(0), "uniV2Router is 0 address"); require(_pools.stabilityPool != address(0), "stabilityPool is 0 address"); - require(_pools.uniV3UsdcErnPool != address(0), "uniV3UsdcErnPool is 0 address"); - require(_pools.veloUsdcErnPool != address(0), "veloUsdcErnPool is 0 address"); - require(_pools.veloWethErnPool != address(0), "veloWethErnPool is 0 address"); __ReaperBaseStrategy_init(_vault, _swapper, _tokens.want, _strategists, _multisigRoles, _keepers); stabilityPool = IStabilityPool(_pools.stabilityPool); priceFeed = IPriceFeed(_priceFeed); usdc = IERC20MetadataUpgradeable(_tokens.usdc); - weth = IERC20MetadataUpgradeable(_tokens.weth); exchangeSettings = _exchangeSettings; updateErnMinAmountOutBPS(9800); usdcToErnExchange = ExchangeType.UniV3; - uniV3TWAP = IStaticOracle(_uniV3TWAP); - uniV3UsdcErnPool = IUniswapV3Pool(_pools.uniV3UsdcErnPool); - veloUsdcErnPool = IVeloPair(_pools.veloUsdcErnPool); - veloWethErnPool = IVeloPair(_pools.veloWethErnPool); compoundingFeeMarginBPS = 9950; - updateUniV3TWAPPeriod(7200); - veloTWAPPeriod = 2 days; + updateOracles(_ernForUsdcOracles); updateAcceptableTWAPBounds(980_000, 1_100_000); } @@ -190,15 +166,15 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { } } - function _revertOnTWAPOutsideRange() internal view { + function _revertOnTWAPOutsideRange() internal { if (shouldOverrideHarvestBlock) { return; } - uint128 ernAmount = 1 ether; // 1 ERN - uint256 usdcAmount = getErnAmountForUsdcAll(ernAmount, MAX_ALLOWED_TOLERANCE); + uint128 usdcAmount = 1 ether; // 1 ERN + uint256 ernAmount = _getErnAmountForUsdc(usdcAmount); - if (usdcAmount < acceptableTWAPLowerBound || usdcAmount > acceptableTWAPUpperBound) { - revert TWAPOutsideAllowedRange(usdcAmount); + if (ernAmount < acceptableTWAPLowerBound || ernAmount > acceptableTWAPUpperBound) { + revert TWAPOutsideAllowedRange(ernAmount); } } @@ -265,20 +241,22 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { /** * @dev Calculates the estimated ERN value of collateral and USDC using Chainlink oracles - * and the Velodrome USDC-ERN TWAP. + * and the set TWAP oracles - uses only view functions. */ function getERNValueOfCollateralGain() public view returns (uint256 ernValueOfCollateral) { uint256 usdValueOfCollateralGain = getUSDValueOfCollateralGain(); - ernValueOfCollateral = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain); + uint256 totalUsdcValue = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain); + ernValueOfCollateral = _getErnAmountForUsdcView(totalUsdcValue); } /** * @dev Calculates the estimated ERN value of collateral using the Ethos price feed, Chainlink oracle for USDC - * and the Velodrome USDC-ERN TWAP. + * and the set TWAP oracles. */ function getERNValueOfCollateralGainUsingPriceFeed() public returns (uint256 ernValueOfCollateral) { uint256 usdValueOfCollateralGain = getUSDValueOfCollateralGainUsingPriceFeed(); - ernValueOfCollateral = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain); + uint256 totalUsdcValue = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain); + ernValueOfCollateral = _getErnAmountForUsdc(totalUsdcValue); } /** @@ -287,12 +265,11 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { function getERNValueOfCollateralGainCommon(uint256 _usdValueOfCollateralGain) public view - returns (uint256 ernValueOfCollateral) + returns (uint256 totalUsdcValue) { uint256 usdcValueOfCollateral = _getUsdcEquivalentOfUSD(_usdValueOfCollateralGain); uint256 usdcBalance = usdc.balanceOf(address(this)); - uint256 totalUsdcValue = usdcBalance + usdcValueOfCollateral; - ernValueOfCollateral = _getErnAmountForUsdc(totalUsdcValue); + totalUsdcValue = usdcBalance + usdcValueOfCollateral; } /** @@ -335,201 +312,31 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { /** * @dev Returns the {expectedErnAmount} for the specified {_usdcAmount} of USDC using - * the UniV3 TWAP. + * TWAPs. */ - function _getErnAmountForUsdc(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { + function _getErnAmountForUsdc(uint256 _usdcAmount) internal returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - expectedErnAmount = getErnAmountForUsdcUniV3(uint128(_usdcAmount), uniV3TWAPPeriod); + uint256[] memory prices = getTwapPrices(ernForUsdcOracles, _usdcAmount); + return getMeanPrice(prices, MAX_MAD_RELATIVE_TO_MEDIAN_BPS, MAX_SCORE_BPS); } } /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the UniV3 TWAP. - */ - function getErnAmountForUsdcUniV3(uint128 _baseAmount, uint32 _period) public view returns (uint256 ernAmount) { - address[] memory pools = new address[](1); - pools[0] = address(uniV3UsdcErnPool); - uint256 quoteAmount = - uniV3TWAP.quoteSpecificPoolsWithTimePeriod(_baseAmount, address(usdc), want, pools, _period); - return quoteAmount; - } - - /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo TWAP - * @notice One sample with wide window (calculations are done inside sample function between two last priceCumulatives and timestamps). - */ - function getErnAmountForUsdcVeloWindow(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - if (_period < MIN_ALLOWED_PERIOD_VELO) { - revert TooShortPeriod(); - } - uint256 window = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; - if (window >= veloUsdcErnPool.observationLength() || window == 0) { - revert WindowLongerThanOrZero(); - } - uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); - uint256[] memory quoteAmount = - veloUsdcErnPool.sample(address(usdc), (10 ** usdc.decimals()), MIN_NR_OF_POINTS, window); - return (_baseAmount * quoteAmount[0] * (10 ** (wantDecimals - usdc.decimals())) / 1 ether); // better math - } - - /** - * @dev provides twap price with user configured granularity, up to the full window size - * - */ - function _quote(address tokenIn, uint256 amountIn, uint256 granularity) private view returns (uint256 amountOut) { - uint256[] memory _prices = veloUsdcErnPool.sample(tokenIn, amountIn, granularity, MIN_LENGTH_OF_WINDOW); - uint256 priceAverageCumulative; - uint256 _length = _prices.length; - for (uint256 i = 0; i < _length; i++) { - priceAverageCumulative += _prices[i]; - } - return priceAverageCumulative / granularity; - } - - /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo TWAP - * @notice Multiple samples with shortest window possible (calculations are the average of small samples calculated from shortest possible window). - */ - function getErnAmountForUsdcVeloPoints(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - if (_period < MIN_ALLOWED_PERIOD_VELO) { - revert TooShortPeriod(); - } - uint256 granuality = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; - if (granuality >= veloUsdcErnPool.observationLength() || granuality == 0) { - revert WindowLongerThanOrZero(); - } - uint256 quoteAmount = _quote(address(usdc), (10 ** usdc.decimals()), granuality); - uint256 wantDecimals = IERC20MetadataUpgradeable(want).decimals(); - - return (_baseAmount * quoteAmount * (10 ** (wantDecimals - usdc.decimals())) / 1 ether); // better math - } - - function getUsdcAmountForWethUsingPriceFeeds() public returns (uint256) { - uint256 tmpPrice = _getUSDEquivalentOfCollateralUsingPriceFeed(address(weth), 1 ether); - return _getUsdcEquivalentOfUSD(tmpPrice); - } - - /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo TWAP. - */ - function getErnAmountForWethVelo(uint128 _baseAmount, uint32 _period) public view returns (uint256) { - if (_period < MIN_ALLOWED_PERIOD_VELO) { - revert TooShortPeriod(); - } - address[] memory pools = new address[](1); - uint256 window = _period / MIN_VELO_PRICE_UPDATE_INTERVAL; - if (window >= veloUsdcErnPool.observationLength() || window == 0) { - revert WindowLongerThanOrZero(); - } - - uint256[] memory quoteAmount = veloWethErnPool.sample(address(weth), 1e18, 1, window); - return (_baseAmount * quoteAmount[0] / 1 ether); // better math - } - - /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the Velo ERN/WETH TWAP and chainlink price feed. - * - * Math: - * 1e18 wei - x ern - * 1e18 wei - y usdc - * ern = 1e18 wei / x => ern = y usdc / x - * @notice Due to price feeds interface this function cannot be viewable - it usage makes difficullties - */ - function getErnAmountForUsdcVeloWeth(uint128 _baseAmount, uint32 _period) public returns (uint256) { - uint256 usdcAmountForWethPriceFeeds = (getUsdcAmountForWethUsingPriceFeeds() * _baseAmount); - // console2.log("Feed: ", usdcAmountForWethPriceFeeds); - uint256 veloAmountForWethVelo = getErnAmountForWethVelo(_baseAmount, _period); - // console2.log("Velo: ", veloAmountForWethVelo); - return ((usdcAmountForWethPriceFeeds * 1 ether * 10 ** usdc.decimals()) / veloAmountForWethVelo); - } - - /** - * @dev Function consumes array of {prices} and check them against one reference price pointed by {idx}. - * If the prices are inside range specified in {tolerance}, function marks it in {indexes} array and increment {nrOfValidPrices}. - * @param prices - array of prices from oracles - * @param idx - array index at which the price will be taken as a reference - * @param tolerance - allowed tolerance of deviation - * @return indexes - indexes at which the prices are in range - * @return nrOfValidPrices - number of prices which are in range + * @dev Returns the {expectedErnAmount} for the specified {_usdcAmount} of USDC using + * the UniV3 TWAP. */ - function getInfoAboutTwapOracles(uint256[] memory prices, uint32 idx, uint32 tolerance) - private - pure - returns (bool[] memory, uint32) - { - require(idx <= prices.length); - bool[] memory indexes = new bool[](prices.length); - uint256 referencePrice = prices[idx]; - uint32 nrOfValidPrices = 1; - indexes[idx] = true; - - /* For loop assumptions: - - {cnt} shall start with value greater than passed {idx} but cannot be greater than length of array of prices - - loop ends when {cnt} reaches value of {idx} - it must happen as we are iterating over finite number of values (modulo {price.length}) - - {cnt} increments by one and starts from 0 when reaches {prices.length} - */ - for (uint32 cnt = (idx + 1) % uint32(prices.length); cnt != idx; cnt = (cnt + 1) % uint32(prices.length)) { - if ( - referencePrice + (referencePrice * tolerance / PERCENT_DIVISOR) >= prices[cnt] - && referencePrice - (referencePrice * tolerance / PERCENT_DIVISOR) <= prices[cnt] - ) { - /* The price is inside the range - store index and increment number of valid prices */ - nrOfValidPrices++; - indexes[cnt] = true; - } + function _getErnAmountForUsdcView(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { + if (_usdcAmount != 0) { + uint256[] memory prices = getTwapPricesView(ernForUsdcViewOracles, _usdcAmount); + return getMeanPrice(prices, MAX_MAD_RELATIVE_TO_MEDIAN_BPS, MAX_SCORE_BPS); } - return (indexes, nrOfValidPrices); } /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the all possible oracles. + * @dev See above. */ - function getErnAmountForUsdcAll(uint128 _baseAmount, uint32 _tolerance) public view returns (uint256) { - uint256[] memory _prices = new uint256[](2); - _prices[0] = getErnAmountForUsdcVeloPoints(_baseAmount, veloTWAPPeriod); - _prices[1] = getErnAmountForUsdcUniV3(_baseAmount, uniV3TWAPPeriod); - //_prices[2] = getErnAmountForUsdcVeloWeth(_baseAmount, _period); // This function is not view - - return getErnAmountForUsdcAll(_prices, _tolerance); - } - - /** - * @dev Returns the {ernAmount} for the specified {_baseAmount} of USDC over a given {_period} (in seconds) - * using the all possible oracles. - */ - function getErnAmountForUsdcAll(uint256[] memory _prices, uint32 _tolerance) public view returns (uint256) { - uint256 averageTwap = 0; - if (_prices.length > 1) { - for (uint32 idx = 0; idx < _prices.length; idx++) { - (bool[] memory indexes, uint32 nrOfValidPrices) = getInfoAboutTwapOracles(_prices, idx, _tolerance); - // Amount of valid prices must be greater than {PRICE_VALIDITY_THRESHOLD}% - if (nrOfValidPrices > (_prices.length * PRICE_VALIDITY_THRESHOLD / PERCENT_DIVISOR)) { - uint256 sumOfPrices = 0; - // Iterate through {indexes} array to see which index of price array shall be taken into {meanTwap} calculations - for (uint32 cnt = 0; cnt < indexes.length; cnt++) { - if (indexes[cnt] != false) { - sumOfPrices += _prices[cnt]; - } - } - averageTwap = sumOfPrices / nrOfValidPrices; - break; - } - } - if (averageTwap == 0) { - revert CouldntDetermineMeanPrice(); - } - } else if (_prices.length == 1) { - averageTwap = _prices[0]; - } else { - revert CouldntDetermineMeanPrice(); - } - return averageTwap; + function getErnAmountForUsdcView(uint256 _usdcAmount) external view returns (uint256) { + return _getErnAmountForUsdcView(_usdcAmount); } /** @@ -551,7 +358,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { internal returns (uint256) { - uint256 price = priceFeed.fetchPrice(_collateral); // Question: This make function not viewable an must be propagated upper + uint256 price = priceFeed.fetchPrice(_collateral); return _getUSDEquivalentOfCollateralCommon(_collateral, _amount, price, ETHOS_DECIMALS); } @@ -608,22 +415,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { } } - /** - * @dev Scales {_collAmount} given in 18 decimals to an amount in {_collDecimals} - */ - function _scaleToCollateralDecimals(uint256 _collAmount, uint256 _collDecimals) - internal - pure - returns (uint256 scaledColl) - { - scaledColl = _collAmount; - if (_collDecimals > ETHOS_DECIMALS) { - scaledColl = scaledColl * (10 ** (_collDecimals - ETHOS_DECIMALS)); - } else if (_collDecimals < ETHOS_DECIMALS) { - scaledColl = scaledColl / (10 ** (ETHOS_DECIMALS - _collDecimals)); - } - } - /** * Swapping to ERN (want) is hardcoded in this strategy and relies on TWAP so * a swap step should not be set to swap to it. @@ -669,52 +460,24 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { } /** - * @dev Sets the period (in seconds) used to query the UniV3 TWAP - * The pool itself has a {currentCardinality} by calling - * increaseObservationCardinalityNext on the UniV3 pool. - * The earliest observation in the pool must be within the given time period. - * Will revert if the observation period is too long. - * DEFAULT_ADMIN is allowed to change the value regardless, but for lower access - * roles a check is performed to see if changing duration would effect the price - * past some threshold, if the strategy holds collateral value (priced by TWAP). + * @dev Sets the period (in seconds) used to query the UniV3 TWAP. */ - function updateUniV3TWAPPeriod(uint32 _uniV3TWAPPeriod) public { - _atLeastRole(ADMIN); - require(_uniV3TWAPPeriod >= 7200, "TWAP period is too short"); - - uint256 newErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), _uniV3TWAPPeriod); - uint256 oldErnAmount = getErnAmountForUsdcUniV3(uint128(1_000_000), uniV3TWAPPeriod); - - uniV3TWAPPeriod = _uniV3TWAPPeriod; - - if (_hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) return; - - uint256 ernCollateralValue = getERNValueOfCollateralGainUsingPriceFeed(); - - if (ernCollateralValue != 0) { - uint256 difference = newErnAmount > oldErnAmount ? newErnAmount - oldErnAmount : oldErnAmount - newErnAmount; - uint256 relativeChange = difference * PERCENT_DIVISOR / oldErnAmount; - require(relativeChange < MAXIMUM_ALLOWED_RELATIVE_CHANGE, "TWAP duration change would change price"); - } - } - - function updateVeloTWAPPeriod(uint32 _veloTWAPPeriod) public { - _atLeastRole(ADMIN); - require(_veloTWAPPeriod >= MIN_ALLOWED_PERIOD_VELO, "TWAP period is too short"); - - uint256 newErnAmount = getErnAmountForUsdcVeloPoints(uint128(1_000_000), _veloTWAPPeriod); - uint256 oldErnAmount = getErnAmountForUsdcVeloPoints(uint128(1_000_000), veloTWAPPeriod); - - veloTWAPPeriod = _veloTWAPPeriod; - - if (_hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) return; - - uint256 ernCollateralValue = getERNValueOfCollateralGainUsingPriceFeed(); - - if (ernCollateralValue != 0) { - uint256 difference = newErnAmount > oldErnAmount ? newErnAmount - oldErnAmount : oldErnAmount - newErnAmount; - uint256 relativeChange = difference * PERCENT_DIVISOR / oldErnAmount; - require(relativeChange < MAXIMUM_ALLOWED_RELATIVE_CHANGE, "TWAP duration change would change price"); + function updateOracles(OracleRoute[] calldata newRoutes) public { + _atLeastRole(DEFAULT_ADMIN_ROLE); + ernForUsdcOracles = newRoutes; + + // reset the view-only oracles) + ernForUsdcViewOracles = new OracleRoute[](0); + // filter out the price feed oracles and set the view-only oracles + for (uint256 i = 0; i < newRoutes.length; i++) { + for (uint256 j = 0; j < newRoutes[i].oracles.length; j++) { + if (newRoutes[i].oracles[j].kind == OracleKind.PriceFeed) { + break; + } + if (j == newRoutes[i].oracles.length - 1) { + ernForUsdcViewOracles.push(newRoutes[i]); + } + } } } diff --git a/src/oracles/VeloTwapMixin.sol b/src/oracles/VeloTwapMixin.sol index 55e2bcc..62d8c3d 100644 --- a/src/oracles/VeloTwapMixin.sol +++ b/src/oracles/VeloTwapMixin.sol @@ -51,8 +51,12 @@ contract VeloTwapMixin { return _veloGetAmountOut(baseAmount, target, reserve0, reserve1, pair.stable(), pair); } - // Utils - + /** + * Utils + * Below are the functions that are used to calculate the price of a token in a Velo pool. + * This code is adapted from Velodrome's contracts directly, with changes to use parameters + * instead of state variables. + */ struct GetAmountOutLocalVars { uint256 decimals0; uint256 decimals1; diff --git a/test/OraclesTest.t.sol b/test/OraclesTest.t.solTODO similarity index 100% rename from test/OraclesTest.t.sol rename to test/OraclesTest.t.solTODO From 94ac0b70f9fd0f6b269f19e10d39724e42cc27f7 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:34:41 -0300 Subject: [PATCH 11/18] improve tests --- foundry.toml | 4 +- src/OracleAggregator.sol | 128 +++++--- src/ReaperStrategyStabilityPool.sol | 48 +-- src/interfaces/IBalancerTwapOracle.sol | 102 +++++++ src/interfaces/IBalancerVault.sol | 164 ++++++++++ src/oracles/BalancerTwapMixin.sol | 72 +++++ src/oracles/UniV3TwapMixin.sol | 16 +- src/oracles/VeloTwapMixin.sol | 6 +- test/OracleAggregatorTest.t.sol | 93 ++++++ test/OraclesForkTests.sol | 133 ++++++++ test/OraclesTest.t.solTODO | 283 ------------------ ...TODO => ReaperStrategyStabilityPool.t.sol} | 130 ++++---- test/test_cases.json | 89 ++++++ 13 files changed, 842 insertions(+), 426 deletions(-) create mode 100644 src/interfaces/IBalancerTwapOracle.sol create mode 100644 src/interfaces/IBalancerVault.sol create mode 100644 src/oracles/BalancerTwapMixin.sol create mode 100644 test/OracleAggregatorTest.t.sol create mode 100644 test/OraclesForkTests.sol delete mode 100644 test/OraclesTest.t.solTODO rename test/{ReaperStrategyStabilityPool.t.solTODO => ReaperStrategyStabilityPool.t.sol} (93%) create mode 100644 test/test_cases.json diff --git a/foundry.toml b/foundry.toml index 27fe069..23b0ced 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,9 @@ src = "src" out = "out" libs = ["lib"] -via-ir = true + + +fs_permissions = [{ access = "read", path = "./test/"}] remappings = [ "vault-v2/=lib/vault-v2/src/", diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 219082b..afe6b43 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -4,54 +4,57 @@ pragma solidity ^0.8.0; import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; +import {BalancerTwapMixin} from "./oracles/BalancerTwapMixin.sol"; import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; +enum OracleKind { + Velo, + UniV3, + Balancer, + PriceFeed +} + +struct OracleRoute { + Oracle[] oracles; +} + +struct Oracle { + address source; + address tokenIn; + uint256 period; + OracleKind kind; +} + // This contract contains tools for computing TWAP values and // making averages between the results, for more reliable prices. // Has support for multiple oracles -contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { +contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { error Oracle_VeloOverflow(); error Oracle_InvalidKind(); - error Oracle_OraclesUnreliable(); + error Oracle_PricesSpreadTooHigh(); + error Oracle_PricesUnreliable(); - uint256 constant BPS = 1_000_000; - - enum OracleKind { - Velo, - UniV3, - PriceFeed - } - - struct OracleRoute { - Oracle[] oracles; - } - - struct Oracle { - address source; - address target; - uint256 period; - OracleKind kind; - } + uint256 constant BPS = 10_000; - function getTwapPricesView(OracleRoute[] memory oracles, uint256 baseAmount) - public + function getTwapPricesView(OracleRoute[] memory oracles, uint256 amountIn) + external view returns (uint256[] memory prices) { prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { - prices[i] = getMultiHopPriceView(oracles[i], baseAmount); + prices[i] = getMultiHopPriceView(oracles[i], amountIn); } } /// @param route List of oracles for multihop price - /// @param baseAmount Input amount of the base token - function getMultiHopPriceView(OracleRoute memory route, uint256 baseAmount) public view returns (uint256 price) { + /// @param amountIn Input amount of the base token + function getMultiHopPriceView(OracleRoute memory route, uint256 amountIn) public view returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { - price = getPrice(route.oracles[i], baseAmount); - baseAmount = price; + price = _getPrice(route.oracles[i], amountIn); + amountIn = price; } } @@ -59,34 +62,44 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { * state-changing versions of the above functions * This allows the Kind of oracle to be PriceFeed */ - function getTwapPrices(OracleRoute[] memory oracles, uint256 baseAmount) public returns (uint256[] memory prices) { + function getTwapPrices(OracleRoute[] memory oracles, uint256 amountIn) external returns (uint256[] memory prices) { prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { - prices[i] = getMultiHopPrice(oracles[i], baseAmount); + prices[i] = _getMultiHopPrice(oracles[i], amountIn); } } + function getMultiHopPrice(OracleRoute memory route, uint256 amountIn) external returns (uint256 price) { + return _getMultiHopPrice(route, amountIn); + } + /// @param route List of oracles for multihop price - /// @param baseAmount Input amount of the base token - function getMultiHopPrice(OracleRoute memory route, uint256 baseAmount) public returns (uint256 price) { + /// @param amountIn Input amount of the base token + function _getMultiHopPrice(OracleRoute memory route, uint256 amountIn) internal returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { if (route.oracles[i].kind == OracleKind.PriceFeed) { - price = getPriceFeedPrice(route.oracles[i].source, route.oracles[i].target, baseAmount); + price = getPriceFeedPrice(route.oracles[i].source, route.oracles[i].tokenIn, amountIn); } else { - price = getPrice(route.oracles[i], baseAmount); + price = _getPrice(route.oracles[i], amountIn); } - price = getPrice(route.oracles[i], baseAmount); - baseAmount = price; + price = _getPrice(route.oracles[i], amountIn); + amountIn = price; } } + function getPrice(Oracle memory oracle, uint256 amountIn) external view returns (uint256 price) { + return _getPrice(oracle, amountIn); + } + /// @param oracle Kind of oracle to use -- see OracleKind - /// @param baseAmount Input amount of the base token - function getPrice(Oracle memory oracle, uint256 baseAmount) public view returns (uint256 price) { + /// @param amountIn Input amount of the base token + function _getPrice(Oracle memory oracle, uint256 amountIn) internal view returns (uint256 price) { if (oracle.kind == OracleKind.Velo) { - return getVeloPrice(oracle.source, oracle.target, uint32(oracle.period), baseAmount); + return getVeloPrice(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); } else if (oracle.kind == OracleKind.UniV3) { - return getUniV3Price(oracle.source, oracle.target, uint32(oracle.period), baseAmount); + return getUniV3Price(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); + } else if (oracle.kind == OracleKind.Balancer) { + return getBalancerPrice(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); } else { revert Oracle_InvalidKind(); } @@ -94,19 +107,33 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { /// @notice Get the mean price of a list of prices, filtering out outliers /// @param prices List of prices - /// @param maxMadRelativeToMedianBPS How many BPS the MAD can be relative to the median. + /// @param spreadTolerance Is used in two ways: + /// 2 prices: The value will be multiplied by 2, and will be how many + /// BPS the difference between the two prices can be. + /// 3+ prices: How many BPS the MAD can be relative to the median. /// For example, a MAD higher than 10% of the median means the prices are too spread out, /// and the whole list is considered unreliable. /// @param maxScoreBPS If a price has a Z-score higher than this, it's considered an outlier and filtered out - function getMeanPrice(uint256[] memory prices, uint256 maxMadRelativeToMedianBPS, uint256 maxScoreBPS) - public + function getMeanPrice(uint256[] memory prices, uint256 spreadTolerance, uint256 maxScoreBPS) + external pure returns (uint256) { + if (prices.length == 1) return prices[0]; + if (prices.length == 2) { + if (prices[0] == 0 || prices[1] == 0) revert Oracle_PricesUnreliable(); + spreadTolerance = spreadTolerance * 2; + if (prices[0] > prices[1]) { + if (prices[0] > (prices[1] * (BPS + spreadTolerance)) / BPS) revert Oracle_PricesSpreadTooHigh(); + } else { + if (prices[1] > (prices[0] * (BPS + spreadTolerance)) / BPS) revert Oracle_PricesSpreadTooHigh(); + } + return (prices[0] + prices[1]) / 2; + } (bool[] memory isInvalid, uint256 mad, uint256 median) = getValidityByZScore(prices, maxScoreBPS); (uint256 mean, uint256 nrOfValidPrices) = getMean(prices, isInvalid); - if (mad > (median * maxMadRelativeToMedianBPS) / BPS) revert Oracle_OraclesUnreliable(); - if (nrOfValidPrices < ((prices.length * 3) / 5)) revert Oracle_OraclesUnreliable(); + if (mad > (median * spreadTolerance) / BPS) revert Oracle_PricesSpreadTooHigh(); + if (nrOfValidPrices < ((prices.length * 3) / 5)) revert Oracle_PricesUnreliable(); return mean; } @@ -123,6 +150,10 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { (uint256 mad, uint256 median) = getMAD(prices); bool[] memory isInvalid = new bool[](prices.length); for (uint256 i = 0; i < prices.length; i++) { + if (mad == 0) { + isInvalid[i] = prices[i] != median; + continue; + } int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); } @@ -174,7 +205,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { } function getMean(uint256[] memory prices, bool[] memory isInvalid) - public + internal pure returns (uint256 mean, uint256 nrValidPrices) { @@ -185,11 +216,12 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin { nrValidPrices++; } } - mean = sum / nrValidPrices; + + if (nrValidPrices > 0) mean = sum / nrValidPrices; } - function getPriceFeedPrice(address source, address target, uint256 baseAmount) public returns (uint256 price) { - return IPriceFeed(source).fetchPrice(target) * baseAmount; + function getPriceFeedPrice(address source, address target, uint256 amountIn) public returns (uint256 price) { + return IPriceFeed(source).fetchPrice(target) * amountIn; } // in the case contracts that inhrerit from this one are upgradeable diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index b62691c..dbb0419 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -14,26 +14,27 @@ import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol"; import {IERC20MetadataUpgradeable} from "oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {SafeERC20Upgradeable} from "oz-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; -import {OracleAggregator} from "./OracleAggregator.sol"; +import {OracleAggregator, OracleRoute, OracleKind, Oracle} from "./OracleAggregator.sol"; /** * @dev Strategy to compound rewards and liquidation collateral gains in the Ethos stability pool */ -contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { +contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { using ReaperMathUtils for uint256; using SafeERC20Upgradeable for IERC20MetadataUpgradeable; // constants uint256 constant MAXIMUM_ALLOWED_RELATIVE_CHANGE = 300; // 3% - uint256 public constant MAX_MAD_RELATIVE_TO_MEDIAN_BPS = 3000000; - uint256 public constant MAX_SCORE_BPS = 10000; + uint256 public constant SPREAD_TOLERANCE = 500; // 5% + uint256 public constant MAX_SCORE_BPS = 25_000; // / 2.5X MAD for price outlier detection // 3rd-party contract addresses IStabilityPool public stabilityPool; IPriceFeed public priceFeed; IERC20MetadataUpgradeable public usdc; ExchangeSettings public exchangeSettings; // Holds addresses to use Velo, UniV3 and Bal through Swapper + OracleAggregator public oracleAggregator; uint256 public constant ETHOS_DECIMALS = 18; // Decimals used by ETHOS uint256 public ernMinAmountOutBPS; // The max allowed slippage when trading in to ERN @@ -53,10 +54,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { address uniV2Router; } - struct Pools { - address stabilityPool; - } - struct Tokens { address want; address usdc; @@ -77,10 +74,10 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { address[] memory _multisigRoles, address[] memory _keepers, address _priceFeed, - address _uniV3TWAP, + address _oracleAggregator, OracleRoute[] calldata _ernForUsdcOracles, ExchangeSettings calldata _exchangeSettings, - Pools calldata _pools, + address _stabilityPool, Tokens calldata _tokens ) public initializer { require(_vault != address(0), "vault is 0 address"); @@ -90,25 +87,26 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { require(_tokens.want != address(0), "want is 0 address"); require(_priceFeed != address(0), "priceFeed is 0 address"); require(_tokens.usdc != address(0), "usdc is 0 address"); - require(_uniV3TWAP != address(0), "uniV3TWAP is 0 address"); require(_exchangeSettings.veloRouter != address(0), "veloRouter is 0 address"); require(_exchangeSettings.balVault != address(0), "balVault is 0 address"); require(_exchangeSettings.uniV3Router != address(0), "uniV3Router is 0 address"); require(_exchangeSettings.uniV2Router != address(0), "uniV2Router is 0 address"); - require(_pools.stabilityPool != address(0), "stabilityPool is 0 address"); + require(_stabilityPool != address(0), "stabilityPool is 0 address"); + require(_oracleAggregator != address(0), "oracleAggregator is 0 address"); __ReaperBaseStrategy_init(_vault, _swapper, _tokens.want, _strategists, _multisigRoles, _keepers); - stabilityPool = IStabilityPool(_pools.stabilityPool); + stabilityPool = IStabilityPool(_stabilityPool); priceFeed = IPriceFeed(_priceFeed); usdc = IERC20MetadataUpgradeable(_tokens.usdc); exchangeSettings = _exchangeSettings; + oracleAggregator = OracleAggregator(_oracleAggregator); updateErnMinAmountOutBPS(9800); usdcToErnExchange = ExchangeType.UniV3; compoundingFeeMarginBPS = 9950; updateOracles(_ernForUsdcOracles); - updateAcceptableTWAPBounds(980_000, 1_100_000); + updateAcceptableTWAPBounds(0.98 ether, 1.1 ether); } /** @@ -170,7 +168,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { if (shouldOverrideHarvestBlock) { return; } - uint128 usdcAmount = 1 ether; // 1 ERN + uint128 usdcAmount = 1e6; // 1 ERN uint256 ernAmount = _getErnAmountForUsdc(usdcAmount); if (ernAmount < acceptableTWAPLowerBound || ernAmount > acceptableTWAPUpperBound) { @@ -316,8 +314,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { */ function _getErnAmountForUsdc(uint256 _usdcAmount) internal returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - uint256[] memory prices = getTwapPrices(ernForUsdcOracles, _usdcAmount); - return getMeanPrice(prices, MAX_MAD_RELATIVE_TO_MEDIAN_BPS, MAX_SCORE_BPS); + uint256[] memory prices = oracleAggregator.getTwapPrices(ernForUsdcOracles, _usdcAmount); + return oracleAggregator.getMeanPrice(prices, SPREAD_TOLERANCE, MAX_SCORE_BPS); } } @@ -327,8 +325,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { */ function _getErnAmountForUsdcView(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - uint256[] memory prices = getTwapPricesView(ernForUsdcViewOracles, _usdcAmount); - return getMeanPrice(prices, MAX_MAD_RELATIVE_TO_MEDIAN_BPS, MAX_SCORE_BPS); + uint256[] memory prices = oracleAggregator.getTwapPricesView(ernForUsdcViewOracles, _usdcAmount); + return oracleAggregator.getMeanPrice(prices, SPREAD_TOLERANCE, MAX_SCORE_BPS); } } @@ -464,10 +462,14 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { */ function updateOracles(OracleRoute[] calldata newRoutes) public { _atLeastRole(DEFAULT_ADMIN_ROLE); - ernForUsdcOracles = newRoutes; + // ernForUsdcOracles = newRoutes; + delete ernForUsdcOracles; + for (uint256 i = 0; i < newRoutes.length; i++) { + ernForUsdcOracles.push(newRoutes[i]); + } // reset the view-only oracles) - ernForUsdcViewOracles = new OracleRoute[](0); + delete ernForUsdcViewOracles; // filter out the price feed oracles and set the view-only oracles for (uint256 i = 0; i < newRoutes.length; i++) { for (uint256 j = 0; j < newRoutes[i].oracles.length; j++) { @@ -496,8 +498,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4, OracleAggregator { */ function updateAcceptableTWAPBounds(uint256 _acceptableTWAPLowerBound, uint256 _acceptableTWAPUpperBound) public { _atLeastRole(DEFAULT_ADMIN_ROLE); - bool aboveMinLimit = _acceptableTWAPLowerBound >= 900_000; - bool belowMaxLimit = _acceptableTWAPUpperBound <= 1_100_000; + bool aboveMinLimit = _acceptableTWAPLowerBound >= 0.9 ether; + bool belowMaxLimit = _acceptableTWAPUpperBound <= 1.1 ether; bool lowerBoundBelowUpperBound = _acceptableTWAPLowerBound < _acceptableTWAPUpperBound; bool hasValidBounds = lowerBoundBelowUpperBound && aboveMinLimit && belowMaxLimit; diff --git a/src/interfaces/IBalancerTwapOracle.sol b/src/interfaces/IBalancerTwapOracle.sol new file mode 100644 index 0000000..59466bb --- /dev/null +++ b/src/interfaces/IBalancerTwapOracle.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import {IVault} from "../interfaces/IBalancerVault.sol"; + +/** + * @dev Interface for querying historical data from a Pool that can be used as a Price Oracle. + * + * This lets third parties retrieve average prices of tokens held by a Pool over a given period of time, as well as the + * price of the Pool share token (BPT) and invariant. Since the invariant is a sensible measure of Pool liquidity, it + * can be used to compare two different price sources, and choose the most liquid one. + * + * Once the oracle is fully initialized, all queries are guaranteed to succeed as long as they require no data that + * is not older than the largest safe query window. + */ +interface IBalancerTwapOracle { + /** + * @notice Returns the Balancer Vault + */ + function getVault() external view returns (IVault); + + function getPoolId() external view returns (bytes32); + + // The three values that can be queried: + // + // - PAIR_PRICE: the price of the tokens in the Pool, expressed as the price of the second token in units of the + // first token. For example, if token A is worth $2, and token B is worth $4, the pair price will be 2.0. + // Note that the price is computed *including* the tokens decimals. This means that the pair price of a Pool with + // DAI and USDC will be close to 1.0, despite DAI having 18 decimals and USDC 6. + // + // - BPT_PRICE: the price of the Pool share token (BPT), in units of the first token. + // Note that the price is computed *including* the tokens decimals. This means that the BPT price of a Pool with + // USDC in which BPT is worth $5 will be 5.0, despite the BPT having 18 decimals and USDC 6. + // + // - INVARIANT: the value of the Pool's invariant, which serves as a measure of its liquidity. + enum Variable { + PAIR_PRICE, + BPT_PRICE, + INVARIANT + } + + /** + * @dev Returns the time average weighted price corresponding to each of `queries`. Prices are represented as 18 + * decimal fixed point values. + */ + function getTimeWeightedAverage(OracleAverageQuery[] memory queries) external view returns (uint256[] memory results); + + /** + * @dev Returns latest sample of `variable`. Prices are represented as 18 decimal fixed point values. + */ + function getLatest(Variable variable) external view returns (uint256); + + /** + * @dev Information for a Time Weighted Average query. + * + * Each query computes the average over a window of duration `secs` seconds that ended `ago` seconds ago. For + * example, the average over the past 30 minutes is computed by settings secs to 1800 and ago to 0. If secs is 1800 + * and ago is 1800 as well, the average between 60 and 30 minutes ago is computed instead. + */ + struct OracleAverageQuery { + Variable variable; + uint256 secs; + uint256 ago; + } + + /** + * @dev Returns largest time window that can be safely queried, where 'safely' means the Oracle is guaranteed to be + * able to produce a result and not revert. + * + * If a query has a non-zero `ago` value, then `secs + ago` (the oldest point in time) must be smaller than this + * value for 'safe' queries. + */ + function getLargestSafeQueryWindow() external view returns (uint256); + + /** + * @dev Returns the accumulators corresponding to each of `queries`. + */ + function getPastAccumulators(OracleAccumulatorQuery[] memory queries) external view returns (int256[] memory results); + + /** + * @dev Information for an Accumulator query. + * + * Each query estimates the accumulator at a time `ago` seconds ago. + */ + struct OracleAccumulatorQuery { + Variable variable; + uint256 ago; + } +} diff --git a/src/interfaces/IBalancerVault.sol b/src/interfaces/IBalancerVault.sol new file mode 100644 index 0000000..eb4630c --- /dev/null +++ b/src/interfaces/IBalancerVault.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma experimental ABIEncoderV2; + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @dev This is an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero + * address sentinel value). We're just relying on the fact that `interface` can be used to declare new address-like + * types. + * + * This concept is unrelated to a Pool's Asset Managers. + */ +interface IAsset { +// solhint-disable-previous-line no-empty-blocks +} + +/** + * @dev Minimal interface for interacting with Balancer's vault. + */ +interface IVault { + /** + * @dev Called by users to join a Pool, which transfers tokens from `sender` into the Pool's balance. This will + * trigger custom Pool behavior, which will typically grant something in return to `recipient` - often tokenized + * Pool shares. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * The `assets` and `maxAmountsIn` arrays must have the same length, and each entry indicates the maximum amount + * to send for each asset. The amounts to send are decided by the Pool and not the Vault: it just enforces + * these maximums. + * + * If joining a Pool that holds WETH, it is possible to send ETH directly: the Vault will do the wrapping. To enable + * this mechanism, the IAsset sentinel value (the zero address) must be passed in the `assets` array instead of the + * WETH address. Note that it is not possible to combine ETH and WETH in the same join. Any excess ETH will be sent + * back to the caller (not the sender, which is important for relayers). + * + * `assets` must have the same length and order as the array returned by `getPoolTokens`. This prevents issues when + * interacting with Pools that register and deregister tokens frequently. If sending ETH however, the array must be + * sorted *before* replacing the WETH address with the ETH sentinel value (the zero address), which means the final + * `assets` array might not be sorted. Pools with no registered tokens cannot be joined. + * + * If `fromInternalBalance` is true, the caller's Internal Balance will be preferred: ERC20 transfers will only + * be made for the difference between the requested amount and Internal Balance (if any). Note that ETH cannot be + * withdrawn from Internal Balance: attempting to do so will trigger a revert. + * + * This causes the Vault to call the `IBasePool.onJoinPool` hook on the Pool's contract, where Pools implement + * their own custom logic. This typically requires additional information from the user (such as the expected number + * of Pool shares). This can be encoded in the `userData` argument, which is ignored by the Vault and passed + * directly to the Pool's contract, as is `recipient`. + * + * Emits a `PoolBalanceChanged` event. + */ + function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) external payable; + + struct JoinPoolRequest { + IAsset[] assets; + uint256[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; + } + + enum PoolSpecialization { + GENERAL, + MINIMAL_SWAP_INFO, + TWO_TOKEN + } + + /** + * @dev Returns a Pool's contract address and specialization setting. + */ + function getPool(bytes32 poolId) external view returns (address, PoolSpecialization); + + /** + * @dev Returns a Pool's registered tokens, the total balance for each, and the latest block when *any* of + * the tokens' `balances` changed. + * + * The order of the `tokens` array is the same order that will be used in `joinPool`, `exitPool`, as well as in all + * Pool hooks (where applicable). Calls to `registerTokens` and `deregisterTokens` may change this order. + * + * If a Pool only registers tokens once, and these are sorted in ascending order, they will be stored in the same + * order as passed to `registerTokens`. + * + * Total balances include both tokens held by the Vault and those withdrawn by the Pool's Asset Managers. These are + * the amounts used by joins, exits and swaps. For a detailed breakdown of token balances, use `getPoolTokenInfo` + * instead. + */ + function getPoolTokens(bytes32 poolId) external view returns (address[] memory tokens, uint256[] memory, uint256); + + /** + * @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the + * `recipient` account. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20 + * transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender` + * must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of + * `joinPool`. + * + * If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of + * transferred. This matches the behavior of `exitPool`. + * + * Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a + * revert. + */ + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + enum SwapKind { + GIVEN_IN, + GIVEN_OUT + } + + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is 'given in' (the number of tokens to send to the Pool is known), it returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is 'given out' (the number of tokens to take from the Pool is known), it returns the amount of tokens + * sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` struct. + * + * Emits a `Swap` event. + */ + function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline) external payable returns (uint256); + + /** + * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on + * the `kind` value. + * + * `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address). + * Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IAsset assetIn; + IAsset assetOut; + uint256 amount; + bytes userData; + } +} diff --git a/src/oracles/BalancerTwapMixin.sol b/src/oracles/BalancerTwapMixin.sol new file mode 100644 index 0000000..a336b3d --- /dev/null +++ b/src/oracles/BalancerTwapMixin.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL1.1 + +pragma solidity ^0.8.0; + +import {Math} from "oz/utils/math/Math.sol"; +import {IBalancerTwapOracle} from "../interfaces/IBalancerTwapOracle.sol"; +import {IVault} from "../interfaces/IBalancerVault.sol"; +import {ERC20} from "oz/token/ERC20/ERC20.sol"; // for decimals() + +contract BalancerTwapMixin { + error BalancerOracle__TWAPOracleNotReady(); + + function getBalancerPrice(address source, address tokenIn, uint32 period, uint256 amountIn) + public + view + returns (uint256) + { + IBalancerTwapOracle balancerTwapOracle = IBalancerTwapOracle(source); + // "PAIR_PRICE: the price of the tokens in the Pool, + // expressed as the price of the second token in units of the first token." + // "Note that the price is computed *including* the tokens decimals. This means that the pair price of a Pool with + // DAI and USDC will be close to 1.0, despite DAI having 18 decimals and USDC 6" + uint256 oraclePrice; + + // ensure the Balancer oracle can return a TWAP value for the specified window + { + uint256 largestSafeQueryWindow = balancerTwapOracle.getLargestSafeQueryWindow(); + if (period > largestSafeQueryWindow) revert BalancerOracle__TWAPOracleNotReady(); + } + + { + IBalancerTwapOracle.OracleAverageQuery[] memory queries = new IBalancerTwapOracle.OracleAverageQuery[](1); + queries[0] = IBalancerTwapOracle.OracleAverageQuery({ + variable: IBalancerTwapOracle.Variable.PAIR_PRICE, + secs: period, + ago: 0 + }); + oraclePrice = balancerTwapOracle.getTimeWeightedAverage(queries)[0]; + } + + // get target price + // must call the vault, as the pool may have improperly ordered tokens + IVault balVault = IVault(balancerTwapOracle.getVault()); + (address[] memory poolTokens,,) = balVault.getPoolTokens(balancerTwapOracle.getPoolId()); + bool tokenInToken0 = poolTokens[0] == tokenIn; + if (tokenInToken0) { + // price query returns the inverse, so we need to invert it + oraclePrice = Math.ceilDiv(1e18 * amountIn, oraclePrice); + } + + uint256 targetPrice = amountIn * oraclePrice / 1e18; + + // fix decimal precision + uint256 decimals0 = ERC20(poolTokens[0]).decimals(); + uint256 decimals1 = ERC20(poolTokens[1]).decimals(); + if (decimals0 >= decimals1) { + uint256 decimalDifference = decimals0 - decimals1; + if (tokenInToken0) { + return targetPrice / 10 ** decimalDifference; + } else { + return targetPrice * 10 ** decimalDifference; + } + } else if (decimals0 < decimals1) { + uint256 decimalDifference = decimals1 - decimals0; + if (tokenInToken0) { + return targetPrice * 10 ** decimalDifference; + } else { + return targetPrice / 10 ** decimalDifference; + } + } + } +} diff --git a/src/oracles/UniV3TwapMixin.sol b/src/oracles/UniV3TwapMixin.sol index 29ace79..c8297db 100644 --- a/src/oracles/UniV3TwapMixin.sol +++ b/src/oracles/UniV3TwapMixin.sol @@ -7,7 +7,7 @@ import {FullMath} from "univ3-core/libraries/FullMath.sol"; import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; contract UniV3TwapMixin { - function getUniV3Price(address source, address targetToken, uint32 period, uint256 baseAmount) + function getUniV3Price(address source, address tokenIn, uint32 period, uint256 amountIn) public view returns (uint256 price) @@ -25,19 +25,19 @@ contract UniV3TwapMixin { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); (address token0, address token1) = (IUniswapV3Pool(source).token0(), IUniswapV3Pool(source).token1()); - address baseToken = token0 < token1 ? token0 : token1; + address tokenOut = token0 > token1 ? token0 : token1; // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself if (sqrtRatioX96 <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; - price = baseToken < targetToken - ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192) - : FullMath.mulDiv(1 << 192, baseAmount, ratioX192); + price = tokenOut > tokenIn + ? FullMath.mulDiv(ratioX192, amountIn, 1 << 192) + : FullMath.mulDiv(1 << 192, amountIn, ratioX192); } else { uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); - price = baseToken < targetToken - ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128) - : FullMath.mulDiv(1 << 128, baseAmount, ratioX128); + price = tokenOut > tokenIn + ? FullMath.mulDiv(ratioX128, amountIn, 1 << 128) + : FullMath.mulDiv(1 << 128, amountIn, ratioX128); } } } diff --git a/src/oracles/VeloTwapMixin.sol b/src/oracles/VeloTwapMixin.sol index 62d8c3d..740b3fa 100644 --- a/src/oracles/VeloTwapMixin.sol +++ b/src/oracles/VeloTwapMixin.sol @@ -9,7 +9,7 @@ import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; contract VeloTwapMixin { uint256 constant VELO_OBSERVATION_PERIOD = 1800; - function getVeloPrice(address source, address target, uint32 period, uint256 baseAmount) + function getVeloPrice(address source, address tokenIn, uint32 period, uint256 amountIn) public view returns (uint256) @@ -48,7 +48,7 @@ contract VeloTwapMixin { uint112 reserve0 = safe112((current.reserve0Cumulative - last.reserve0Cumulative) / time); uint112 reserve1 = safe112((current.reserve1Cumulative - last.reserve1Cumulative) / time); - return _veloGetAmountOut(baseAmount, target, reserve0, reserve1, pair.stable(), pair); + return _veloGetAmountOut(amountIn, tokenIn, reserve0, reserve1, pair.stable(), pair); } /** @@ -163,7 +163,7 @@ contract VeloTwapMixin { } function safe112(uint256 n) private pure returns (uint112) { - if (n >= 2 ** 112) revert("lol"); + if (n > type(uint112).max) revert("safe112"); return uint112(n); } } diff --git a/test/OracleAggregatorTest.t.sol b/test/OracleAggregatorTest.t.sol new file mode 100644 index 0000000..456e54f --- /dev/null +++ b/test/OracleAggregatorTest.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OracleAggregator, OracleKind, OracleRoute, Oracle} from "src/OracleAggregator.sol"; +import {Math} from "oz/utils/math/Math.sol"; +import "forge-std/Test.sol"; + +struct TestCase { + uint256 expected; + uint256[] prices; + bool shouldRevert; +} + +contract OracleTest is Test { + using stdJson for string; + + OracleAggregator oracleAggregator; + + uint256 maxMadRelativeToMedianBPS = 500; // MADs can be at most 8% of the median + uint256 maxScoreBPS = 25_000; // prices that are 2.5x MAD away from the median are rejected + + function setUp() public { + oracleAggregator = new OracleAggregator(); + } + + /// Math related functions + + function test_revertHighSpread2Values(uint256 price1, uint256 price2) public { + // avoid prices above 2**128 + price1 = bound(price1, 1, type(uint128).max); // if a price is 0, the spread will be infinite + // make sure the prices are sufficiently apart + uint256 minPrice2 = Math.max(1, Math.ceilDiv(price1 * 111, 100)); + vm.assume(minPrice2 < type(uint128).max); + price2 = bound(price2, minPrice2, type(uint128).max); + + uint256[] memory prices = new uint256[](2); + prices[0] = price1; + prices[1] = price2; + vm.expectRevert(OracleAggregator.Oracle_PricesSpreadTooHigh.selector); + oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); + } + + function test_revertHighSpread3Values(uint256 price1, uint256 price2, uint256 price3) public { + // avoid prices above 2**128 + price1 = bound(price1, 0, type(uint128).max); + // make sure the prices are sufficiently apart + uint256 minPrice2 = Math.max(1, Math.ceilDiv(price1 * 110, 100)); + vm.assume(minPrice2 < type(uint128).max); + price2 = bound(price2, minPrice2, type(uint128).max); + uint256 minPrice3 = Math.max(1, Math.ceilDiv(price2 * 110, 100)); + vm.assume(minPrice3 < type(uint128).max); + price3 = bound(price3, minPrice3, type(uint128).max); + + uint256[] memory prices = new uint256[](3); + prices[0] = price1; + prices[1] = price2; + prices[2] = price3; + vm.expectRevert(OracleAggregator.Oracle_PricesSpreadTooHigh.selector); + oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); + } + + function test_ignoreOutliers(uint256 price1, uint256 price2, uint256 outlier) public { + price1 = bound(price1, 0, type(uint128).max); + price2 = bound(price2, Math.ceilDiv(price1 * 96, 100), price1 * 104 / 100); // 8% spread + uint256 mean = (price1 + price2) / 2; + outlier = bound(outlier, 0, type(uint128).max); + vm.assume(outlier < mean * 80 / 100 || outlier > mean * 120 / 100); // make sure the outlier is far from the mean + + uint256[] memory prices = new uint256[](3); + prices[0] = price1; + prices[1] = price2; + prices[2] = outlier; + uint256 result = oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); + + assertEq(result, mean, "Outlier should be ignored"); + } + + function test_testCases() public { + string memory json = vm.readFile("test/test_cases.json"); + TestCase[] memory testCases = abi.decode(json.parseRaw(".testCases"), (TestCase[])); + + for (uint256 i = 0; i < testCases.length; i++) { + TestCase memory testCase = testCases[i]; + uint256 result; + if (testCase.shouldRevert) { + vm.expectRevert(); + } + result = oracleAggregator.getMeanPrice(testCase.prices, maxMadRelativeToMedianBPS, maxScoreBPS); + + assertEq(result, testCase.expected, "Unexpected result"); + } + } +} diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.sol new file mode 100644 index 0000000..fe3c616 --- /dev/null +++ b/test/OraclesForkTests.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OracleAggregator, OracleKind, OracleRoute, Oracle} from "src/OracleAggregator.sol"; +import {ERC20} from "oz/token/ERC20/ERC20.sol"; +import {Math} from "oz/utils/math/Math.sol"; +import "forge-std/Test.sol"; + +contract OracleForkTests is Test { + uint256 opFork; + + OracleAggregator oracleAggregator; + + address WETH_OP_UNIV3_POOL = 0x68F5C0A2DE713a54991E01858Fd27a3832401849; + address WETH_OP_VELO_POOL = 0xd25711EdfBf747efCE181442Cc1D8F5F8fc8a0D3; + address USDC_ERN_VELO_POOL = 0x605cCE502dEe6BD201b493782e351e645D44abBB; + address USDC_ERN_UNIV3_POOL = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; + + address USDC_ADDRESS = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address ERN_ADDRESS = 0xc5b001DC33727F8F26880B184090D3E252470D45; + + address OP_ADDRESS = 0x4200000000000000000000000000000000000042; + address WETH_ADDRESS = 0x4200000000000000000000000000000000000006; + + function setUp() public { + opFork = vm.createSelectFork("https://go.getblock.io/bec4b0dd7017435c8880f2cae8ea2d4d", 118638228); + + oracleAggregator = new OracleAggregator(); + } + + function test_uniV3() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = + Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + + uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + assertEq(price, 1194216670556036888562); + + route.oracles[0] = + Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: OP_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + + uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e18); + assertEq(price2, 837368983916789); + } + + function test_velo() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = + Oracle({source: WETH_OP_VELO_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Velo}); + + uint256 expected = 1192245433864621830052; + + uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + assertEq(price, expected); + + route.oracles[0] = Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, period: 3600, kind: OracleKind.Velo}); + + uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e18); + assertEq(price2, 838020130098509); + } + + // velo stable pairs have a different pricing method + function test_veloStable() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: ERN_ADDRESS, period: 3600, kind: OracleKind.Velo}); + + uint256 expected = 982575; + + uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + assertEq(price, expected); + + route.oracles[0] = + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.Velo}); + + uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e6); + assertEq(price2, 1017733222640936418); + } + + function test_balancer() public { + address VMEX = 0x6D2E5b8841a6Aa5f0f973436357f75D3Eeb93312; + address VMEX_POOL = 0x4Dde571Dc66217a062e4B50f9b20c4D08b3245a0; + OracleRoute memory route; + + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, period: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + + // check decimal normalization + vm.mockCall(VMEX, abi.encodeWithSelector(ERC20.decimals.selector), abi.encode(6)); + + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, period: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + } + + function test_twoPrices() public { + OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); + + OracleRoute memory _veloOracle; + _veloOracle.oracles = new Oracle[](1); + _veloOracle.oracles[0] = + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.Velo}); + + OracleRoute memory _uniV3Oracle; + _uniV3Oracle.oracles = new Oracle[](1); + _uniV3Oracle.oracles[0] = + Oracle({source: USDC_ERN_UNIV3_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + + _ernForUsdcAllOracles[0] = _veloOracle; + _ernForUsdcAllOracles[1] = _uniV3Oracle; + + uint256[] memory prices = oracleAggregator.getTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); + + uint256 priceUniV3 = oracleAggregator.getMultiHopPriceView(_uniV3Oracle, 1e10); + uint256 priceVelo = oracleAggregator.getMultiHopPriceView(_veloOracle, 1e10); + + console.log("priceVelo", priceVelo); + console.log("prices1 ", prices[0]); + + console.log("priceUniV3", priceUniV3); + console.log("prices2 ", prices[1]); + } +} diff --git a/test/OraclesTest.t.solTODO b/test/OraclesTest.t.solTODO deleted file mode 100644 index 8f5035c..0000000 --- a/test/OraclesTest.t.solTODO +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import "src/ReaperStrategyStabilityPool.sol"; -import "vault-v2/ReaperVaultV2.sol"; -import {IERC20Upgradeable} from "oz-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; -import {IERC20} from "oz/token/ERC20/IERC20.sol"; -import {ReaperSwapper, MinAmountOutData, MinAmountOutKind} from "vault-v2/ReaperSwapper.sol"; -import {IVeloRouter} from "vault-v2/interfaces/IVeloRouter.sol"; - -contract TarotOracleTest is Test { - uint256 FORK_BLOCK = 115641661; - - ReaperVaultV2 public vault; - string public vaultName = "ERN Stability Pool Vault"; - string public vaultSymbol = "rf-SP-ERN"; - uint256 public vaultTvlCap = type(uint256).max; - address public treasuryAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; - address public strategistAddr = 0x1A20D7A31e5B3Bc5f02c8A146EF6f394502a10c4; - address public superAdminAddress = 0x9BC776dBb134Ef9D7014dB1823Cd755Ac5015203; - address public adminAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; - address public guardianAddress = 0xb0C9D5851deF8A2Aac4A23031CA2610f8C3483F9; - address public uniV3UsdcErnPool = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; - address public wantHolderAddr = strategistAddr; - address[] public strategists = [strategistAddr]; - address[] public multisigRoles = [superAdminAddress, adminAddress, guardianAddress]; - - address public balVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; - address public uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - address public uniV2Router = 0xbeeF000000000000000000000000000000000000; - address public uniV3TWAP = 0xB210CE856631EeEB767eFa666EC7C1C57738d438; - - address[] keepers = [ - 0xe0268Aa6d55FfE1AA7A77587e56784e5b29004A2, - 0x34Df14D42988e4Dc622e37dc318e70429336B6c5, - 0x73C882796Ea481fe0A2B8DE499d95e60ff971663, - 0x36a63324edFc157bE22CF63A6Bf1C3B49a0E72C0, - 0x9a2AdcbFb972e0EC2946A342f46895702930064F, - 0x7B540a4D24C906E5fB3d3EcD0Bb7B1aEd3823897, - 0x8456a746e09A18F9187E5babEe6C60211CA728D1, - 0x55a078AFC2e20C8c20d1aa4420710d827Ee494d4, - 0x5241F63D0C1f2970c45234a0F5b345036117E3C2, - 0xf58d534290Ce9fc4Ea639B8b9eE238Fe83d2efA6, - 0x5318250BD0b44D1740f47a5b6BE4F7fD5042682D, - 0x33D6cB7E91C62Dd6980F16D61e0cfae082CaBFCA, - 0x51263D56ec81B5e823e34d7665A1F505C327b014, - 0x87A5AfC8cdDa71B5054C698366E97DB2F3C2BC2f - ]; - - address public wethAddress = 0x4200000000000000000000000000000000000006; - address public wbtcAddress = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; - address public usdcAddress = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; - address public ernAddress = 0xc5b001DC33727F8F26880B184090D3E252470D45; - address public usdceAddress = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607; - address public veloRouter = 0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858; - address public veloFactoryV1 = 0x25CbdDb98b35ab1FF77413456B31EC81A6B6B746; - address public veloFactoryV2Default = 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a; - address public veloUsdcErnPool = 0x605cCE502dEe6BD201b493782e351e645D44abBB; - address public veloWethErnPool = 0xFFf37730744930Cb61Be34c0014068F4f1eC28cF; - address public wantAddress = ernAddress; - //address public veloUsdcErnPoolOLD = 0x5e4A183Fa83C52B1c55b11f2682f6a8421206633; - address public stabilityPoolAddress = 0x8B147A2d4Fc3598079C64b8BF9Ad2f776786CFed; - - address public priceFeedAddress = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; - address public priceFeedOwnerAddress = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C; - - bytes32 public balErnPoolId = 0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1; - - address public ernWhale = 0x223341f84E784f0cFD3e30438DBDF5Aa4384A8b9; - address public usdcWhale = 0xf491d040110384DBcf7F241fFE2A546513fD873d; - - uint256 public optimismFork; - - ReaperSwapper reaperSwapper; - ReaperStrategyStabilityPool implementation; - ReaperStrategyStabilityPool wrappedProxy; - - function setUp() public { - // Forking - string memory rpc = vm.envString("RPC"); - optimismFork = vm.createSelectFork(rpc, FORK_BLOCK); - assertEq(vm.activeFork(), optimismFork); - - /* Reaper deployment and configuration */ - ERC1967Proxy tmpProxy; - reaperSwapper = new ReaperSwapper(); - tmpProxy = new ERC1967Proxy(address(reaperSwapper), ""); - reaperSwapper = ReaperSwapper(address(tmpProxy)); - reaperSwapper.initialize(strategists, address(this), address(this)); - IVeloRouter.Route[] memory veloPath = new IVeloRouter.Route[](1); - veloPath[0] = IVeloRouter.Route(ernAddress, usdcAddress, true, veloFactoryV2Default); - reaperSwapper.updateVeloSwapPath(ernAddress, usdcAddress, address(veloRouter), veloPath); - veloPath[0] = IVeloRouter.Route(usdcAddress, ernAddress, true, veloFactoryV2Default); - reaperSwapper.updateVeloSwapPath(usdcAddress, ernAddress, address(veloRouter), veloPath); - - vault = new ReaperVaultV2( - wantAddress, vaultName, vaultSymbol, vaultTvlCap, treasuryAddress, strategists, multisigRoles - ); - - ReaperStrategyStabilityPool.ExchangeSettings memory exchangeSettings; - exchangeSettings.veloRouter = veloRouter; - exchangeSettings.balVault = balVault; - exchangeSettings.uniV3Router = uniV3Router; - exchangeSettings.uniV2Router = uniV2Router; - - ReaperStrategyStabilityPool.Pools memory pools; - pools.stabilityPool = stabilityPoolAddress; - pools.uniV3UsdcErnPool = uniV3UsdcErnPool; - pools.veloUsdcErnPool = veloUsdcErnPool; - pools.veloWethErnPool = veloWethErnPool; - - ReaperStrategyStabilityPool.Tokens memory tokens; - tokens.want = wantAddress; - tokens.usdc = usdcAddress; - tokens.weth = wethAddress; - - implementation = new ReaperStrategyStabilityPool(); - tmpProxy = new ERC1967Proxy(address(implementation), ""); - wrappedProxy = ReaperStrategyStabilityPool(address(tmpProxy)); - - wrappedProxy.initialize( - address(vault), - address(reaperSwapper), - strategists, - multisigRoles, - keepers, - priceFeedAddress, - uniV3TWAP, - exchangeSettings, - pools, - tokens - ); - } - - function testMultipleOracles_PositiveOnePrice(uint128 baseAmount, uint32 period) public { - baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); - period = uint32(bound(period, 2 days, 9 days)); - uint32 tolerance = 500; - uint256[] memory prices = new uint256[](1); - prices[0] = wrappedProxy.getErnAmountForUsdcVeloPoints(baseAmount, period); - console2.log("Price Velo: ", prices[0]); - uint256 finalPrice = wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); - console2.log("2.Price All: ", finalPrice); - uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 10_000; - uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 10_000; - assert(finalPrice < highBoundary && finalPrice > lowBoundary); - } - - function testMultipleOracles_Positive(uint128 baseAmount, uint32 period) public { - baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); - period = uint32(bound(period, 2 days, 9 days)); - uint32 tolerance = 500; - uint256[] memory prices = new uint256[](6); - prices[0] = wrappedProxy.getErnAmountForUsdcVeloWindow(baseAmount, period); - prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); - prices[2] = 1059945924123 * baseAmount; - prices[3] = 959945924123 * baseAmount; - prices[4] = 1009945924123 * baseAmount; - prices[5] = 0 * baseAmount; - console2.log("Price Velo: ", prices[0]); - console2.log("Price UniV3: ", prices[1]); - uint256 finalPrice_1 = wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); - console2.log("2.Price_1 All: ", finalPrice_1); - - uint256 finalPrice_2 = wrappedProxy.getErnAmountForUsdcAll(baseAmount, tolerance); - console2.log("2.Price_2 All: ", finalPrice_2); - uint256 highBoundary = (baseAmount * 1e12) + ((baseAmount * 1e12) * tolerance) / 10_000; - uint256 lowBoundary = (baseAmount * 1e12) - ((baseAmount * 1e12) * tolerance) / 10_000; - assert(finalPrice_1 < highBoundary && finalPrice_1 > lowBoundary); - assert(finalPrice_2 < highBoundary && finalPrice_2 > lowBoundary); - } - - function testMultipleOracles_Negative(uint128 baseAmount, uint32 period) public { - baseAmount = uint128(bound(baseAmount, 1, 10_000 ether)); - period = uint32(bound(period, 2 days, 9 days)); - uint32 tolerance = 200; // tolerance set to narrow values - uint256[] memory prices = new uint256[](6); - prices[0] = wrappedProxy.getErnAmountForUsdcVeloWindow(baseAmount, period); - prices[1] = wrappedProxy.getErnAmountForUsdcUniV3(baseAmount, period); - prices[2] = 1059945924123 * baseAmount; - prices[3] = 959945924123 * baseAmount; - prices[4] = 1009945924123 * baseAmount; - prices[5] = 0 * baseAmount; - console2.log("Price Velo: ", prices[0]); - console2.log("Price UniV3: ", prices[1]); - vm.expectRevert(bytes4(keccak256("CouldntDetermineMeanPrice()"))); - wrappedProxy.getErnAmountForUsdcAll(prices, tolerance); - } - - function testSeparaeteOracles() public { - console2.log("Velo 1 WETH = %d ERN", wrappedProxy.getErnAmountForWethVelo(1 ether, 2 days)); - console2.log("Chainlink 1 WETH = %d USDC", wrappedProxy.getUsdcAmountForWethUsingPriceFeeds()); - console2.log(wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days)); - console2.log(wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days)); - console2.log(wrappedProxy.getErnAmountForUsdcUniV3(1 ether, 2 days)); - console2.log(wrappedProxy.getErnAmountForUsdcVeloWeth(1 ether, 2 days)); - } - - function testOraclesOnPriceManipulationsComparison_ErnWhale() public { - uint256 amount = IERC20(ernAddress).balanceOf(ernWhale); - MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); - - uint256 result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("1. Result before swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("1. Result1 before swap: ", result); - vm.startPrank(ernWhale); - console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); - console2.log("1. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); - IERC20Upgradeable(ernAddress).approve(address(reaperSwapper), amount); - reaperSwapper.swapVelo(ernAddress, usdcAddress, amount, minAmountOutData, address(veloRouter)); - console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); - console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); - vm.warp(block.timestamp + 1 days); - IVeloPair(veloUsdcErnPool).sync(); // imitates vm.warp(block.timestamp + 1 days); - result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("2. Result (points) after 1 day from swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("2. Result (window) after 1 day from swap: ", result); - vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("2. Result (points) after 1 week from swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("2. Result (window) after 1 week from swap: ", result); - } - - function testOraclesOnPriceManipulationsComparison_UsdcWhale() public { - uint256 amount = IERC20(usdcAddress).balanceOf(usdcWhale); - MinAmountOutData memory minAmountOutData = MinAmountOutData(MinAmountOutKind.Absolute, 0); - - uint256 result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("1. Result before swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("1. Result1 before swap: ", result); - vm.startPrank(usdcWhale); - console2.log("1. Whale balance of ERN", IERC20(ernAddress).balanceOf(usdcWhale)); - console2.log("1. Whale balance of USDC", amount * 1e12); - IERC20Upgradeable(usdcAddress).approve(address(reaperSwapper), amount); - reaperSwapper.swapVelo(usdcAddress, ernAddress, amount, minAmountOutData, address(veloRouter)); - console2.log("2. Whale balance of ERN", IERC20(ernAddress).balanceOf(ernWhale)); - console2.log("2. Whale balance of USDC", IERC20(usdcAddress).balanceOf(ernWhale) * 1e12); - vm.warp(block.timestamp + 1 days); - IVeloPair(veloUsdcErnPool).sync(); // imitates vm.warp(block.timestamp + 1 days); - result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("2. Result (points) after 1 day from swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("2. Result (window) after 1 day from swap: ", result); - vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("2. Result (points) after 1 week from swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("2. Result (window) after 1 week from swap: ", result); - vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); //Imitates vm.warp(block.timestamp + 1 weeks); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - IVeloPair(veloUsdcErnPool).sync(); - result = wrappedProxy.getErnAmountForUsdcVeloPoints(1 ether, 2 days); - console2.log("2. Result (points) after 2 weeks from swap: ", result); - result = wrappedProxy.getErnAmountForUsdcVeloWindow(1 ether, 2 days); - console2.log("2. Result (window) after 2 weeks from swap: ", result); - } -} diff --git a/test/ReaperStrategyStabilityPool.t.solTODO b/test/ReaperStrategyStabilityPool.t.sol similarity index 93% rename from test/ReaperStrategyStabilityPool.t.solTODO rename to test/ReaperStrategyStabilityPool.t.sol index 63300a5..100fe7f 100644 --- a/test/ReaperStrategyStabilityPool.t.solTODO +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "src/ReaperStrategyStabilityPool.sol"; -import {ReaperSwapper, ISwapRouter, TransferHelper} from "vault-v2/ReaperSwapper.sol"; +import "vault-v2/ReaperSwapper.sol"; import "vault-v2/ReaperVaultV2.sol"; import "vault-v2/ReaperBaseStrategyv4.sol"; import "vault-v2/interfaces/ISwapper.sol"; @@ -19,6 +19,7 @@ import {IStaticOracle} from "src/interfaces/IStaticOracle.sol"; import {IERC20Mintable} from "src/interfaces/IERC20Mintable.sol"; import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; import {IERC20Upgradeable} from "oz-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {OracleAggregator, OracleRoute} from "src/OracleAggregator.sol"; contract ReaperStrategyStabilityPoolTest is Test { using stdStorage for StdStorage; @@ -28,7 +29,7 @@ contract ReaperStrategyStabilityPoolTest is Test { // Registry address public treasuryAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; - address public stabilityPoolAddress = 0x8B147A2d4Fc3598079C64b8BF9Ad2f776786CFed; + address public stabilityPoolAddress = 0xD839A111598d5e27BD8f7A1A18ce9Bf079F0c0a2; address public priceFeedAddress = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; address public priceFeedOwnerAddress = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C; address public troveManager = 0xd584A5E956106DB2fE74d56A0B14a9d64BE8DC93; @@ -39,7 +40,6 @@ contract ReaperStrategyStabilityPoolTest is Test { address public uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address public uniV2Router = 0xbeeF000000000000000000000000000000000000; // Any non-0 address when UniV2 router does not exist address public veloUsdcErnPool = 0x605cCE502dEe6BD201b493782e351e645D44abBB; - address public veloWethErnPool = 0xFFf37730744930Cb61Be34c0014068F4f1eC28cF; address public uniV3UsdcErnPool = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; address public chainlinkUsdcOracle = 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3; address public uniV3TWAP = 0xB210CE856631EeEB767eFa666EC7C1C57738d438; @@ -51,15 +51,15 @@ contract ReaperStrategyStabilityPoolTest is Test { address public wantAddress = 0xc5b001DC33727F8F26880B184090D3E252470D45; address public wethAddress = 0x4200000000000000000000000000000000000006; address public wbtcAddress = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; - address public usdcAddress = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607; - address public oathAddress = 0x39FdE572a18448F8139b7788099F0a0740f51205; + address public usdcAddress = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address public oathAddress = 0x00e1724885473B63bCE08a9f0a52F35b0979e35A; address public opAddress = 0x4200000000000000000000000000000000000042; address public strategistAddr = 0x1A20D7A31e5B3Bc5f02c8A146EF6f394502a10c4; address public wantHolderAddr = strategistAddr; - address public borrowerOperationsAddress = 0x0a4582d3d9ecBAb80a66DAd8A881BE3b771d3e5B; - address public oathOwner = 0x80A16016cC4A2E6a2CACA8a4a498b1699fF0f844; + address public borrowerOperationsAddress = 0xaA0B41B61f76587cf85155147d7F3B7725D14Eb3; // 0x0a4582d3d9ecBAb80a66DAd8A881BE3b771d3e5B; + address public oathOwner = 0xe432150cce91c13a887f7D836923d5597adD8E31; address public wbtcHolder = 0x85C31FFA3706d1cce9d525a00f1C7D4A2911754c; address public opHolder = 0x790b4086D106Eafd913e71843AED987eFE291c92; @@ -105,6 +105,7 @@ contract ReaperStrategyStabilityPoolTest is Test { ReaperStrategyStabilityPool public implementation; ERC1967Proxy public proxy; ReaperStrategyStabilityPool public wrappedProxy; + OracleAggregator public oracleAggregator; ISwapper public swapper; @@ -114,24 +115,17 @@ contract ReaperStrategyStabilityPoolTest is Test { function setUp() public { // Forking string memory rpc = vm.envString("RPC"); - optimismFork = vm.createSelectFork(rpc, 115641661); + optimismFork = vm.createSelectFork(rpc, 118851023 /*107994026*/ ); assertEq(vm.activeFork(), optimismFork); // // Deploying stuff - ReaperSwapper swapperImpl = new ReaperSwapper(); - ERC1967Proxy swapperProxy = new ERC1967Proxy(address(swapperImpl), ""); + ERC1967Proxy swapperProxy = new ERC1967Proxy(address(new ReaperSwapper()), ""); ReaperSwapper wrappedSwapperProxy = ReaperSwapper(address(swapperProxy)); wrappedSwapperProxy.initialize(strategists, guardianAddress, superAdminAddress); swapper = ISwapper(address(swapperProxy)); vault = new ReaperVaultV2( - wantAddress, - vaultName, - vaultSymbol, - vaultTvlCap, - treasuryAddress, - strategists, - multisigRoles + wantAddress, vaultName, vaultSymbol, vaultTvlCap, treasuryAddress, strategists, multisigRoles ); implementation = new ReaperStrategyStabilityPool(); proxy = new ERC1967Proxy(address(implementation), ""); @@ -143,12 +137,6 @@ contract ReaperStrategyStabilityPoolTest is Test { exchangeSettings.uniV3Router = uniV3Router; exchangeSettings.uniV2Router = uniV2Router; - ReaperStrategyStabilityPool.Pools memory pools; - pools.stabilityPool = stabilityPoolAddress; - pools.uniV3UsdcErnPool = uniV3UsdcErnPool; - pools.veloUsdcErnPool = veloUsdcErnPool; - pools.veloWethErnPool = veloWethErnPool; - address[] memory usdcErnPath = new address[](2); usdcErnPath[0] = usdcAddress; usdcErnPath[1] = wantAddress; @@ -156,9 +144,21 @@ contract ReaperStrategyStabilityPoolTest is Test { ReaperStrategyStabilityPool.Tokens memory tokens; tokens.want = wantAddress; tokens.usdc = usdcAddress; - tokens.weth = wethAddress; - uint256 allowedTWAPDiscrepancy = 500; + OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); + + OracleRoute memory _veloOracle; + _veloOracle.oracles = new Oracle[](1); + _veloOracle.oracles[0] = + Oracle({source: veloUsdcErnPool, tokenIn: usdcAddress, period: 3600, kind: OracleKind.Velo}); + + OracleRoute memory _uniV3Oracle; + _uniV3Oracle.oracles = new Oracle[](1); + _uniV3Oracle.oracles[0] = + Oracle({source: uniV3UsdcErnPool, tokenIn: usdcAddress, period: 3600, kind: OracleKind.UniV3}); + + _ernForUsdcAllOracles[0] = _veloOracle; + _ernForUsdcAllOracles[1] = _uniV3Oracle; wrappedProxy.initialize( address(vault), @@ -167,9 +167,10 @@ contract ReaperStrategyStabilityPoolTest is Test { multisigRoles, keepers, priceFeedAddress, - uniV3TWAP, + address(new OracleAggregator()), + _ernForUsdcAllOracles, exchangeSettings, - pools, + stabilityPoolAddress, tokens ); @@ -198,7 +199,7 @@ contract ReaperStrategyStabilityPoolTest is Test { vm.startPrank(strategistAddr); swapper.updateVeloSwapPath(usdcAddress, wantAddress, veloRouter, usdcErnRoute); swapper.updateUniV3SwapPath(usdcAddress, wantAddress, uniV3Router, usdcErnSwapData); - swapper.updateBalSwapPoolID(usdcAddress, wantAddress, balVault, balErnPoolId); + // swapper.updateBalSwapPoolID(usdcAddress, wantAddress, balVault, balErnPoolId); IVeloRouter.Route[] memory wethErnRoute = new IVeloRouter.Route[](2); wethErnRoute[0] = @@ -216,17 +217,19 @@ contract ReaperStrategyStabilityPoolTest is Test { IVeloRouter.Route[] memory oathErnRoute = new IVeloRouter.Route[](2); oathErnRoute[0] = - IVeloRouter.Route({from: oathAddress, to: usdcAddress, stable: false, factory: veloFactoryV2Default}); + IVeloRouter.Route({from: oathAddress, to: wethAddress, stable: false, factory: veloFactoryV2Default}); oathErnRoute[1] = - IVeloRouter.Route({from: usdcAddress, to: wantAddress, stable: true, factory: veloFactoryV2Default}); + IVeloRouter.Route({from: wethAddress, to: wantAddress, stable: false, factory: veloFactoryV2Default}); swapper.updateVeloSwapPath(oathAddress, wantAddress, veloRouter, oathErnRoute); IVeloRouter.Route[] memory oathUsdcRoute = new IVeloRouter.Route[](2); oathUsdcRoute[0] = - IVeloRouter.Route({from: oathAddress, to: usdcAddress, stable: false, factory: veloFactoryV2Default}); - //swapper.updateVeloSwapPath(oathAddress, usdcAddress, veloRouter, oathUsdcRoute); + IVeloRouter.Route({from: oathAddress, to: wethAddress, stable: false, factory: veloFactoryV2Default}); + oathUsdcRoute[1] = + IVeloRouter.Route({from: wethAddress, to: usdcAddress, stable: false, factory: veloFactoryV2Default}); + swapper.updateVeloSwapPath(oathAddress, usdcAddress, veloRouter, oathUsdcRoute); - swapper.updateBalSwapPoolID(oathAddress, usdcAddress, balVault, oatsAndGrainPoolId); + //swapper.updateBalSwapPoolID(oathAddress, usdcAddress, balVault, oatsAndGrainPoolId); address[] memory wethUsdcPath = new address[](2); wethUsdcPath[0] = wethAddress; @@ -295,11 +298,11 @@ contract ReaperStrategyStabilityPoolTest is Test { exchangeAddress: uniV3Router }); ReaperBaseStrategyv4.SwapStep memory step4 = ReaperBaseStrategyv4.SwapStep({ - exType: ReaperBaseStrategyv4.ExchangeType.Bal, + exType: ReaperBaseStrategyv4.ExchangeType.VeloSolid, start: oathAddress, end: usdcAddress, minAmountOutData: MinAmountOutData({kind: MinAmountOutKind.Absolute, absoluteOrBPSValue: 0}), - exchangeAddress: balVault + exchangeAddress: veloRouter }); ReaperBaseStrategyv4.SwapStep[] memory steps = new ReaperBaseStrategyv4.SwapStep[](4); steps[0] = step1; @@ -790,19 +793,20 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("poolBalanceBefore: ", poolBalanceBefore); console.log("poolBalanceAfter: ", poolBalanceAfter); - uint32 currentTwapPeriod = wrappedProxy.uniV3TWAPPeriod(); + /// @TODO + /* uint32 currentUniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod(); address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); uint256 priceQuote = IStaticOracle(uniV3TWAP).quoteSpecificPoolsWithTimePeriod( - uint128(usdcAmount), usdcAddress, wantAddress, pools, currentTwapPeriod + uint128(usdcAmount), usdcAddress, wantAddress, pools, currentUniV3TWAPPeriod ); // Values should be the same because the usdc balance will be valued // using the Velo TWAP - assertEq(valueInCollateralAfter, priceQuote); + assertEq(valueInCollateralAfter, priceQuote); */ uint256 compoundingFeeMarginBPS = wrappedProxy.compoundingFeeMarginBPS(); - uint256 expectedPoolBalance = (valueInCollateralAfter * compoundingFeeMarginBPS) / BPS_UNIT; + uint256 expectedPoolBalance = valueInCollateralAfter * compoundingFeeMarginBPS / BPS_UNIT; console.log("expectedPoolBalance: ", expectedPoolBalance); assertEq(poolBalanceAfter, expectedPoolBalance); } @@ -864,8 +868,8 @@ contract ReaperStrategyStabilityPoolTest is Test { // All usd values must have 18 decimals for comparison. // WETH and OP already have 18 decimals, but we need to scale WBTC. uint256 wbtcUsdValue = wbtcAmount * uint256(wbtcPrice) * (10 ** 2); - uint256 wethUsdValue = (wethAmount * uint256(wethPrice)) / (10 ** 8); - uint256 opUsdValue = (opAmount * uint256(opPrice)) / (10 ** 8); + uint256 wethUsdValue = wethAmount * uint256(wethPrice) / (10 ** 8); + uint256 opUsdValue = opAmount * uint256(opPrice) / (10 ** 8); uint256 expectedUsdValueInCollateral = wbtcUsdValue + wethUsdValue + opUsdValue; console.log("wbtcUsdValue: ", wbtcUsdValue); console.log("wethUsdValue: ", wethUsdValue); @@ -884,12 +888,13 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 usdcAmount = ((usdValueInCollateral / (10 ** 12)) * (10 ** 8)) / usdcPrice; console.log("usdcAmount: ", usdcAmount); - address[] memory pools = new address[](1); + /// @TODO + /* address[] memory pools = new address[](1); pools[0] = address(uniV3UsdcErnPool); - uint32 uniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod(); - console.log("uniV3TWAPPeriod: ", uniV3TWAPPeriod); + uint32 twapPeriod = wrappedProxy.uniV3TWAPPeriod(); + console.log("twapPeriod: ", twapPeriod); uint256 ernAmount = IStaticOracle(uniV3TWAP).quoteSpecificPoolsWithTimePeriod( - uint128(usdcAmount), usdcAddress, wantAddress, pools, uniV3TWAPPeriod + uint128(usdcAmount), usdcAddress, wantAddress, pools, twapPeriod ); uint256 wantValueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); @@ -898,7 +903,8 @@ contract ReaperStrategyStabilityPoolTest is Test { assertApproxEqRel(ernAmount, wantValueInCollateral, 1e8); uint256 compoundingFeeMarginBPS = wrappedProxy.compoundingFeeMarginBPS(); - uint256 expectedPoolIncrease = (ernAmount * compoundingFeeMarginBPS) / BPS_UNIT; + uint256 expectedPoolIncrease = ernAmount * compoundingFeeMarginBPS / BPS_UNIT; */ + // console.log("poolBalanceIncrease: ", poolBalanceAfter - poolBalanceBefore); // console.log("expectedPoolIncrease: ", expectedPoolIncrease); // assertEq(poolBalanceAfter - poolBalanceBefore, expectedPoolIncrease); @@ -936,7 +942,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 valueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); console.log("valueInCollateral: ", valueInCollateral); - uint256 newUsdcPrice = (usdcPrice * 9500) / BPS_UNIT; + uint256 newUsdcPrice = usdcPrice * 9500 / BPS_UNIT; vm.startPrank(usdcOracleOwner); mockChainlink.setPrice(int256(newUsdcPrice)); mockChainlink.setPrevPrice(int256(newUsdcPrice)); @@ -944,7 +950,7 @@ contract ReaperStrategyStabilityPoolTest is Test { // uint256 usdcPrice = uint256(usdcAggregator.latestAnswer()); // console.log("usdcPrice: ", usdcPrice); - uint256 expectedValueInCollateral = (valueInCollateral * 10_526) / BPS_UNIT; + uint256 expectedValueInCollateral = valueInCollateral * 10_526 / BPS_UNIT; valueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); console.log("expectedValueInCollateral: ", expectedValueInCollateral); console.log("valueInCollateral: ", valueInCollateral); @@ -979,14 +985,15 @@ contract ReaperStrategyStabilityPoolTest is Test { // console.log("priceQuote10: ", priceQuote10 / 1_000_000_000); // } - function testUniV3TWAPMultipleSwaps() public { + // @TODO + /* function testUniV3TWAPMultipleSwaps() public { uint128 usdcUnit = 10 ** 6; uint32 period = 120; uint256 timeToSkip = 20; uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = (usdcInPool * 9999) / 10_000; + uint256 usdcToDump = usdcInPool * 9999 / 10_000; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); @@ -1062,14 +1069,14 @@ contract ReaperStrategyStabilityPoolTest is Test { priceQuoteSpot = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, 0); console.log("priceQuote9: ", priceQuote); console.log("priceQuoteSpot9: ", priceQuoteSpot); - } + } */ function testUniV3TWAPSingleSwap() public { uint32 period = 3600; uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = (usdcInPool * 9999) / 10_000; + uint256 usdcToDump = usdcInPool * 9999 / 10_000; uint256 ernToDump = 10 * 1 ether; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); deal({token: wantAddress, to: address(this), give: ernToDump * 100}); @@ -1089,7 +1096,8 @@ contract ReaperStrategyStabilityPoolTest is Test { // _skipBlockAndTime(1); // } - uint256 priceQuote = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, period); + /// @TODO + /* uint256 priceQuote = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, period); console.log("priceQuote: ", priceQuote); @@ -1113,10 +1121,11 @@ contract ReaperStrategyStabilityPoolTest is Test { console.log("priceQuoteQuarter: ", priceQuoteQuarter); console.log("priceQuoteEigth: ", priceQuoteEigth); console.log("priceQuoteSixteenth: ", priceQuoteSixteenth); - console.log("priceQuoteSpot1: ", priceQuoteSpot); + console.log("priceQuoteSpot1: ", priceQuoteSpot); */ } - function testUpdateUniV3TWAPPeriod() public { + /* function testUpdateUniV3TWAPPeriod() public { + /// @TODO uint32 period = 36000; wrappedProxy.updateUniV3TWAPPeriod(period); @@ -1135,15 +1144,16 @@ contract ReaperStrategyStabilityPoolTest is Test { period = type(uint32).max; vm.expectRevert(bytes("OLD")); wrappedProxy.updateUniV3TWAPPeriod(period); - } + } */ - function testChangeTwapPeriod() public { + /* function testChangeTWAPPeriod() public { + /// @TODO uint32 oldPeriod = 36000; wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); uint256 usdcInPool = IERC20Upgradeable(usdcAddress).balanceOf(uniV3UsdcErnPool); console.log("usdcInPool: ", usdcInPool); - uint256 usdcToDump = (usdcInPool * 9999) / 10_000; + uint256 usdcToDump = usdcInPool * 9999 / 10_000; deal({token: usdcAddress, to: address(this), give: usdcToDump * 100}); uint256 nrOfSwaps = 100; @@ -1167,7 +1177,7 @@ contract ReaperStrategyStabilityPoolTest is Test { wrappedProxy.updateUniV3TWAPPeriod(newPeriod); vm.stopPrank(); wrappedProxy.updateUniV3TWAPPeriod(oldPeriod); - } + } */ function liquidateTroves(address asset) internal { ITroveManager(troveManager).liquidateTroves(asset, 100); @@ -1200,7 +1210,7 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 minAmountOut = 0; bytes memory pathBytes = _encodePathV3(path, fees); - TransferHelper.safeApprove(path[0], uniV3Router, _amount); + // TransferHelper.safeApprove(path[0], uniV3Router, _amount); ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ path: pathBytes, recipient: address(this), diff --git a/test/test_cases.json b/test/test_cases.json new file mode 100644 index 0000000..2798e94 --- /dev/null +++ b/test/test_cases.json @@ -0,0 +1,89 @@ +{ + "testCases": [ + { + "expected": 1005000000000000000, + "prices": [ + 1000000000000000000, + 1010000000000000000 + ], + "shouldRevert": false + }, + { + "expected": 5203360986091762542, + "prices": [ + 5204570984673230502, + 5202150987510294582 + ], + "shouldRevert": false + }, + { + "expected": 104, + "prices": [ + 100, + 109 + ], + "shouldRevert": false + }, + { + "expected": 0, + "prices": [ + 100, + 111 + ], + "shouldRevert": true + }, + { + "expected": 1010000000000000000000000000000, + "prices": [ + 1000000000000000000000000000000, + 1020000000000000000000000000000 + ], + "shouldRevert": false + }, + { + "expected": 1010000000000000000, + "prices": [ + 1000000000000000000, + 1010000000000000000, + 1020000000000000000 + ], + "shouldRevert": false + }, + { + "expected": 10250, + "prices": [ + 10000, + 10500, + 100000000000 + ], + "shouldRevert": false + }, + { + "expected": 0, + "prices": [ + 10000, + 10600, + 100000000000 + ], + "shouldRevert": true + }, + { + "expected": 23929806355460, + "prices": [ + 23409284234987, + 24450328475934, + 99999999999999999999 + ], + "shouldRevert": false + }, + { + "expected": 0, + "prices": [ + 22409284234987, + 24850328475934, + 26502394802349 + ], + "shouldRevert": true + } + ] +} \ No newline at end of file From 7872c097bb2848f6d4199bb40c3afd5494f729eb Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:16:51 -0300 Subject: [PATCH 12/18] minor fixes --- src/OracleAggregator.sol | 5 +++-- src/ReaperStrategyStabilityPool.sol | 1 - test/OraclesForkTests.sol | 13 +++++++++++++ test/ReaperStrategyStabilityPool.t.sol | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index afe6b43..586a21f 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -6,6 +6,7 @@ import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; import {BalancerTwapMixin} from "./oracles/BalancerTwapMixin.sol"; import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; +import {ERC20} from "oz/token/ERC20/ERC20.sol"; // has decimals(), as opposed to IERC20 import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; @@ -82,7 +83,6 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { } else { price = _getPrice(route.oracles[i], amountIn); } - price = _getPrice(route.oracles[i], amountIn); amountIn = price; } } @@ -220,8 +220,9 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { if (nrValidPrices > 0) mean = sum / nrValidPrices; } + // always returns usd value function getPriceFeedPrice(address source, address target, uint256 amountIn) public returns (uint256 price) { - return IPriceFeed(source).fetchPrice(target) * amountIn; + return IPriceFeed(source).fetchPrice(target) * amountIn / (10 ** ERC20(target).decimals()); } // in the case contracts that inhrerit from this one are upgradeable diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index dbb0419..28f5c01 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -25,7 +25,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { // constants - uint256 constant MAXIMUM_ALLOWED_RELATIVE_CHANGE = 300; // 3% uint256 public constant SPREAD_TOLERANCE = 500; // 5% uint256 public constant MAX_SCORE_BPS = 25_000; // / 2.5X MAD for price outlier detection diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.sol index fe3c616..6b7d907 100644 --- a/test/OraclesForkTests.sol +++ b/test/OraclesForkTests.sol @@ -22,6 +22,9 @@ contract OracleForkTests is Test { address OP_ADDRESS = 0x4200000000000000000000000000000000000042; address WETH_ADDRESS = 0x4200000000000000000000000000000000000006; + address PRICE_FEED = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; + address WBTC_ADDRESS = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; + function setUp() public { opFork = vm.createSelectFork("https://go.getblock.io/bec4b0dd7017435c8880f2cae8ea2d4d", 118638228); @@ -103,6 +106,16 @@ contract OracleForkTests is Test { emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); } + function test_priceFeed() public { + OracleRoute memory route; + + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, period: 0, kind: OracleKind.PriceFeed}); + + uint256 price = oracleAggregator.getMultiHopPrice(route, 1e8); + console.log("price", price); + } + function test_twoPrices() public { OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol index 100fe7f..514f45e 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -661,6 +661,7 @@ contract ReaperStrategyStabilityPoolTest is Test { vaultWantBalance = want.balanceOf(address(vault)); strategyBalance = wrappedProxy.balanceOf(); assertGt(vaultBalance, depositAmount); + console.log(vaultBalance, depositAmount); assertGt(vaultWantBalance, 30 ether); assertEq(strategyBalance, 70 ether); From 687524627a660624406d03e9d2d94c9ef4b86770 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Tue, 14 May 2024 16:55:15 -0300 Subject: [PATCH 13/18] standardize return variable names --- src/OracleAggregator.sol | 26 ++++++++++---------------- src/oracles/BalancerTwapMixin.sol | 10 +++++----- src/oracles/VeloTwapMixin.sol | 4 ++-- test/OraclesForkTests.sol | 8 +++++++- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 586a21f..87578e1 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -32,7 +32,6 @@ struct Oracle { // making averages between the results, for more reliable prices. // Has support for multiple oracles contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { - error Oracle_VeloOverflow(); error Oracle_InvalidKind(); error Oracle_PricesSpreadTooHigh(); error Oracle_PricesUnreliable(); @@ -117,7 +116,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { function getMeanPrice(uint256[] memory prices, uint256 spreadTolerance, uint256 maxScoreBPS) external pure - returns (uint256) + returns (uint256 mean) { if (prices.length == 1) return prices[0]; if (prices.length == 2) { @@ -131,7 +130,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { return (prices[0] + prices[1]) / 2; } (bool[] memory isInvalid, uint256 mad, uint256 median) = getValidityByZScore(prices, maxScoreBPS); - (uint256 mean, uint256 nrOfValidPrices) = getMean(prices, isInvalid); + uint256 nrOfValidPrices; + (mean, nrOfValidPrices) = getMean(prices, isInvalid); if (mad > (median * spreadTolerance) / BPS) revert Oracle_PricesSpreadTooHigh(); if (nrOfValidPrices < ((prices.length * 3) / 5)) revert Oracle_PricesUnreliable(); return mean; @@ -139,27 +139,24 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @param prices List of prices to be checked /// @param maxScoreBPS If a price has a Z-score higher than this, it's considered an outlier and filtered out - /// @return An array mask for the prices array, where true means the price is invalid - /// @return The MAD - Median Absolute Deviation - /// @return The median of the prices + /// @return isInvalid An array mask for the prices array, where true means the price is invalid + /// @return mad The MAD - Median Absolute Deviation + /// @return median The median of the prices function getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) public pure - returns (bool[] memory, uint256, uint256) + returns (bool[] memory isInvalid, uint256 mad, uint256 median) { - (uint256 mad, uint256 median) = getMAD(prices); - bool[] memory isInvalid = new bool[](prices.length); + (mad, median) = getMAD(prices); + isInvalid = new bool[](prices.length); for (uint256 i = 0; i < prices.length; i++) { - if (mad == 0) { - isInvalid[i] = prices[i] != median; - continue; - } int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); } return (isInvalid, mad, median); } + // https://ethereum.stackexchange.com/questions/1517/sorting-an-array-of-integer-with-ethereum function quickSort(uint256[] memory arr, int256 left, int256 right) internal pure { int256 i = left; int256 j = right; @@ -224,7 +221,4 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { function getPriceFeedPrice(address source, address target, uint256 amountIn) public returns (uint256 price) { return IPriceFeed(source).fetchPrice(target) * amountIn / (10 ** ERC20(target).decimals()); } - - // in the case contracts that inhrerit from this one are upgradeable - uint256[50] private __gap; } diff --git a/src/oracles/BalancerTwapMixin.sol b/src/oracles/BalancerTwapMixin.sol index a336b3d..19c6541 100644 --- a/src/oracles/BalancerTwapMixin.sol +++ b/src/oracles/BalancerTwapMixin.sol @@ -13,7 +13,7 @@ contract BalancerTwapMixin { function getBalancerPrice(address source, address tokenIn, uint32 period, uint256 amountIn) public view - returns (uint256) + returns (uint256 price) { IBalancerTwapOracle balancerTwapOracle = IBalancerTwapOracle(source); // "PAIR_PRICE: the price of the tokens in the Pool, @@ -56,16 +56,16 @@ contract BalancerTwapMixin { if (decimals0 >= decimals1) { uint256 decimalDifference = decimals0 - decimals1; if (tokenInToken0) { - return targetPrice / 10 ** decimalDifference; + price = targetPrice / 10 ** decimalDifference; } else { - return targetPrice * 10 ** decimalDifference; + price = targetPrice * 10 ** decimalDifference; } } else if (decimals0 < decimals1) { uint256 decimalDifference = decimals1 - decimals0; if (tokenInToken0) { - return targetPrice * 10 ** decimalDifference; + price = targetPrice * 10 ** decimalDifference; } else { - return targetPrice / 10 ** decimalDifference; + price = targetPrice / 10 ** decimalDifference; } } } diff --git a/src/oracles/VeloTwapMixin.sol b/src/oracles/VeloTwapMixin.sol index 740b3fa..39f73bb 100644 --- a/src/oracles/VeloTwapMixin.sol +++ b/src/oracles/VeloTwapMixin.sol @@ -12,7 +12,7 @@ contract VeloTwapMixin { function getVeloPrice(address source, address tokenIn, uint32 period, uint256 amountIn) public view - returns (uint256) + returns (uint256 price) { IVeloPair pair = IVeloPair(source); Cumulatives memory current = pair.currentCumulativePrices(); @@ -48,7 +48,7 @@ contract VeloTwapMixin { uint112 reserve0 = safe112((current.reserve0Cumulative - last.reserve0Cumulative) / time); uint112 reserve1 = safe112((current.reserve1Cumulative - last.reserve1Cumulative) / time); - return _veloGetAmountOut(amountIn, tokenIn, reserve0, reserve1, pair.stable(), pair); + price = _veloGetAmountOut(amountIn, tokenIn, reserve0, reserve1, pair.stable(), pair); } /** diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.sol index 6b7d907..0d14c03 100644 --- a/test/OraclesForkTests.sol +++ b/test/OraclesForkTests.sol @@ -117,7 +117,7 @@ contract OracleForkTests is Test { } function test_twoPrices() public { - OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); + OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](3); OracleRoute memory _veloOracle; _veloOracle.oracles = new Oracle[](1); @@ -129,8 +129,14 @@ contract OracleForkTests is Test { _uniV3Oracle.oracles[0] = Oracle({source: USDC_ERN_UNIV3_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + OracleRoute memory _priceFeedOracle; + _priceFeedOracle.oracles = new Oracle[](1); + _priceFeedOracle.oracles[0] = + Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, period: 0, kind: OracleKind.PriceFeed}); + _ernForUsdcAllOracles[0] = _veloOracle; _ernForUsdcAllOracles[1] = _uniV3Oracle; + _ernForUsdcAllOracles[2] = _priceFeedOracle; uint256[] memory prices = oracleAggregator.getTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); From 51b3d3eebbc9d719668678b4b748676f886cb243 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Thu, 16 May 2024 16:10:08 -0300 Subject: [PATCH 14/18] change number of good prices required --- src/OracleAggregator.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 87578e1..79d0564 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -133,7 +133,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { uint256 nrOfValidPrices; (mean, nrOfValidPrices) = getMean(prices, isInvalid); if (mad > (median * spreadTolerance) / BPS) revert Oracle_PricesSpreadTooHigh(); - if (nrOfValidPrices < ((prices.length * 3) / 5)) revert Oracle_PricesUnreliable(); + // if more than 1/3 of the prices are invalid, the whole list is considered unreliable + if ((prices.length - nrOfValidPrices) > ((prices.length) / 3)) revert Oracle_PricesUnreliable(); return mean; } From 4e6486004ed3647e628c9cd680c175327f9c3658 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Thu, 23 May 2024 18:28:54 -0300 Subject: [PATCH 15/18] rework oracle + refactor --- src/OracleAggregator.sol | 143 +++++++++++++---------- src/ReaperStrategyStabilityPool.sol | 43 +------ src/interfaces/AggregatorV3Interface.sol | 20 ++++ src/interfaces/ICommunityIssuance.sol | 7 ++ src/oracles/BalancerTwapMixin.sol | 6 +- src/oracles/UniV3TwapMixin.sol | 6 +- src/oracles/VeloTwapMixin.sol | 123 +++++++++++++------ test/OracleAggregatorTest.t.sol | 20 +--- test/OraclesForkTests.sol | 83 +++++++------ test/ReaperStrategyStabilityPool.t.sol | 84 +++++++------ test/test_cases.json | 40 ------- 11 files changed, 307 insertions(+), 268 deletions(-) create mode 100644 src/interfaces/AggregatorV3Interface.sol create mode 100644 src/interfaces/ICommunityIssuance.sol diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 79d0564..40e4815 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -5,16 +5,15 @@ pragma solidity ^0.8.0; import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; import {BalancerTwapMixin} from "./oracles/BalancerTwapMixin.sol"; -import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; +import {AggregatorV3Interface} from "./interfaces/AggregatorV3Interface.sol"; import {ERC20} from "oz/token/ERC20/ERC20.sol"; // has decimals(), as opposed to IERC20 - import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; enum OracleKind { Velo, UniV3, Balancer, - PriceFeed + Chainlink } struct OracleRoute { @@ -24,7 +23,7 @@ struct OracleRoute { struct Oracle { address source; address tokenIn; - uint256 period; + uint256 windowOrDecimalOffset; OracleKind kind; } @@ -38,98 +37,109 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { uint256 constant BPS = 10_000; - function getTwapPricesView(OracleRoute[] memory oracles, uint256 amountIn) + // @notice Fetches the mean price of a list of oracles, filtering out outliers + // and checking if the prices are reliable. + function getReliablePrice(OracleRoute[] memory oracles, uint256 amountIn, uint256 spreadTolerance, uint256 maxScoreBPS) + external + view + returns (uint256 price) + { + uint256[] memory prices = new uint256[](oracles.length); + for (uint256 i = 0; i < oracles.length; i++) { + prices[i] = _fetchMultiHopPrice(oracles[i], amountIn, false); + } + if (prices.length == 1) { + return prices[0]; + } + if (prices.length == 2) { + uint256[] memory delayedPrices = new uint256[](2); + for (uint256 i = 0; i < oracles.length; i++) { + delayedPrices[i] = _fetchMultiHopPrice(oracles[i], amountIn, true); + } + (uint256 delayedMean, ) = getMean(delayedPrices, new bool[](2)); + uint256[] memory combinedPrices = new uint256[](3); + combinedPrices[0] = prices[0]; + combinedPrices[2] = delayedMean; + combinedPrices[1] = prices[1]; + return _getValidatedMeanPrice(combinedPrices, spreadTolerance, maxScoreBPS); + } + return _getValidatedMeanPrice(prices, spreadTolerance, maxScoreBPS); + } + + // @notice Fetches the prices without performing any validation + function fetchTwapPrices(OracleRoute[] memory oracles, uint256 amountIn) external view returns (uint256[] memory prices) { prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { - prices[i] = getMultiHopPriceView(oracles[i], amountIn); + prices[i] = _fetchMultiHopPrice(oracles[i], amountIn, false); } } /// @param route List of oracles for multihop price /// @param amountIn Input amount of the base token - function getMultiHopPriceView(OracleRoute memory route, uint256 amountIn) public view returns (uint256 price) { + function fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn) external view returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { - price = _getPrice(route.oracles[i], amountIn); + price = _fetchPrice(route.oracles[i], amountIn, false); amountIn = price; } } - /** - * state-changing versions of the above functions - * This allows the Kind of oracle to be PriceFeed - */ - function getTwapPrices(OracleRoute[] memory oracles, uint256 amountIn) external returns (uint256[] memory prices) { - prices = new uint256[](oracles.length); - for (uint256 i = 0; i < oracles.length; i++) { - prices[i] = _getMultiHopPrice(oracles[i], amountIn); - } - } - - function getMultiHopPrice(OracleRoute memory route, uint256 amountIn) external returns (uint256 price) { - return _getMultiHopPrice(route, amountIn); + function fetchPrice(Oracle memory oracle, uint256 amountIn) external view returns (uint256 price) { + return _fetchPrice(oracle, amountIn, false); } /// @param route List of oracles for multihop price /// @param amountIn Input amount of the base token - function _getMultiHopPrice(OracleRoute memory route, uint256 amountIn) internal returns (uint256 price) { + function _fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn, bool delayWindow) internal view returns (uint256 price) { for (uint256 i = 0; i < route.oracles.length; i++) { - if (route.oracles[i].kind == OracleKind.PriceFeed) { - price = getPriceFeedPrice(route.oracles[i].source, route.oracles[i].tokenIn, amountIn); - } else { - price = _getPrice(route.oracles[i], amountIn); - } + price = _fetchPrice(route.oracles[i], amountIn, delayWindow); amountIn = price; } } - function getPrice(Oracle memory oracle, uint256 amountIn) external view returns (uint256 price) { - return _getPrice(oracle, amountIn); - } - /// @param oracle Kind of oracle to use -- see OracleKind /// @param amountIn Input amount of the base token - function _getPrice(Oracle memory oracle, uint256 amountIn) internal view returns (uint256 price) { + /// @param delayWindow If true, the price is calculated with a delayed window - used for 2 price comparisons - incompatible with Chainlink + function _fetchPrice(Oracle memory oracle, uint256 amountIn, bool delayWindow) internal view returns (uint256 price) { + uint32 period; + uint32 ago; + if (delayWindow) { + period = uint32(oracle.windowOrDecimalOffset * 2); + ago = uint32(oracle.windowOrDecimalOffset); + } else { + period = uint32(oracle.windowOrDecimalOffset); + ago = 0; + } + if (oracle.kind == OracleKind.Velo) { - return getVeloPrice(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); + return getVeloPrice(oracle.source, oracle.tokenIn, period, ago, amountIn); } else if (oracle.kind == OracleKind.UniV3) { - return getUniV3Price(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); + return getUniV3Price(oracle.source, oracle.tokenIn, period, ago, amountIn); } else if (oracle.kind == OracleKind.Balancer) { - return getBalancerPrice(oracle.source, oracle.tokenIn, uint32(oracle.period), amountIn); + return getBalancerPrice(oracle.source, oracle.tokenIn, period, ago, amountIn); + } else if (oracle.kind == OracleKind.Chainlink) { + return getChainlinkPrice(oracle.source, oracle.windowOrDecimalOffset, oracle.tokenIn, amountIn); } else { revert Oracle_InvalidKind(); } } - /// @notice Get the mean price of a list of prices, filtering out outliers + /// @notice Get the mean price of a list of prices, filtering out outliers from 3+ price lists. /// @param prices List of prices - /// @param spreadTolerance Is used in two ways: - /// 2 prices: The value will be multiplied by 2, and will be how many - /// BPS the difference between the two prices can be. - /// 3+ prices: How many BPS the MAD can be relative to the median. + /// @param spreadTolerance The spread tolerance in BPS + /// How many BPS the MAD can be relative to the median. /// For example, a MAD higher than 10% of the median means the prices are too spread out, /// and the whole list is considered unreliable. /// @param maxScoreBPS If a price has a Z-score higher than this, it's considered an outlier and filtered out - function getMeanPrice(uint256[] memory prices, uint256 spreadTolerance, uint256 maxScoreBPS) - external + function _getValidatedMeanPrice(uint256[] memory prices, uint256 spreadTolerance, uint256 maxScoreBPS) + public pure returns (uint256 mean) { - if (prices.length == 1) return prices[0]; - if (prices.length == 2) { - if (prices[0] == 0 || prices[1] == 0) revert Oracle_PricesUnreliable(); - spreadTolerance = spreadTolerance * 2; - if (prices[0] > prices[1]) { - if (prices[0] > (prices[1] * (BPS + spreadTolerance)) / BPS) revert Oracle_PricesSpreadTooHigh(); - } else { - if (prices[1] > (prices[0] * (BPS + spreadTolerance)) / BPS) revert Oracle_PricesSpreadTooHigh(); - } - return (prices[0] + prices[1]) / 2; - } - (bool[] memory isInvalid, uint256 mad, uint256 median) = getValidityByZScore(prices, maxScoreBPS); + (bool[] memory isInvalid, uint256 mad, uint256 median) = _getValidityByZScore(prices, maxScoreBPS); uint256 nrOfValidPrices; (mean, nrOfValidPrices) = getMean(prices, isInvalid); if (mad > (median * spreadTolerance) / BPS) revert Oracle_PricesSpreadTooHigh(); @@ -143,8 +153,8 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @return isInvalid An array mask for the prices array, where true means the price is invalid /// @return mad The MAD - Median Absolute Deviation /// @return median The median of the prices - function getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) - public + function _getValidityByZScore(uint256[] memory prices, uint256 maxScoreBPS) + internal pure returns (bool[] memory isInvalid, uint256 mad, uint256 median) { @@ -182,7 +192,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @notice Get the Median Absolute Deviation of a list of values /// @param arr List of values - function getMAD(uint256[] memory arr) public pure returns (uint256 mad, uint256 median) { + function getMAD(uint256[] memory arr) internal pure returns (uint256 mad, uint256 median) { uint256 n = arr.length; quickSort(arr, 0, int256(n - 1)); if (n % 2 == 0) { @@ -218,8 +228,21 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { if (nrValidPrices > 0) mean = sum / nrValidPrices; } - // always returns usd value - function getPriceFeedPrice(address source, address target, uint256 amountIn) public returns (uint256 price) { - return IPriceFeed(source).fetchPrice(target) * amountIn / (10 ** ERC20(target).decimals()); + + // @notice Fetches the price from a Chainlink oracle + // @param source Chainlink oracle address + // @param tokenIn address(0) for price, address(1) for inverted price + // @param decimalOffset Difference between tokenIn and tokenOut decimals + // @param amountIn Input amount of the base token + function getChainlinkPrice(address source, uint256 decimalOffset, address tokenIn, uint256 amountIn) internal view returns (uint256 price) { + AggregatorV3Interface chainlinkOracle = AggregatorV3Interface(source); + (, int256 answer, , , ) = chainlinkOracle.latestRoundData(); + uint8 chainlinkDecimals = chainlinkOracle.decimals(); + if (tokenIn == address(0)) { + price = amountIn * uint256(answer) / (10**uint256(chainlinkDecimals)) / (10**decimalOffset); + } else { + price = amountIn * (10**uint256(chainlinkDecimals)) / uint256(answer) * (10**decimalOffset); + } } + } diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index 28f5c01..bb50b79 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -163,7 +163,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { } } - function _revertOnTWAPOutsideRange() internal { + function _revertOnTWAPOutsideRange() internal view { if (shouldOverrideHarvestBlock) { return; } @@ -238,12 +238,12 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { /** * @dev Calculates the estimated ERN value of collateral and USDC using Chainlink oracles - * and the set TWAP oracles - uses only view functions. + * and the set TWAP oracles. */ function getERNValueOfCollateralGain() public view returns (uint256 ernValueOfCollateral) { uint256 usdValueOfCollateralGain = getUSDValueOfCollateralGain(); uint256 totalUsdcValue = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain); - ernValueOfCollateral = _getErnAmountForUsdcView(totalUsdcValue); + ernValueOfCollateral = _getErnAmountForUsdc(totalUsdcValue); } /** @@ -311,31 +311,12 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { * @dev Returns the {expectedErnAmount} for the specified {_usdcAmount} of USDC using * TWAPs. */ - function _getErnAmountForUsdc(uint256 _usdcAmount) internal returns (uint256 expectedErnAmount) { - if (_usdcAmount != 0) { - uint256[] memory prices = oracleAggregator.getTwapPrices(ernForUsdcOracles, _usdcAmount); - return oracleAggregator.getMeanPrice(prices, SPREAD_TOLERANCE, MAX_SCORE_BPS); - } - } - - /** - * @dev Returns the {expectedErnAmount} for the specified {_usdcAmount} of USDC using - * the UniV3 TWAP. - */ - function _getErnAmountForUsdcView(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { + function _getErnAmountForUsdc(uint256 _usdcAmount) internal view returns (uint256 expectedErnAmount) { if (_usdcAmount != 0) { - uint256[] memory prices = oracleAggregator.getTwapPricesView(ernForUsdcViewOracles, _usdcAmount); - return oracleAggregator.getMeanPrice(prices, SPREAD_TOLERANCE, MAX_SCORE_BPS); + return oracleAggregator.getReliablePrice(ernForUsdcOracles, _usdcAmount, SPREAD_TOLERANCE, MAX_SCORE_BPS); } } - /** - * @dev See above. - */ - function getErnAmountForUsdcView(uint256 _usdcAmount) external view returns (uint256) { - return _getErnAmountForUsdcView(_usdcAmount); - } - /** * @dev Returns USD equivalent of {_amount} of {_collateral} with 18 digits of decimal precision. * The precision of {_amount} is whatever {_collateral}'s native decimals are (ex. 8 for wBTC) @@ -466,20 +447,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { for (uint256 i = 0; i < newRoutes.length; i++) { ernForUsdcOracles.push(newRoutes[i]); } - - // reset the view-only oracles) - delete ernForUsdcViewOracles; - // filter out the price feed oracles and set the view-only oracles - for (uint256 i = 0; i < newRoutes.length; i++) { - for (uint256 j = 0; j < newRoutes[i].oracles.length; j++) { - if (newRoutes[i].oracles[j].kind == OracleKind.PriceFeed) { - break; - } - if (j == newRoutes[i].oracles.length - 1) { - ernForUsdcViewOracles.push(newRoutes[i]); - } - } - } } /** diff --git a/src/interfaces/AggregatorV3Interface.sol b/src/interfaces/AggregatorV3Interface.sol new file mode 100644 index 0000000..d67f2fb --- /dev/null +++ b/src/interfaces/AggregatorV3Interface.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// solhint-disable-next-line interface-starts-with-i +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} \ No newline at end of file diff --git a/src/interfaces/ICommunityIssuance.sol b/src/interfaces/ICommunityIssuance.sol new file mode 100644 index 0000000..8588140 --- /dev/null +++ b/src/interfaces/ICommunityIssuance.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +interface ICommunityIssuance { + function fund(uint256 amount) external; +} \ No newline at end of file diff --git a/src/oracles/BalancerTwapMixin.sol b/src/oracles/BalancerTwapMixin.sol index 19c6541..5a7e8e2 100644 --- a/src/oracles/BalancerTwapMixin.sol +++ b/src/oracles/BalancerTwapMixin.sol @@ -10,7 +10,7 @@ import {ERC20} from "oz/token/ERC20/ERC20.sol"; // for decimals() contract BalancerTwapMixin { error BalancerOracle__TWAPOracleNotReady(); - function getBalancerPrice(address source, address tokenIn, uint32 period, uint256 amountIn) + function getBalancerPrice(address source, address tokenIn, uint32 period, uint32 ago, uint256 amountIn) public view returns (uint256 price) @@ -33,7 +33,7 @@ contract BalancerTwapMixin { queries[0] = IBalancerTwapOracle.OracleAverageQuery({ variable: IBalancerTwapOracle.Variable.PAIR_PRICE, secs: period, - ago: 0 + ago: ago }); oraclePrice = balancerTwapOracle.getTimeWeightedAverage(queries)[0]; } @@ -60,7 +60,7 @@ contract BalancerTwapMixin { } else { price = targetPrice * 10 ** decimalDifference; } - } else if (decimals0 < decimals1) { + } else { uint256 decimalDifference = decimals1 - decimals0; if (tokenInToken0) { price = targetPrice * 10 ** decimalDifference; diff --git a/src/oracles/UniV3TwapMixin.sol b/src/oracles/UniV3TwapMixin.sol index c8297db..cf0197b 100644 --- a/src/oracles/UniV3TwapMixin.sol +++ b/src/oracles/UniV3TwapMixin.sol @@ -7,7 +7,7 @@ import {FullMath} from "univ3-core/libraries/FullMath.sol"; import {IUniswapV3Pool} from "univ3-core/interfaces/IUniswapV3Pool.sol"; contract UniV3TwapMixin { - function getUniV3Price(address source, address tokenIn, uint32 period, uint256 amountIn) + function getUniV3Price(address source, address tokenIn, uint32 period, uint32 ago, uint256 amountIn) public view returns (uint256 price) @@ -15,8 +15,8 @@ contract UniV3TwapMixin { require(period != 0, "BP"); uint32[] memory secondsAgos = new uint32[](2); - secondsAgos[0] = period; - secondsAgos[1] = 0; + secondsAgos[0] = period + ago; + secondsAgos[1] = ago; (int56[] memory tickCumulatives,) = IUniswapV3Pool(source).observe(secondsAgos); diff --git a/src/oracles/VeloTwapMixin.sol b/src/oracles/VeloTwapMixin.sol index 39f73bb..fe0701d 100644 --- a/src/oracles/VeloTwapMixin.sol +++ b/src/oracles/VeloTwapMixin.sol @@ -9,53 +9,104 @@ import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; contract VeloTwapMixin { uint256 constant VELO_OBSERVATION_PERIOD = 1800; - function getVeloPrice(address source, address tokenIn, uint32 period, uint256 amountIn) + function getVeloPrice(address source, address tokenIn, uint32 period, uint32 ago, uint256 amountIn) public view returns (uint256 price) { IVeloPair pair = IVeloPair(source); - Cumulatives memory current = pair.currentCumulativePrices(); - Cumulatives memory last; + Cumulatives memory end; + Cumulatives memory start; uint256 observationLength = pair.observationLength(); - uint256 time; - - // avoid stack too deep - { - uint256 maxTimestampRequired = current.blockTimestamp - period; - // the minimum amount of observations the pair must have registered in the period of the query. - // the actual amount of observations since (block.timestamp - period) is likely to be smaller - uint256 minObservationsPassed = MathUpgradeable.ceilDiv(period, VELO_OBSERVATION_PERIOD); - // this observation is guaranteed to be from before the period (left side of the binary search) - uint256 L = observationLength - minObservationsPassed - 1; - uint256 R = observationLength - 1; // right side of the binary search - // binary search for the observation that's closest to the most recent one, yet still within the period - while (L < R) { - uint256 observationIndex = (L + R) / 2; - - (last.blockTimestamp, last.reserve0Cumulative, last.reserve1Cumulative) = - pair.observations(observationIndex); - if (last.blockTimestamp > maxTimestampRequired) { - R = observationIndex - 1; - } else { - L = observationIndex + 1; - } + if (ago == 0) { + end = pair.currentCumulativePrices(); + + (Cumulatives memory _before, Cumulatives memory _after) = _getObservations(pair, block.timestamp - period, observationLength); + + start = _averageObservations(_before, _after, end.blockTimestamp - period); + } else { + end.blockTimestamp = block.timestamp - ago; + (Cumulatives memory _before, Cumulatives memory _after) = _getObservations(pair, end.blockTimestamp, observationLength); + // get mean of the two observations weighted by the target + end = _averageObservations(_before, _after, end.blockTimestamp); + + start.blockTimestamp = end.blockTimestamp - period; + if (start.blockTimestamp >= _before.blockTimestamp) { + // this means that the start timestamp is within the same observation period above, + // so we can just use the same observations + start = _averageObservations(_before, _after, start.blockTimestamp); + } else { + (_before, _after) = _getObservations(pair, start.blockTimestamp, observationLength); + start = _averageObservations(_before, _after, start.blockTimestamp); } - time = current.blockTimestamp - last.blockTimestamp; } - uint112 reserve0 = safe112((current.reserve0Cumulative - last.reserve0Cumulative) / time); - uint112 reserve1 = safe112((current.reserve1Cumulative - last.reserve1Cumulative) / time); + uint256 timeElapsed = end.blockTimestamp - start.blockTimestamp; + uint256 reserve0 = (end.reserve0Cumulative - start.reserve0Cumulative) / timeElapsed; + uint256 reserve1 = (end.reserve1Cumulative - start.reserve1Cumulative) / timeElapsed; price = _veloGetAmountOut(amountIn, tokenIn, reserve0, reserve1, pair.stable(), pair); } + // gets the observations immediately before and after the target timestamp using binary search + function _getObservations(IVeloPair pair, uint256 targetTimestamp, uint256 observationLength) + public + view + returns (Cumulatives memory _before, Cumulatives memory _after) + { + uint256 minObservationsPassed = MathUpgradeable.ceilDiv(block.timestamp - targetTimestamp, VELO_OBSERVATION_PERIOD); + // this observation is guaranteed to be from before the period (left side of the binary search) + uint256 L = observationLength - minObservationsPassed - 1; + uint256 R = observationLength - 1; // right side of the binary search + + // Binary search to find the closest observation before targetTimestamp + while (L < R) { + uint256 observationIndex = (L + R + 1) / 2; // round up + (uint256 blockTimestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative) = pair.observations(observationIndex); + if (blockTimestamp > targetTimestamp) { + R = observationIndex - 1; + } else { + L = observationIndex; + _before.blockTimestamp = blockTimestamp; + _before.reserve0Cumulative = reserve0Cumulative; + _before.reserve1Cumulative = reserve1Cumulative; + } + } + if (_before.blockTimestamp == 0) { + // ensure that the observation is assigned + (_before.blockTimestamp, _before.reserve0Cumulative, _before.reserve1Cumulative) = pair.observations(L); + } + if (L == observationLength - 1) { + _after = pair.currentCumulativePrices(); + } else { + (_after.blockTimestamp, _after.reserve0Cumulative, _after.reserve1Cumulative) = pair.observations(L + 1); + } + } + + // This function is used to calculate the average of two observations, after and before the target timestamp, + // weighted by the target timestamp. + function _averageObservations(Cumulatives memory _before, Cumulatives memory _after, uint256 targetTimestamp) + private + view + returns (Cumulatives memory) + { + uint256 weight1 = targetTimestamp - _before.blockTimestamp; + uint256 weight2 = _after.blockTimestamp - targetTimestamp; + uint256 weightSum = weight1 + weight2; + return + Cumulatives({ + reserve0Cumulative: (_before.reserve0Cumulative * weight2 + _after.reserve0Cumulative * weight1) / weightSum, + reserve1Cumulative: (_before.reserve1Cumulative * weight2 + _after.reserve1Cumulative * weight1) / weightSum, + blockTimestamp: targetTimestamp + }); + } + /** * Utils * Below are the functions that are used to calculate the price of a token in a Velo pool. * This code is adapted from Velodrome's contracts directly, with changes to use parameters - * instead of state variables. + * instead of state variables, and additional comments for clarification. */ struct GetAmountOutLocalVars { uint256 decimals0; @@ -63,6 +114,7 @@ contract VeloTwapMixin { uint256 xy; } + // This function calculates the amount of tokenOut that will be received for a given amount of tokenIn function _veloGetAmountOut( uint256 amountIn, address tokenIn, @@ -90,6 +142,7 @@ contract VeloTwapMixin { } } + // This function calculates the product of the reserves of a Velo pool function _k(uint256 x, uint256 y, uint256 decimals0, uint256 decimals1, bool stable) private pure @@ -106,6 +159,10 @@ contract VeloTwapMixin { } } + // The following functions are used to calculate the price of a token in a stable Velo pool + + // _f calculates an estimate of the product of x3y+y3x + // for the first estimate, it's given reserveIn + amountIn and reserveOut function _f(uint256 x0, uint256 y) private pure returns (uint256) { uint256 _a = (x0 * y) / 1e18; uint256 _b = ((x0 * x0) / 1e18 + (y * y) / 1e18); @@ -116,6 +173,8 @@ contract VeloTwapMixin { return (3 * x0 * ((y * y) / 1e18)) / 1e18 + ((((x0 * x0) / 1e18) * x0) / 1e18); } + // _get_y calculates the reserveOut for a given trade + // it uses an optimized binary search to find the correct value function _get_y(uint256 x0, uint256 xy, uint256 y, uint256 decimals0, uint256 decimals1, bool stable) private pure @@ -161,9 +220,5 @@ contract VeloTwapMixin { } revert("!y"); } - - function safe112(uint256 n) private pure returns (uint112) { - if (n > type(uint112).max) revert("safe112"); - return uint112(n); - } + } diff --git a/test/OracleAggregatorTest.t.sol b/test/OracleAggregatorTest.t.sol index 456e54f..4fe755f 100644 --- a/test/OracleAggregatorTest.t.sol +++ b/test/OracleAggregatorTest.t.sol @@ -25,20 +25,6 @@ contract OracleTest is Test { /// Math related functions - function test_revertHighSpread2Values(uint256 price1, uint256 price2) public { - // avoid prices above 2**128 - price1 = bound(price1, 1, type(uint128).max); // if a price is 0, the spread will be infinite - // make sure the prices are sufficiently apart - uint256 minPrice2 = Math.max(1, Math.ceilDiv(price1 * 111, 100)); - vm.assume(minPrice2 < type(uint128).max); - price2 = bound(price2, minPrice2, type(uint128).max); - - uint256[] memory prices = new uint256[](2); - prices[0] = price1; - prices[1] = price2; - vm.expectRevert(OracleAggregator.Oracle_PricesSpreadTooHigh.selector); - oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); - } function test_revertHighSpread3Values(uint256 price1, uint256 price2, uint256 price3) public { // avoid prices above 2**128 @@ -56,7 +42,7 @@ contract OracleTest is Test { prices[1] = price2; prices[2] = price3; vm.expectRevert(OracleAggregator.Oracle_PricesSpreadTooHigh.selector); - oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); + oracleAggregator._getValidatedMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); } function test_ignoreOutliers(uint256 price1, uint256 price2, uint256 outlier) public { @@ -70,7 +56,7 @@ contract OracleTest is Test { prices[0] = price1; prices[1] = price2; prices[2] = outlier; - uint256 result = oracleAggregator.getMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); + uint256 result = oracleAggregator._getValidatedMeanPrice(prices, maxMadRelativeToMedianBPS, maxScoreBPS); assertEq(result, mean, "Outlier should be ignored"); } @@ -85,7 +71,7 @@ contract OracleTest is Test { if (testCase.shouldRevert) { vm.expectRevert(); } - result = oracleAggregator.getMeanPrice(testCase.prices, maxMadRelativeToMedianBPS, maxScoreBPS); + result = oracleAggregator._getValidatedMeanPrice(testCase.prices, maxMadRelativeToMedianBPS, maxScoreBPS); assertEq(result, testCase.expected, "Unexpected result"); } diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.sol index 0d14c03..89ae0a7 100644 --- a/test/OraclesForkTests.sol +++ b/test/OraclesForkTests.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.0; import {OracleAggregator, OracleKind, OracleRoute, Oracle} from "src/OracleAggregator.sol"; import {ERC20} from "oz/token/ERC20/ERC20.sol"; import {Math} from "oz/utils/math/Math.sol"; +import {VeloTwapMixin} from "src/oracles/VeloTwapMixin.sol"; +import {IVeloPair, Cumulatives} from "src/interfaces/IVeloPair.sol"; import "forge-std/Test.sol"; contract OracleForkTests is Test { @@ -26,7 +28,7 @@ contract OracleForkTests is Test { address WBTC_ADDRESS = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; function setUp() public { - opFork = vm.createSelectFork("https://go.getblock.io/bec4b0dd7017435c8880f2cae8ea2d4d", 118638228); + opFork = vm.createSelectFork("https://go.getblock.io/bec4b0dd7017435c8880f2cae8ea2d4d"/* , 118638228 */); oracleAggregator = new OracleAggregator(); } @@ -35,15 +37,15 @@ contract OracleForkTests is Test { OracleRoute memory route; route.oracles = new Oracle[](1); route.oracles[0] = - Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); - uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, 1194216670556036888562); route.oracles[0] = - Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: OP_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); - uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e18); + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price2, 837368983916789); } @@ -51,16 +53,16 @@ contract OracleForkTests is Test { OracleRoute memory route; route.oracles = new Oracle[](1); route.oracles[0] = - Oracle({source: WETH_OP_VELO_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Velo}); + Oracle({source: WETH_OP_VELO_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); uint256 expected = 1192245433864621830052; - uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, expected); - route.oracles[0] = Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, period: 3600, kind: OracleKind.Velo}); + route.oracles[0] = Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); - uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e18); + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price2, 838020130098509); } @@ -69,17 +71,17 @@ contract OracleForkTests is Test { OracleRoute memory route; route.oracles = new Oracle[](1); route.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: ERN_ADDRESS, period: 3600, kind: OracleKind.Velo}); + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: ERN_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); uint256 expected = 982575; - uint256 price = oracleAggregator.getMultiHopPriceView(route, 1e18); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, expected); route.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.Velo}); + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); - uint256 price2 = oracleAggregator.getMultiHopPriceView(route, 1e6); + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e6); assertEq(price2, 1017733222640936418); } @@ -89,59 +91,60 @@ contract OracleForkTests is Test { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, period: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); // check decimal normalization vm.mockCall(VMEX, abi.encodeWithSelector(ERC20.decimals.selector), abi.encode(6)); route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, period: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, period: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.getMultiHopPriceView(route, 1e18), 18); + route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); } - function test_priceFeed() public { + /* function test_priceFeed() public { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, period: 0, kind: OracleKind.PriceFeed}); + route.oracles[0] = Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); - uint256 price = oracleAggregator.getMultiHopPrice(route, 1e8); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e8); console.log("price", price); - } + } */ function test_twoPrices() public { - OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](3); + OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); OracleRoute memory _veloOracle; _veloOracle.oracles = new Oracle[](1); _veloOracle.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.Velo}); + Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); OracleRoute memory _uniV3Oracle; _uniV3Oracle.oracles = new Oracle[](1); _uniV3Oracle.oracles[0] = - Oracle({source: USDC_ERN_UNIV3_POOL, tokenIn: USDC_ADDRESS, period: 3600, kind: OracleKind.UniV3}); + Oracle({source: USDC_ERN_UNIV3_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); - OracleRoute memory _priceFeedOracle; - _priceFeedOracle.oracles = new Oracle[](1); - _priceFeedOracle.oracles[0] = - Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, period: 0, kind: OracleKind.PriceFeed}); + // OracleRoute memory _priceFeedOracle; + // _priceFeedOracle.oracles = new Oracle[](1); + // _priceFeedOracle.oracles[0] = + // Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); _ernForUsdcAllOracles[0] = _veloOracle; _ernForUsdcAllOracles[1] = _uniV3Oracle; - _ernForUsdcAllOracles[2] = _priceFeedOracle; + // _ernForUsdcAllOracles[2] = _priceFeedOracle; + + uint256[] memory prices = oracleAggregator.fetchTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); - uint256[] memory prices = oracleAggregator.getTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); - uint256 priceUniV3 = oracleAggregator.getMultiHopPriceView(_uniV3Oracle, 1e10); - uint256 priceVelo = oracleAggregator.getMultiHopPriceView(_veloOracle, 1e10); + uint256 priceUniV3 = oracleAggregator.fetchMultiHopPrice(_uniV3Oracle, 1e10); + uint256 priceVelo = oracleAggregator.fetchMultiHopPrice(_veloOracle, 1e10); console.log("priceVelo", priceVelo); console.log("prices1 ", prices[0]); @@ -149,4 +152,12 @@ contract OracleForkTests is Test { console.log("priceUniV3", priceUniV3); console.log("prices2 ", prices[1]); } + + function test_chainLink() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({source: 0x13e3Ee699D1909E989722E753853AE30b17e08c5, tokenIn: address(1), windowOrDecimalOffset: 12, kind: OracleKind.Chainlink}); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e6); + console.log("price", price); + } } diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol index 514f45e..4f66dcb 100644 --- a/test/ReaperStrategyStabilityPool.t.sol +++ b/test/ReaperStrategyStabilityPool.t.sol @@ -14,6 +14,7 @@ import "src/mocks/MockAggregator.sol"; import "src/interfaces/ITroveManager.sol"; import "src/interfaces/IStabilityPool.sol"; import "src/interfaces/IAggregatorAdmin.sol"; +import "src/interfaces/ICommunityIssuance.sol"; import {IUniswapV3Pool} from "src/interfaces/IUniswapV3Pool.sol"; import {IStaticOracle} from "src/interfaces/IStaticOracle.sol"; import {IERC20Mintable} from "src/interfaces/IERC20Mintable.sol"; @@ -30,7 +31,9 @@ contract ReaperStrategyStabilityPoolTest is Test { // Registry address public treasuryAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B; address public stabilityPoolAddress = 0xD839A111598d5e27BD8f7A1A18ce9Bf079F0c0a2; - address public priceFeedAddress = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; + address public communityIssuanceOwner = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C; + address public communityIssuanceAddress = 0x323A9C4CB4Be7A3c9d31209B0a2bAd56276bbf89; + address public priceFeedAddress = 0xadd6F326a395629926D9a535d809B5e3d8c7FE8d; address public priceFeedOwnerAddress = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C; address public troveManager = 0xd584A5E956106DB2fE74d56A0B14a9d64BE8DC93; address public veloRouter = 0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858; @@ -53,7 +56,7 @@ contract ReaperStrategyStabilityPoolTest is Test { address public wbtcAddress = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; address public usdcAddress = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; address public oathAddress = 0x00e1724885473B63bCE08a9f0a52F35b0979e35A; - address public opAddress = 0x4200000000000000000000000000000000000042; + address public wstethAddress = 0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb; address public strategistAddr = 0x1A20D7A31e5B3Bc5f02c8A146EF6f394502a10c4; address public wantHolderAddr = strategistAddr; @@ -61,7 +64,7 @@ contract ReaperStrategyStabilityPoolTest is Test { address public borrowerOperationsAddress = 0xaA0B41B61f76587cf85155147d7F3B7725D14Eb3; // 0x0a4582d3d9ecBAb80a66DAd8A881BE3b771d3e5B; address public oathOwner = 0xe432150cce91c13a887f7D836923d5597adD8E31; address public wbtcHolder = 0x85C31FFA3706d1cce9d525a00f1C7D4A2911754c; - address public opHolder = 0x790b4086D106Eafd913e71843AED987eFE291c92; + address public wstethHolder = 0x583f5777a69830fCB6F811a1b8e781D545D37923; bytes32 public balErnPoolId = 0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1; bytes32 public oatsAndGrainPoolId = 0x1cc3e990b23a09fc9715aaf7ccf21c212a9cbc160001000000000000000000bd; @@ -70,7 +73,7 @@ contract ReaperStrategyStabilityPoolTest is Test { AggregatorV3Interface wbtcAggregator; AggregatorV3Interface wethAggregator; - AggregatorV3Interface opAggregator; + AggregatorV3Interface wstethAggregator; AggregatorV3Interface usdcAggregator; address[] keepers = [ @@ -115,7 +118,7 @@ contract ReaperStrategyStabilityPoolTest is Test { function setUp() public { // Forking string memory rpc = vm.envString("RPC"); - optimismFork = vm.createSelectFork(rpc, 118851023 /*107994026*/ ); + optimismFork = vm.createSelectFork(rpc, 118425007 /*107994026*/ ); assertEq(vm.activeFork(), optimismFork); // // Deploying stuff @@ -150,12 +153,12 @@ contract ReaperStrategyStabilityPoolTest is Test { OracleRoute memory _veloOracle; _veloOracle.oracles = new Oracle[](1); _veloOracle.oracles[0] = - Oracle({source: veloUsdcErnPool, tokenIn: usdcAddress, period: 3600, kind: OracleKind.Velo}); + Oracle({source: veloUsdcErnPool, tokenIn: usdcAddress, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); OracleRoute memory _uniV3Oracle; _uniV3Oracle.oracles = new Oracle[](1); _uniV3Oracle.oracles[0] = - Oracle({source: uniV3UsdcErnPool, tokenIn: usdcAddress, period: 3600, kind: OracleKind.UniV3}); + Oracle({source: uniV3UsdcErnPool, tokenIn: usdcAddress, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); _ernForUsdcAllOracles[0] = _veloOracle; _ernForUsdcAllOracles[1] = _uniV3Oracle; @@ -249,32 +252,32 @@ contract ReaperStrategyStabilityPoolTest is Test { UniV3SwapData memory wbtcUsdcSwapData = UniV3SwapData({path: wbtcUsdcPath, fees: wbtcUsdcFees}); swapper.updateUniV3SwapPath(wbtcAddress, usdcAddress, uniV3Router, wbtcUsdcSwapData); - address[] memory opUsdcPath = new address[](3); - opUsdcPath[0] = opAddress; - opUsdcPath[1] = wethAddress; - opUsdcPath[2] = usdcAddress; - uint24[] memory opUsdcFees = new uint24[](2); - opUsdcFees[0] = 3000; - opUsdcFees[1] = 500; - UniV3SwapData memory opUsdcSwapData = UniV3SwapData({path: opUsdcPath, fees: opUsdcFees}); - swapper.updateUniV3SwapPath(opAddress, usdcAddress, uniV3Router, opUsdcSwapData); + address[] memory wstethUsdcPath = new address[](3); + wstethUsdcPath[0] = wstethAddress; + wstethUsdcPath[1] = wethAddress; + wstethUsdcPath[2] = usdcAddress; + uint24[] memory wstethUsdcFees = new uint24[](2); + wstethUsdcFees[0] = 3000; + wstethUsdcFees[1] = 500; + UniV3SwapData memory wstethUsdcSwapData = UniV3SwapData({path: wstethUsdcPath, fees: wstethUsdcFees}); + swapper.updateUniV3SwapPath(wstethAddress, usdcAddress, uniV3Router, wstethUsdcSwapData); vm.stopPrank(); - // Register CL aggregators in Swapper for WETH, WBTC, OP, and USDC + // Register CL aggregators in Swapper for WETH, WBTC, WSTETH, and USDC // We set high timeouts since we do a lot of manual time skipping in tests // 2 days should be plenty = 2 * 24 * 60 * 60 = 172800 // Since our strategy assumes that USDC ~= ERN, we reuse the USDC aggregator for ERN vm.startPrank(superAdminAddress); swapper.updateTokenAggregator(wethAddress, 0x13e3Ee699D1909E989722E753853AE30b17e08c5, 172800); swapper.updateTokenAggregator(wbtcAddress, 0xD702DD976Fb76Fffc2D3963D037dfDae5b04E593, 172800); - swapper.updateTokenAggregator(opAddress, 0x0D276FC14719f9292D5C1eA2198673d1f4269246, 172800); + swapper.updateTokenAggregator(wstethAddress, 0x698B585CbC4407e2D54aa898B2600B53C68958f7, 172800); swapper.updateTokenAggregator(usdcAddress, 0x16a9FA2FDa030272Ce99B29CF780dFA30361E0f3, 172800); vm.stopPrank(); // set our swap steps // step 1: weth -> usdc using univ3 w/ CL aggregators and minAmountOutBPS as 9950 // step 2: wbtc -> usdc using univ3 w/ CL aggregators and minAmountOutBPS as 9950 - // step 3: op -> usdc using univ3 w/ CL aggregators and minAmountOutBPS as 9950 + // step 3: wsteth -> usdc using univ3 w/ CL aggregators and minAmountOutBPS as 9950 // step 4: oath -> usdc using velo w/ 0 for minAmountOut ReaperBaseStrategyv4.SwapStep memory step1 = ReaperBaseStrategyv4.SwapStep({ exType: ReaperBaseStrategyv4.ExchangeType.UniV3, @@ -292,7 +295,7 @@ contract ReaperStrategyStabilityPoolTest is Test { }); ReaperBaseStrategyv4.SwapStep memory step3 = ReaperBaseStrategyv4.SwapStep({ exType: ReaperBaseStrategyv4.ExchangeType.UniV3, - start: opAddress, + start: wstethAddress, end: usdcAddress, minAmountOutData: MinAmountOutData({kind: MinAmountOutKind.ChainlinkBased, absoluteOrBPSValue: 9950}), exchangeAddress: uniV3Router @@ -327,17 +330,23 @@ contract ReaperStrategyStabilityPoolTest is Test { vm.prank(wbtcHolder); IERC20Mintable(wbtcAddress).approve(address(wrappedProxy), wbtcBalance); - uint256 opBalance = IERC20Mintable(opAddress).balanceOf(opHolder); - console.log("approving: ", opBalance); - vm.prank(opHolder); - IERC20Mintable(opAddress).approve(address(wrappedProxy), opBalance); + uint256 wstethBalance = IERC20Mintable(wstethAddress).balanceOf(wstethHolder); + console.log("approving: ", wstethBalance); + vm.prank(wstethHolder); + IERC20Mintable(wstethAddress).approve(address(wrappedProxy), wstethBalance); wrappedProxy.updateErnMinAmountOutBPS(9950); wrappedProxy.updateUsdcToErnExchange(ReaperBaseStrategyv4.ExchangeType.VeloSolid); + deal(oathAddress, communityIssuanceOwner, 10_000 ether); + vm.startPrank(communityIssuanceOwner); + IERC20Mintable(oathAddress).approve(communityIssuanceAddress, 10_000 ether); + ICommunityIssuance(communityIssuanceAddress).fund(10_000 ether); + vm.stopPrank(); + wbtcAggregator = AggregatorV3Interface(IPriceFeed(priceFeedAddress).priceAggregator(wbtcAddress)); wethAggregator = AggregatorV3Interface(IPriceFeed(priceFeedAddress).priceAggregator(wethAddress)); - opAggregator = AggregatorV3Interface(IPriceFeed(priceFeedAddress).priceAggregator(opAddress)); + wstethAggregator = AggregatorV3Interface(IPriceFeed(priceFeedAddress).priceAggregator(wstethAddress)); usdcAggregator = AggregatorV3Interface(chainlinkUsdcOracle); } @@ -839,10 +848,10 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 wbtcAmount = 1 * (10 ** 8); uint256 wethAmount = 10 ether; - uint256 opAmount = 1000 ether; + uint256 wstethAmount = 10 ether; deal({token: wbtcAddress, to: address(wrappedProxy), give: wbtcAmount}); deal({token: wethAddress, to: address(wrappedProxy), give: wethAmount}); - deal({token: opAddress, to: address(wrappedProxy), give: opAmount}); + deal({token: wstethAddress, to: address(wrappedProxy), give: wstethAmount}); // uint256 valueInCollateralAfter = wrappedProxy.getERNValueOfCollateralGain(); uint256 poolBalanceAfter = wrappedProxy.balanceOfPool(); @@ -855,26 +864,27 @@ contract ReaperStrategyStabilityPoolTest is Test { // console.log("wbtcAggregator: ", wbtcAggregator); // console.log("wethAggregator: ", wethAggregator); - // console.log("opAggregator: ", opAggregator); + // console.log("wstethAggregator: ", wstethAggregator); (, int256 wbtcPrice,,,) = wbtcAggregator.latestRoundData(); (, int256 wethPrice,,,) = wethAggregator.latestRoundData(); - (, int256 opPrice,,,) = opAggregator.latestRoundData(); + (, int256 wstethPrice,,,) = wstethAggregator.latestRoundData(); console.log("wbtcPrice: "); console.logInt(wbtcPrice); console.log("wethPrice: "); console.logInt(wethPrice); - console.log("opPrice: "); - console.logInt(opPrice); + console.log("wstethPrice: "); + console.logInt(wstethPrice); // All usd values must have 18 decimals for comparison. - // WETH and OP already have 18 decimals, but we need to scale WBTC. + // WETH and WSTETH already have 18 decimals, but we need to scale WBTC. uint256 wbtcUsdValue = wbtcAmount * uint256(wbtcPrice) * (10 ** 2); uint256 wethUsdValue = wethAmount * uint256(wethPrice) / (10 ** 8); - uint256 opUsdValue = opAmount * uint256(opPrice) / (10 ** 8); - uint256 expectedUsdValueInCollateral = wbtcUsdValue + wethUsdValue + opUsdValue; + uint256 wstethUsdValue = wstethAmount * uint256(wstethPrice) / (10 ** 8); + uint256 expectedUsdValueInCollateral = wbtcUsdValue + wethUsdValue + wstethUsdValue; + console.log("wbtcUsdValue: ", wbtcUsdValue); console.log("wethUsdValue: ", wethUsdValue); - console.log("opUsdValue: ", opUsdValue); + console.log("wstethUsdValue:", wstethUsdValue); uint256 usdValueInCollateral = wrappedProxy.getUSDValueOfCollateralGain(); console.log("expectedUsdValueInCollateral: ", expectedUsdValueInCollateral); @@ -935,10 +945,10 @@ contract ReaperStrategyStabilityPoolTest is Test { uint256 wbtcAmount = 1 * (10 ** 8); uint256 wethAmount = 10 ether; - uint256 opAmount = 1000 ether; + uint256 wstethAmount = 10 ether; deal({token: wbtcAddress, to: address(wrappedProxy), give: wbtcAmount}); deal({token: wethAddress, to: address(wrappedProxy), give: wethAmount}); - deal({token: opAddress, to: address(wrappedProxy), give: opAmount}); + deal({token: wstethAddress, to: address(wrappedProxy), give: wstethAmount}); uint256 valueInCollateral = wrappedProxy.getERNValueOfCollateralGain(); console.log("valueInCollateral: ", valueInCollateral); diff --git a/test/test_cases.json b/test/test_cases.json index 2798e94..77029a9 100644 --- a/test/test_cases.json +++ b/test/test_cases.json @@ -1,45 +1,5 @@ { "testCases": [ - { - "expected": 1005000000000000000, - "prices": [ - 1000000000000000000, - 1010000000000000000 - ], - "shouldRevert": false - }, - { - "expected": 5203360986091762542, - "prices": [ - 5204570984673230502, - 5202150987510294582 - ], - "shouldRevert": false - }, - { - "expected": 104, - "prices": [ - 100, - 109 - ], - "shouldRevert": false - }, - { - "expected": 0, - "prices": [ - 100, - 111 - ], - "shouldRevert": true - }, - { - "expected": 1010000000000000000000000000000, - "prices": [ - 1000000000000000000000000000000, - 1020000000000000000000000000000 - ], - "shouldRevert": false - }, { "expected": 1010000000000000000, "prices": [ From 2d35091cba0b200e4b80069ff408dd936823f478 Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:41:27 -0300 Subject: [PATCH 16/18] review fixes --- src/OracleAggregator.sol | 42 ++++++++--- src/ReaperStrategyStabilityPool.sol | 2 +- test/OracleAggregatorTest.t.sol | 3 +- test/OraclesForkTests.sol | 107 +++++++++++++++++++--------- 4 files changed, 107 insertions(+), 47 deletions(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 40e4815..9ebc6e0 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -34,6 +34,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { error Oracle_InvalidKind(); error Oracle_PricesSpreadTooHigh(); error Oracle_PricesUnreliable(); + error Oracle_InvalidInput(); uint256 constant BPS = 10_000; @@ -44,19 +45,23 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { view returns (uint256 price) { + if (oracles.length == 0) revert Oracle_InvalidInput(); uint256[] memory prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { prices[i] = _fetchMultiHopPrice(oracles[i], amountIn, false); } if (prices.length == 1) { return prices[0]; - } - if (prices.length == 2) { + } else if (prices.length == 2) { + /* The algorithm needs at least 3 prices to be reliable. + * When only 2 prices are available, we can craft a third price, + * by fetch the 2 oracles with a delay and computing their mean. + */ uint256[] memory delayedPrices = new uint256[](2); for (uint256 i = 0; i < oracles.length; i++) { delayedPrices[i] = _fetchMultiHopPrice(oracles[i], amountIn, true); } - (uint256 delayedMean, ) = getMean(delayedPrices, new bool[](2)); + uint256 delayedMean = getMean(delayedPrices); uint256[] memory combinedPrices = new uint256[](3); combinedPrices[0] = prices[0]; combinedPrices[2] = delayedMean; @@ -141,7 +146,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { { (bool[] memory isInvalid, uint256 mad, uint256 median) = _getValidityByZScore(prices, maxScoreBPS); uint256 nrOfValidPrices; - (mean, nrOfValidPrices) = getMean(prices, isInvalid); + (mean, nrOfValidPrices) = getMeanValid(prices, isInvalid); if (mad > (median * spreadTolerance) / BPS) revert Oracle_PricesSpreadTooHigh(); // if more than 1/3 of the prices are invalid, the whole list is considered unreliable if ((prices.length - nrOfValidPrices) > ((prices.length) / 3)) revert Oracle_PricesUnreliable(); @@ -160,10 +165,18 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { { (mad, median) = getMAD(prices); isInvalid = new bool[](prices.length); + if (mad != 0) { for (uint256 i = 0; i < prices.length; i++) { int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); } + } else { + // if the MAD is 0, more than half of the prices are the same + for (uint256 i = 0; i < prices.length; i++) { + isInvalid[i] = prices[i] != median; + } + } + return (isInvalid, mad, median); } @@ -209,10 +222,15 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { } } quickSort(deviations, 0, int256(n - 1)); + + if (n % 2 == 0) { + mad = (deviations[n / 2 - 1] + deviations[n / 2]) / 2; + } else { mad = deviations[n / 2]; + } } - function getMean(uint256[] memory prices, bool[] memory isInvalid) + function getMeanValid(uint256[] memory prices, bool[] memory isInvalid) internal pure returns (uint256 mean, uint256 nrValidPrices) @@ -228,6 +246,13 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { if (nrValidPrices > 0) mean = sum / nrValidPrices; } + function getMean(uint256[] memory prices) internal pure returns (uint256 mean) { + uint256 sum = 0; + for (uint256 i = 0; i < prices.length; i++) { + sum += prices[i]; + } + mean = sum / prices.length; + } // @notice Fetches the price from a Chainlink oracle // @param source Chainlink oracle address @@ -236,13 +261,12 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { // @param amountIn Input amount of the base token function getChainlinkPrice(address source, uint256 decimalOffset, address tokenIn, uint256 amountIn) internal view returns (uint256 price) { AggregatorV3Interface chainlinkOracle = AggregatorV3Interface(source); - (, int256 answer, , , ) = chainlinkOracle.latestRoundData(); + (, int256 answer,,,) = chainlinkOracle.latestRoundData(); uint8 chainlinkDecimals = chainlinkOracle.decimals(); if (tokenIn == address(0)) { - price = amountIn * uint256(answer) / (10**uint256(chainlinkDecimals)) / (10**decimalOffset); + price = amountIn * uint256(answer) / (10 ** uint256(chainlinkDecimals)) / (10 ** decimalOffset); } else { - price = amountIn * (10**uint256(chainlinkDecimals)) / uint256(answer) * (10**decimalOffset); + price = amountIn * (10 ** uint256(chainlinkDecimals)) / uint256(answer) * (10 ** decimalOffset); } } - } diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index bb50b79..a5a6a16 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -438,7 +438,7 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { } /** - * @dev Sets the period (in seconds) used to query the UniV3 TWAP. + * @dev Sets price oracle configuration. */ function updateOracles(OracleRoute[] calldata newRoutes) public { _atLeastRole(DEFAULT_ADMIN_ROLE); diff --git a/test/OracleAggregatorTest.t.sol b/test/OracleAggregatorTest.t.sol index 4fe755f..2cb02a0 100644 --- a/test/OracleAggregatorTest.t.sol +++ b/test/OracleAggregatorTest.t.sol @@ -16,7 +16,7 @@ contract OracleTest is Test { OracleAggregator oracleAggregator; - uint256 maxMadRelativeToMedianBPS = 500; // MADs can be at most 8% of the median + uint256 maxMadRelativeToMedianBPS = 500; // MADs can be at most 5% of the median uint256 maxScoreBPS = 25_000; // prices that are 2.5x MAD away from the median are rejected function setUp() public { @@ -25,7 +25,6 @@ contract OracleTest is Test { /// Math related functions - function test_revertHighSpread3Values(uint256 price1, uint256 price2, uint256 price3) public { // avoid prices above 2**128 price1 = bound(price1, 0, type(uint128).max); diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.sol index 89ae0a7..476c912 100644 --- a/test/OraclesForkTests.sol +++ b/test/OraclesForkTests.sol @@ -28,7 +28,7 @@ contract OracleForkTests is Test { address WBTC_ADDRESS = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; function setUp() public { - opFork = vm.createSelectFork("https://go.getblock.io/bec4b0dd7017435c8880f2cae8ea2d4d"/* , 118638228 */); + opFork = vm.createSelectFork(vm.envString("RPC"), 118638228); oracleAggregator = new OracleAggregator(); } @@ -36,14 +36,22 @@ contract OracleForkTests is Test { function test_uniV3() public { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = - Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); + route.oracles[0] = Oracle({ + source: WETH_OP_UNIV3_POOL, + tokenIn: WETH_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, 1194216670556036888562); - route.oracles[0] = - Oracle({source: WETH_OP_UNIV3_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); + route.oracles[0] = Oracle({ + source: WETH_OP_UNIV3_POOL, + tokenIn: OP_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price2, 837368983916789); @@ -52,37 +60,50 @@ contract OracleForkTests is Test { function test_velo() public { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = - Oracle({source: WETH_OP_VELO_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + route.oracles[0] = Oracle({ + source: WETH_OP_VELO_POOL, + tokenIn: WETH_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); - uint256 expected = 1192245433864621830052; + uint256 expected = 1192241375504066768022; uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, expected); - route.oracles[0] = Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + route.oracles[0] = + Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price2, 838020130098509); + assertEq(price2, 838022983982765); } // velo stable pairs have a different pricing method function test_veloStable() public { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: ERN_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + route.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: ERN_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); uint256 expected = 982575; uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); assertEq(price, expected); - route.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + route.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e6); - assertEq(price2, 1017733222640936418); + assertEq(price2, 1017732658860914652); } function test_balancer() public { @@ -91,21 +112,25 @@ contract OracleForkTests is Test { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000002101414223776 ether); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 475870.006344163244524176 ether); // check decimal normalization vm.mockCall(VMEX, abi.encodeWithSelector(ERC20.decimals.selector), abi.encode(6)); route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 2101414.223776 ether); - route.oracles[0] = Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - emit log_named_decimal_uint("price", oracleAggregator.fetchMultiHopPrice(route, 1e18), 18); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000000475870006344 ether); } /* function test_priceFeed() public { @@ -123,13 +148,21 @@ contract OracleForkTests is Test { OracleRoute memory _veloOracle; _veloOracle.oracles = new Oracle[](1); - _veloOracle.oracles[0] = - Oracle({source: USDC_ERN_VELO_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + _veloOracle.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); OracleRoute memory _uniV3Oracle; _uniV3Oracle.oracles = new Oracle[](1); - _uniV3Oracle.oracles[0] = - Oracle({source: USDC_ERN_UNIV3_POOL, tokenIn: USDC_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3}); + _uniV3Oracle.oracles[0] = Oracle({ + source: USDC_ERN_UNIV3_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); // OracleRoute memory _priceFeedOracle; // _priceFeedOracle.oracles = new Oracle[](1); @@ -142,22 +175,26 @@ contract OracleForkTests is Test { uint256[] memory prices = oracleAggregator.fetchTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); - uint256 priceUniV3 = oracleAggregator.fetchMultiHopPrice(_uniV3Oracle, 1e10); uint256 priceVelo = oracleAggregator.fetchMultiHopPrice(_veloOracle, 1e10); - console.log("priceVelo", priceVelo); - console.log("prices1 ", prices[0]); + assertEq(prices[0], priceVelo); + assertEq(prices[1], priceUniV3); - console.log("priceUniV3", priceUniV3); - console.log("prices2 ", prices[1]); + uint256 price = oracleAggregator.getReliablePrice(_ernForUsdcAllOracles, 1e10, 500, 25_000); + assertEq(price, 10161025621902873496771); } - + function test_chainLink() public { OracleRoute memory route; route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: 0x13e3Ee699D1909E989722E753853AE30b17e08c5, tokenIn: address(1), windowOrDecimalOffset: 12, kind: OracleKind.Chainlink}); + route.oracles[0] = Oracle({ + source: 0x13e3Ee699D1909E989722E753853AE30b17e08c5, + tokenIn: address(1), + windowOrDecimalOffset: 12, + kind: OracleKind.Chainlink + }); uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e6); - console.log("price", price); + assertEq(price, 285000000000000); } } From fd8433ca042eb0dc011d34ff2ca421f52fef6f8a Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:14:27 -0300 Subject: [PATCH 17/18] review fixes --- src/OracleAggregator.sol | 5 +---- src/ReaperStrategyStabilityPool.sol | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index 9ebc6e0..d7beacd 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -86,10 +86,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @param route List of oracles for multihop price /// @param amountIn Input amount of the base token function fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn) external view returns (uint256 price) { - for (uint256 i = 0; i < route.oracles.length; i++) { - price = _fetchPrice(route.oracles[i], amountIn, false); - amountIn = price; - } + _fetchMultiHopPrice(route, amountIn, false); } function fetchPrice(Oracle memory oracle, uint256 amountIn) external view returns (uint256 price) { diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol index a5a6a16..79421aa 100644 --- a/src/ReaperStrategyStabilityPool.sol +++ b/src/ReaperStrategyStabilityPool.sol @@ -44,7 +44,6 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 { uint256 acceptableTWAPLowerBound; // The normal lower price for the , reverts harvest if below OracleRoute[] internal ernForUsdcOracles; - OracleRoute[] internal ernForUsdcViewOracles; struct ExchangeSettings { address veloRouter; From 7225b09b25f2aae39a5189f1142b8f3018ed820e Mon Sep 17 00:00:00 2001 From: lookeey <71905281+lookeey@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:55:12 -0300 Subject: [PATCH 18/18] review fixes --- README.md | 6 +- foundry.toml | 2 +- src/OracleAggregator.sol | 57 +-- src/oracles/ChainlinkAdapterMixin.sol | 27 ++ ...esForkTests.sol => OraclesForkTests.t.sol} | 458 ++++++++++-------- 5 files changed, 315 insertions(+), 235 deletions(-) create mode 100644 src/oracles/ChainlinkAdapterMixin.sol rename test/{OraclesForkTests.sol => OraclesForkTests.t.sol} (78%) diff --git a/README.md b/README.md index e648567..ff95c6c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Reaper multistrategy vault -An [ERC4626](https://eips.ethereum.org/EIPS/eip-4626) compliant vault using multistrategy (Yearn V2 style) architecture. +A strategy contract for ERC4626 compliant vault (Yearn V2 style) architecture, that can manage assets, and claim and convert rewards from an external system. The strategy is responsible for managing users’ deposits so they can accrue rewards from the Stability Pool contract. During a harvest cycle, the strategy may claim multiple asset rewards from the Stability Pool and convert them into a singular asset. Firstly, all rewards are converted into USDC using a token Swapper mechanism. Then, USDC is converted into ERN. To avoid frontrunning attacks in this last exchange, a TWAP oracle is used. However, due to liquidity concerns and manipulability of the TWAP, an oracle aggregator solution was devised, capable of sourcing multiple prices from different pools and outputting a single reliable value. This solution has support for diverse AMM protocols. -Run `npm i && git submodule update --init --recursive` after cloning to ensure all submodules are initialized recursively. +## Velodrome Code + +The file `src/oracles/VeloTwapMixin.sol` contains code that is originally from the Velodrome [Pool.sol](https://github.com/velodrome-finance/contracts/blob/main/contracts/Pool.sol) contract, adapted to use memory variables and parameters rather than the pool's internal variables. This is because we cannot access these functions externally. \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 23b0ced..27a054d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = "out" libs = ["lib"] - +line_length = 170 fs_permissions = [{ access = "read", path = "./test/"}] remappings = [ diff --git a/src/OracleAggregator.sol b/src/OracleAggregator.sol index d7beacd..7174f73 100644 --- a/src/OracleAggregator.sol +++ b/src/OracleAggregator.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; import {BalancerTwapMixin} from "./oracles/BalancerTwapMixin.sol"; -import {AggregatorV3Interface} from "./interfaces/AggregatorV3Interface.sol"; +import {ChainlinkAdapterMixin} from "./oracles/ChainlinkAdapterMixin.sol"; import {ERC20} from "oz/token/ERC20/ERC20.sol"; // has decimals(), as opposed to IERC20 import {MathUpgradeable} from "oz-upgradeable/utils/math/MathUpgradeable.sol"; @@ -30,7 +30,7 @@ struct Oracle { // This contract contains tools for computing TWAP values and // making averages between the results, for more reliable prices. // Has support for multiple oracles -contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { +contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin, ChainlinkAdapterMixin { error Oracle_InvalidKind(); error Oracle_PricesSpreadTooHigh(); error Oracle_PricesUnreliable(); @@ -40,11 +40,12 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { // @notice Fetches the mean price of a list of oracles, filtering out outliers // and checking if the prices are reliable. - function getReliablePrice(OracleRoute[] memory oracles, uint256 amountIn, uint256 spreadTolerance, uint256 maxScoreBPS) - external - view - returns (uint256 price) - { + function getReliablePrice( + OracleRoute[] memory oracles, + uint256 amountIn, + uint256 spreadTolerance, + uint256 maxScoreBPS + ) external view returns (uint256 price) { if (oracles.length == 0) revert Oracle_InvalidInput(); uint256[] memory prices = new uint256[](oracles.length); for (uint256 i = 0; i < oracles.length; i++) { @@ -86,7 +87,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @param route List of oracles for multihop price /// @param amountIn Input amount of the base token function fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn) external view returns (uint256 price) { - _fetchMultiHopPrice(route, amountIn, false); + return _fetchMultiHopPrice(route, amountIn, false); } function fetchPrice(Oracle memory oracle, uint256 amountIn) external view returns (uint256 price) { @@ -95,7 +96,11 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @param route List of oracles for multihop price /// @param amountIn Input amount of the base token - function _fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn, bool delayWindow) internal view returns (uint256 price) { + function _fetchMultiHopPrice(OracleRoute memory route, uint256 amountIn, bool delayWindow) + internal + view + returns (uint256 price) + { for (uint256 i = 0; i < route.oracles.length; i++) { price = _fetchPrice(route.oracles[i], amountIn, delayWindow); amountIn = price; @@ -105,7 +110,11 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { /// @param oracle Kind of oracle to use -- see OracleKind /// @param amountIn Input amount of the base token /// @param delayWindow If true, the price is calculated with a delayed window - used for 2 price comparisons - incompatible with Chainlink - function _fetchPrice(Oracle memory oracle, uint256 amountIn, bool delayWindow) internal view returns (uint256 price) { + function _fetchPrice(Oracle memory oracle, uint256 amountIn, bool delayWindow) + internal + view + returns (uint256 price) + { uint32 period; uint32 ago; if (delayWindow) { @@ -115,7 +124,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { period = uint32(oracle.windowOrDecimalOffset); ago = 0; } - + if (oracle.kind == OracleKind.Velo) { return getVeloPrice(oracle.source, oracle.tokenIn, period, ago, amountIn); } else if (oracle.kind == OracleKind.UniV3) { @@ -163,10 +172,10 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { (mad, median) = getMAD(prices); isInvalid = new bool[](prices.length); if (mad != 0) { - for (uint256 i = 0; i < prices.length; i++) { - int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); - isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); - } + for (uint256 i = 0; i < prices.length; i++) { + int256 score = (int256(prices[i]) - int256(median)) * int256(BPS) / int256(mad); + isInvalid[i] = score < -int256(maxScoreBPS) || score > int256(maxScoreBPS); + } } else { // if the MAD is 0, more than half of the prices are the same for (uint256 i = 0; i < prices.length; i++) { @@ -223,7 +232,7 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { if (n % 2 == 0) { mad = (deviations[n / 2 - 1] + deviations[n / 2]) / 2; } else { - mad = deviations[n / 2]; + mad = deviations[n / 2]; } } @@ -250,20 +259,4 @@ contract OracleAggregator is VeloTwapMixin, UniV3TwapMixin, BalancerTwapMixin { } mean = sum / prices.length; } - - // @notice Fetches the price from a Chainlink oracle - // @param source Chainlink oracle address - // @param tokenIn address(0) for price, address(1) for inverted price - // @param decimalOffset Difference between tokenIn and tokenOut decimals - // @param amountIn Input amount of the base token - function getChainlinkPrice(address source, uint256 decimalOffset, address tokenIn, uint256 amountIn) internal view returns (uint256 price) { - AggregatorV3Interface chainlinkOracle = AggregatorV3Interface(source); - (, int256 answer,,,) = chainlinkOracle.latestRoundData(); - uint8 chainlinkDecimals = chainlinkOracle.decimals(); - if (tokenIn == address(0)) { - price = amountIn * uint256(answer) / (10 ** uint256(chainlinkDecimals)) / (10 ** decimalOffset); - } else { - price = amountIn * (10 ** uint256(chainlinkDecimals)) / uint256(answer) * (10 ** decimalOffset); - } - } } diff --git a/src/oracles/ChainlinkAdapterMixin.sol b/src/oracles/ChainlinkAdapterMixin.sol new file mode 100644 index 0000000..e2e01b9 --- /dev/null +++ b/src/oracles/ChainlinkAdapterMixin.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL1.1 + +pragma solidity ^0.8.0; + +import {AggregatorV3Interface} from "../interfaces/AggregatorV3Interface.sol"; + +contract ChainlinkAdapterMixin { + // @notice Fetches the price from a Chainlink oracle + // @param source Chainlink oracle address + // @param tokenIn address(0) for price, address(1) for inverted price + // @param decimalOffset Difference between tokenIn and tokenOut decimals + // @param amountIn Input amount of the base token + function getChainlinkPrice(address source, uint256 decimalOffset, address tokenIn, uint256 amountIn) + internal + view + returns (uint256 price) + { + AggregatorV3Interface chainlinkOracle = AggregatorV3Interface(source); + (, int256 answer,,,) = chainlinkOracle.latestRoundData(); + uint8 chainlinkDecimals = chainlinkOracle.decimals(); + if (tokenIn == address(0)) { + price = amountIn * uint256(answer) / (10 ** uint256(chainlinkDecimals)) / (10 ** decimalOffset); + } else { + price = amountIn * (10 ** uint256(chainlinkDecimals)) / uint256(answer) * (10 ** decimalOffset); + } + } +} diff --git a/test/OraclesForkTests.sol b/test/OraclesForkTests.t.sol similarity index 78% rename from test/OraclesForkTests.sol rename to test/OraclesForkTests.t.sol index 476c912..ab90cf5 100644 --- a/test/OraclesForkTests.sol +++ b/test/OraclesForkTests.t.sol @@ -1,200 +1,258 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import {OracleAggregator, OracleKind, OracleRoute, Oracle} from "src/OracleAggregator.sol"; -import {ERC20} from "oz/token/ERC20/ERC20.sol"; -import {Math} from "oz/utils/math/Math.sol"; -import {VeloTwapMixin} from "src/oracles/VeloTwapMixin.sol"; -import {IVeloPair, Cumulatives} from "src/interfaces/IVeloPair.sol"; -import "forge-std/Test.sol"; - -contract OracleForkTests is Test { - uint256 opFork; - - OracleAggregator oracleAggregator; - - address WETH_OP_UNIV3_POOL = 0x68F5C0A2DE713a54991E01858Fd27a3832401849; - address WETH_OP_VELO_POOL = 0xd25711EdfBf747efCE181442Cc1D8F5F8fc8a0D3; - address USDC_ERN_VELO_POOL = 0x605cCE502dEe6BD201b493782e351e645D44abBB; - address USDC_ERN_UNIV3_POOL = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; - - address USDC_ADDRESS = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; - address ERN_ADDRESS = 0xc5b001DC33727F8F26880B184090D3E252470D45; - - address OP_ADDRESS = 0x4200000000000000000000000000000000000042; - address WETH_ADDRESS = 0x4200000000000000000000000000000000000006; - - address PRICE_FEED = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; - address WBTC_ADDRESS = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; - - function setUp() public { - opFork = vm.createSelectFork(vm.envString("RPC"), 118638228); - - oracleAggregator = new OracleAggregator(); - } - - function test_uniV3() public { - OracleRoute memory route; - route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({ - source: WETH_OP_UNIV3_POOL, - tokenIn: WETH_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.UniV3 - }); - - uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price, 1194216670556036888562); - - route.oracles[0] = Oracle({ - source: WETH_OP_UNIV3_POOL, - tokenIn: OP_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.UniV3 - }); - - uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price2, 837368983916789); - } - - function test_velo() public { - OracleRoute memory route; - route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({ - source: WETH_OP_VELO_POOL, - tokenIn: WETH_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.Velo - }); - - uint256 expected = 1192241375504066768022; - - uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price, expected); - - route.oracles[0] = - Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); - - uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price2, 838022983982765); - } - - // velo stable pairs have a different pricing method - function test_veloStable() public { - OracleRoute memory route; - route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({ - source: USDC_ERN_VELO_POOL, - tokenIn: ERN_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.Velo - }); - - uint256 expected = 982575; - - uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); - assertEq(price, expected); - - route.oracles[0] = Oracle({ - source: USDC_ERN_VELO_POOL, - tokenIn: USDC_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.Velo - }); - - uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e6); - assertEq(price2, 1017732658860914652); - } - - function test_balancer() public { - address VMEX = 0x6D2E5b8841a6Aa5f0f973436357f75D3Eeb93312; - address VMEX_POOL = 0x4Dde571Dc66217a062e4B50f9b20c4D08b3245a0; - OracleRoute memory route; - - route.oracles = new Oracle[](1); - route.oracles[0] = - Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000002101414223776 ether); - - route.oracles[0] = - Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 475870.006344163244524176 ether); - - // check decimal normalization - vm.mockCall(VMEX, abi.encodeWithSelector(ERC20.decimals.selector), abi.encode(6)); - - route.oracles = new Oracle[](1); - route.oracles[0] = - Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 2101414.223776 ether); - - route.oracles[0] = - Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); - assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000000475870006344 ether); - } - - /* function test_priceFeed() public { - OracleRoute memory route; - - route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); - - uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e8); - console.log("price", price); - } */ - - function test_twoPrices() public { - OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); - - OracleRoute memory _veloOracle; - _veloOracle.oracles = new Oracle[](1); - _veloOracle.oracles[0] = Oracle({ - source: USDC_ERN_VELO_POOL, - tokenIn: USDC_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.Velo - }); - - OracleRoute memory _uniV3Oracle; - _uniV3Oracle.oracles = new Oracle[](1); - _uniV3Oracle.oracles[0] = Oracle({ - source: USDC_ERN_UNIV3_POOL, - tokenIn: USDC_ADDRESS, - windowOrDecimalOffset: 3600, - kind: OracleKind.UniV3 - }); - - // OracleRoute memory _priceFeedOracle; - // _priceFeedOracle.oracles = new Oracle[](1); - // _priceFeedOracle.oracles[0] = - // Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); - - _ernForUsdcAllOracles[0] = _veloOracle; - _ernForUsdcAllOracles[1] = _uniV3Oracle; - // _ernForUsdcAllOracles[2] = _priceFeedOracle; - - uint256[] memory prices = oracleAggregator.fetchTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); - - uint256 priceUniV3 = oracleAggregator.fetchMultiHopPrice(_uniV3Oracle, 1e10); - uint256 priceVelo = oracleAggregator.fetchMultiHopPrice(_veloOracle, 1e10); - - assertEq(prices[0], priceVelo); - assertEq(prices[1], priceUniV3); - - uint256 price = oracleAggregator.getReliablePrice(_ernForUsdcAllOracles, 1e10, 500, 25_000); - assertEq(price, 10161025621902873496771); - } - - function test_chainLink() public { - OracleRoute memory route; - route.oracles = new Oracle[](1); - route.oracles[0] = Oracle({ - source: 0x13e3Ee699D1909E989722E753853AE30b17e08c5, - tokenIn: address(1), - windowOrDecimalOffset: 12, - kind: OracleKind.Chainlink - }); - uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e6); - assertEq(price, 285000000000000); - } -} +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {OracleAggregator, OracleKind, OracleRoute, Oracle} from "src/OracleAggregator.sol"; +import {ERC20} from "oz/token/ERC20/ERC20.sol"; +import {Math} from "oz/utils/math/Math.sol"; +import {VeloTwapMixin} from "src/oracles/VeloTwapMixin.sol"; +import {IVeloPair, Cumulatives} from "src/interfaces/IVeloPair.sol"; +import "forge-std/Test.sol"; + +contract OracleForkTests is Test { + uint256 opFork; + + OracleAggregator oracleAggregator; + + address WETH_OP_UNIV3_POOL = 0x68F5C0A2DE713a54991E01858Fd27a3832401849; + address WETH_OP_VELO_POOL = 0xd25711EdfBf747efCE181442Cc1D8F5F8fc8a0D3; + address USDC_ERN_VELO_POOL = 0x605cCE502dEe6BD201b493782e351e645D44abBB; + address USDC_ERN_UNIV3_POOL = 0x4CE4a1a593Ea9f2e6B2c05016a00a2D300C9fFd8; + + address USDC_ADDRESS = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address ERN_ADDRESS = 0xc5b001DC33727F8F26880B184090D3E252470D45; + + address OP_ADDRESS = 0x4200000000000000000000000000000000000042; + address WETH_ADDRESS = 0x4200000000000000000000000000000000000006; + + address PRICE_FEED = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427; + address WBTC_ADDRESS = 0x68f180fcCe6836688e9084f035309E29Bf0A2095; + + function setUp() public { + opFork = vm.createSelectFork(vm.envString("RPC"), 118638228); + + oracleAggregator = new OracleAggregator(); + } + + function test_uniV3() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: WETH_OP_UNIV3_POOL, + tokenIn: WETH_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price, 1194216670556036888562); + + route.oracles[0] = Oracle({ + source: WETH_OP_UNIV3_POOL, + tokenIn: OP_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); + + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price2, 837368983916789); + } + + function test_velo() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: WETH_OP_VELO_POOL, + tokenIn: WETH_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); + + uint256 expected = 1192241375504066768022; + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price, expected); + + route.oracles[0] = + Oracle({source: WETH_OP_VELO_POOL, tokenIn: OP_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price2, 838022983982765); + } + + function test_compatibilityVeloRamses() public { + vm.createSelectFork(vm.envString("ARBITRUM_RPC"), 247299346); + oracleAggregator = new OracleAggregator(); + + address WETH_RAM_POOL = 0x1E50482e9185D9DAC418768D14b2F2AC2b4DAF39; + address ARB_WETH_ADDRESS = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + address RAM_ADDRESS = 0xAAA6C1E32C55A7Bfa8066A6FAE9b42650F262418; + + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: WETH_RAM_POOL, // WETH/RAM + tokenIn: ARB_WETH_ADDRESS, // WETH + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); + + uint256 expected = 134553924611581644372855; + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price, expected); + + route.oracles[0] = + Oracle({source: WETH_RAM_POOL, tokenIn: RAM_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Velo}); + + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price2, 7395874629137); + } + + function test_compatibilityUniV3Slipstream() public { + // different block time + vm.rollFork(125193811); + oracleAggregator = new OracleAggregator(); + address WETH_OP_SLIPSTREAM = 0x4DC22588Ade05C40338a9D95A6da9dCeE68Bcd60; + + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: WETH_OP_SLIPSTREAM, + tokenIn: WETH_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price, 1471202414273643349311); + + route.oracles[0] = Oracle({ + source: WETH_OP_SLIPSTREAM, + tokenIn: OP_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); + + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price2, 679716122199076); + } + + // velo stable pairs have a different pricing method + function test_veloStable() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: ERN_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); + + uint256 expected = 982575; + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e18); + assertEq(price, expected); + + route.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); + + uint256 price2 = oracleAggregator.fetchMultiHopPrice(route, 1e6); + assertEq(price2, 1017732658860914652); + } + + function test_balancer() public { + address VMEX = 0x6D2E5b8841a6Aa5f0f973436357f75D3Eeb93312; + address VMEX_POOL = 0x4Dde571Dc66217a062e4B50f9b20c4D08b3245a0; + OracleRoute memory route; + + route.oracles = new Oracle[](1); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000002101414223776 ether); + + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 475870.006344163244524176 ether); + + // check decimal normalization + vm.mockCall(VMEX, abi.encodeWithSelector(ERC20.decimals.selector), abi.encode(6)); + + route.oracles = new Oracle[](1); + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: VMEX, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 2101414.223776 ether); + + route.oracles[0] = + Oracle({source: VMEX_POOL, tokenIn: WETH_ADDRESS, windowOrDecimalOffset: 3600, kind: OracleKind.Balancer}); + assertEq(oracleAggregator.fetchMultiHopPrice(route, 1e18), 0.000000475870006344 ether); + } + + /* function test_priceFeed() public { + OracleRoute memory route; + + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); + + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e8); + console.log("price", price); + } */ + + function test_twoPrices() public { + OracleRoute[] memory _ernForUsdcAllOracles = new OracleRoute[](2); + + OracleRoute memory _veloOracle; + _veloOracle.oracles = new Oracle[](1); + _veloOracle.oracles[0] = Oracle({ + source: USDC_ERN_VELO_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.Velo + }); + + OracleRoute memory _uniV3Oracle; + _uniV3Oracle.oracles = new Oracle[](1); + _uniV3Oracle.oracles[0] = Oracle({ + source: USDC_ERN_UNIV3_POOL, + tokenIn: USDC_ADDRESS, + windowOrDecimalOffset: 3600, + kind: OracleKind.UniV3 + }); + + // OracleRoute memory _priceFeedOracle; + // _priceFeedOracle.oracles = new Oracle[](1); + // _priceFeedOracle.oracles[0] = + // Oracle({source: PRICE_FEED, tokenIn: WBTC_ADDRESS, windowOrDecimalOffset: 0, kind: OracleKind.PriceFeed}); + + _ernForUsdcAllOracles[0] = _veloOracle; + _ernForUsdcAllOracles[1] = _uniV3Oracle; + // _ernForUsdcAllOracles[2] = _priceFeedOracle; + + uint256[] memory prices = oracleAggregator.fetchTwapPrices(_ernForUsdcAllOracles, 10_000 * 1e6); + + uint256 priceUniV3 = oracleAggregator.fetchMultiHopPrice(_uniV3Oracle, 1e10); + uint256 priceVelo = oracleAggregator.fetchMultiHopPrice(_veloOracle, 1e10); + + assertEq(prices[0], priceVelo); + assertEq(prices[1], priceUniV3); + + uint256 price = oracleAggregator.getReliablePrice(_ernForUsdcAllOracles, 1e10, 500, 25_000); + assertEq(price, 10161025621902873496771); + } + + function test_chainLink() public { + OracleRoute memory route; + route.oracles = new Oracle[](1); + route.oracles[0] = Oracle({ + source: 0x13e3Ee699D1909E989722E753853AE30b17e08c5, + tokenIn: address(1), + windowOrDecimalOffset: 12, + kind: OracleKind.Chainlink + }); + uint256 price = oracleAggregator.fetchMultiHopPrice(route, 1e6); + assertEq(price, 285000000000000); + } +}