-
Notifications
You must be signed in to change notification settings - Fork 2
Feat/multiple oracles #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xrave110
wants to merge
18
commits into
master
Choose a base branch
from
feat/multipleOracles
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
ed039cf
handling multiple prices and velo twaps
xrave110 c7872de
Tests updates and adjustments
xrave110 0fb2f31
New velo twap added and improved interfaces
xrave110 47e8696
Disabled ReaperStrategyStabilityPool.t tests
xrave110 59cbf93
Cleaning
xrave110 fccc79f
Name refactor
xrave110 4b68bc0
Create OracleAggregator
lookeey affe3f8
Use MAD instead of standard deviation
lookeey ceed2cf
add natspec
lookeey 0ee142e
Integrate OracleAggregator into Strategy
lookeey 94ac0b7
improve tests
lookeey 7872c09
minor fixes
lookeey 6875246
standardize return variable names
lookeey 51b3d3e
change number of good prices required
lookeey 4e64860
rework oracle + refactor
lookeey 2d35091
review fixes
lookeey fd8433c
review fixes
lookeey 7225b09
review fixes
lookeey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| [submodule "lib/vault-v2"] | ||
| path = lib/vault-v2 | ||
| url = git@github.com:Byte-Masons/vault-v2.git | ||
| url = https://github.com/Byte-Masons/vault-v2 | ||
| [submodule "lib/v3-core"] | ||
| path = lib/v3-core | ||
| url = https://github.com/Uniswap/v3-core | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "editor.formatOnSave": true, | ||
| "editor.defaultFormatter": "NomicFoundation.hardhat-solidity", | ||
| "solidity.formatter": "forge" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,4 +14,4 @@ main() | |
| .catch((error) => { | ||
| console.error(error); | ||
| process.exit(1); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| // SPDX-License-Identifier: BUSL1.1 | ||
|
|
||
| pragma solidity ^0.8.0; | ||
|
|
||
| import {VeloTwapMixin} from "./oracles/VeloTwapMixin.sol"; | ||
| import {UniV3TwapMixin} from "./oracles/UniV3TwapMixin.sol"; | ||
| import {BalancerTwapMixin} from "./oracles/BalancerTwapMixin.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"; | ||
|
|
||
| enum OracleKind { | ||
| Velo, | ||
| UniV3, | ||
| Balancer, | ||
| Chainlink | ||
| } | ||
|
|
||
| struct OracleRoute { | ||
| Oracle[] oracles; | ||
| } | ||
|
|
||
| struct Oracle { | ||
| address source; | ||
| address tokenIn; | ||
| uint256 windowOrDecimalOffset; | ||
| 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, BalancerTwapMixin, ChainlinkAdapterMixin { | ||
| error Oracle_InvalidKind(); | ||
| error Oracle_PricesSpreadTooHigh(); | ||
| error Oracle_PricesUnreliable(); | ||
| error Oracle_InvalidInput(); | ||
|
|
||
| uint256 constant BPS = 10_000; | ||
|
|
||
| // @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) { | ||
| 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) { | ||
|
lookeey marked this conversation as resolved.
|
||
| return prices[0]; | ||
| } 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); | ||
|
lookeey marked this conversation as resolved.
|
||
| } | ||
| uint256 delayedMean = getMean(delayedPrices); | ||
| 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); | ||
|
lookeey marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // @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] = _fetchMultiHopPrice(oracles[i], amountIn, false); | ||
| } | ||
| } | ||
|
|
||
| /// @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) { | ||
| return _fetchMultiHopPrice(route, amountIn, false); | ||
| } | ||
|
|
||
| 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 _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; | ||
| } | ||
| } | ||
|
|
||
| /// @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) | ||
| { | ||
| 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, period, ago, amountIn); | ||
| } else if (oracle.kind == OracleKind.UniV3) { | ||
| return getUniV3Price(oracle.source, oracle.tokenIn, period, ago, amountIn); | ||
| } else if (oracle.kind == OracleKind.Balancer) { | ||
| 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 { | ||
|
lookeey marked this conversation as resolved.
|
||
| revert Oracle_InvalidKind(); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Get the mean price of a list of prices, filtering out outliers from 3+ price lists. | ||
| /// @param prices List of prices | ||
| /// @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 _getValidatedMeanPrice(uint256[] memory prices, uint256 spreadTolerance, uint256 maxScoreBPS) | ||
| public | ||
|
lookeey marked this conversation as resolved.
|
||
| pure | ||
| returns (uint256 mean) | ||
| { | ||
| (bool[] memory isInvalid, uint256 mad, uint256 median) = _getValidityByZScore(prices, maxScoreBPS); | ||
| uint256 nrOfValidPrices; | ||
| (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(); | ||
| 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 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) | ||
| internal | ||
| pure | ||
| returns (bool[] memory isInvalid, uint256 mad, uint256 median) | ||
| { | ||
| (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); | ||
| } | ||
|
|
||
| // 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; | ||
| 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); | ||
| } | ||
| } | ||
|
|
||
| /// @notice Get the Median Absolute Deviation of a list of values | ||
| /// @param arr List of values | ||
| 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) { | ||
| 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)); | ||
|
|
||
| if (n % 2 == 0) { | ||
| mad = (deviations[n / 2 - 1] + deviations[n / 2]) / 2; | ||
| } else { | ||
| mad = deviations[n / 2]; | ||
| } | ||
| } | ||
|
|
||
| function getMeanValid(uint256[] memory prices, bool[] memory isInvalid) | ||
| internal | ||
| pure | ||
| returns (uint256 mean, uint256 nrValidPrices) | ||
| { | ||
| uint256 sum = 0; | ||
| for (uint256 i = 0; i < prices.length; i++) { | ||
| if (!isInvalid[i]) { | ||
| sum += prices[i]; | ||
| nrValidPrices++; | ||
| } | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.