Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitmodules
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
Comment thread
lookeey marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .vscode/settings.json
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"
}
6 changes: 4 additions & 2 deletions README.md
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.
13 changes: 13 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,17 @@ src = "src"
out = "out"
libs = ["lib"]

line_length = 170
fs_permissions = [{ access = "read", path = "./test/"}]

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
1 change: 1 addition & 0 deletions lib/v3-core
Submodule v3-core added at 6562c5
6 changes: 0 additions & 6 deletions remappings.txt

This file was deleted.

2 changes: 1 addition & 1 deletion script/upgrade/validateUpgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ main()
.catch((error) => {
console.error(error);
process.exit(1);
});
});
262 changes: 262 additions & 0 deletions src/OracleAggregator.sol
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) {
Comment thread
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);
Comment thread
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);
Comment thread
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 {
Comment thread
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
Comment thread
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;
}
}
Loading