diff --git a/.gitmodules b/.gitmodules
index 81a3ec5..047f42b 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
+ 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/.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/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 4ff40c4..27a054d 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -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
\ No newline at end of file
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
deleted file mode 100644
index b42cd60..0000000
--- a/remappings.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-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/
-oz/=lib/vault-v2/lib/openzeppelin-contracts/contracts/
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/OracleAggregator.sol b/src/OracleAggregator.sol
new file mode 100644
index 0000000..7174f73
--- /dev/null
+++ b/src/OracleAggregator.sol
@@ -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) {
+ 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);
+ }
+ 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);
+ }
+
+ // @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 {
+ 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
+ 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;
+ }
+}
diff --git a/src/ReaperStrategyStabilityPool.sol b/src/ReaperStrategyStabilityPool.sol
index f370728..79421aa 100644
--- a/src/ReaperStrategyStabilityPool.sol
+++ b/src/ReaperStrategyStabilityPool.sol
@@ -14,32 +14,37 @@ 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, OracleRoute, OracleKind, Oracle} from "./OracleAggregator.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;
+ // constants
+
+ 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
- IUniswapV3Pool public uniV3UsdcErnPool;
- IStaticOracle public uniV3TWAP;
+ 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
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
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;
+
struct ExchangeSettings {
address veloRouter;
address balVault;
@@ -47,25 +52,19 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 {
address uniV2Router;
}
- struct Pools {
- address stabilityPool;
- address uniV3UsdcErnPool;
- }
-
struct Tokens {
address want;
address usdc;
}
error InvalidUsdcToErnExchange(uint256 exchangeEnum);
- error InvalidUsdcToErnTWAP(uint256 twapEnum);
- error TWAPOutsideAllowedRange(uint256 usdcPrice);
+ error TWAPOutsideAllowedRange(uint256 ernPrice);
error InvalidSwapStep();
-
/**
* @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,
@@ -73,9 +72,10 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 {
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");
@@ -85,28 +85,26 @@ 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(_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(_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;
- uniV3TWAP = IStaticOracle(_uniV3TWAP);
- uniV3UsdcErnPool = IUniswapV3Pool(_pools.uniV3UsdcErnPool);
compoundingFeeMarginBPS = 9950;
- updateUniV3TWAPPeriod(7200);
- updateAcceptableTWAPBounds(980_000, 1_100_000);
+ updateOracles(_ernForUsdcOracles);
+ updateAcceptableTWAPBounds(0.98 ether, 1.1 ether);
}
/**
@@ -168,14 +166,11 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 {
if (shouldOverrideHarvestBlock) {
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, uniV3TWAPPeriod);
-
- if (usdcAmount < acceptableTWAPLowerBound || usdcAmount > acceptableTWAPUpperBound) {
- revert TWAPOutsideAllowedRange(usdcAmount);
+ uint128 usdcAmount = 1e6; // 1 ERN
+ uint256 ernAmount = _getErnAmountForUsdc(usdcAmount);
+
+ if (ernAmount < acceptableTWAPLowerBound || ernAmount > acceptableTWAPUpperBound) {
+ revert TWAPOutsideAllowedRange(ernAmount);
}
}
@@ -242,20 +237,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.
*/
function getERNValueOfCollateralGain() public view returns (uint256 ernValueOfCollateral) {
uint256 usdValueOfCollateralGain = getUSDValueOfCollateralGain();
- ernValueOfCollateral = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain);
+ uint256 totalUsdcValue = getERNValueOfCollateralGainCommon(usdValueOfCollateralGain);
+ ernValueOfCollateral = _getErnAmountForUsdc(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);
}
/**
@@ -264,12 +261,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;
}
/**
@@ -312,26 +308,14 @@ 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) {
if (_usdcAmount != 0) {
- expectedErnAmount = getErnAmountForUsdcUniV3(uint128(_usdcAmount), uniV3TWAPPeriod);
+ return oracleAggregator.getReliablePrice(ernForUsdcOracles, _usdcAmount, SPREAD_TOLERANCE, 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 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)
@@ -408,22 +392,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.
@@ -469,32 +437,14 @@ 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 price oracle configuration.
*/
- 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 < 300, "TWAP duration change would change price");
+ function updateOracles(OracleRoute[] calldata newRoutes) public {
+ _atLeastRole(DEFAULT_ADMIN_ROLE);
+ // ernForUsdcOracles = newRoutes;
+ delete ernForUsdcOracles;
+ for (uint256 i = 0; i < newRoutes.length; i++) {
+ ernForUsdcOracles.push(newRoutes[i]);
}
}
@@ -513,8 +463,8 @@ contract ReaperStrategyStabilityPool is ReaperBaseStrategyv4 {
*/
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/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/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/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/interfaces/IVeloPair.sol b/src/interfaces/IVeloPair.sol
new file mode 100644
index 0000000..c936553
--- /dev/null
+++ b/src/interfaces/IVeloPair.sol
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+struct Cumulatives {
+ uint256 reserve0Cumulative;
+ uint256 reserve1Cumulative;
+ uint256 blockTimestamp;
+}
+
+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 reserve0CumulativeLast() external view returns (uint256);
+
+ function reserve1CumulativeLast() external view returns (uint256);
+
+ 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);
+
+ function sample(address tokenIn, uint256 amountIn, uint256 points, uint256 window)
+ external
+ 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/BalancerTwapMixin.sol b/src/oracles/BalancerTwapMixin.sol
new file mode 100644
index 0000000..5a7e8e2
--- /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, uint32 ago, uint256 amountIn)
+ public
+ view
+ returns (uint256 price)
+ {
+ 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: ago
+ });
+ 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) {
+ price = targetPrice / 10 ** decimalDifference;
+ } else {
+ price = targetPrice * 10 ** decimalDifference;
+ }
+ } else {
+ uint256 decimalDifference = decimals1 - decimals0;
+ if (tokenInToken0) {
+ price = targetPrice * 10 ** decimalDifference;
+ } else {
+ price = targetPrice / 10 ** decimalDifference;
+ }
+ }
+ }
+}
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/src/oracles/UniV3TwapMixin.sol b/src/oracles/UniV3TwapMixin.sol
new file mode 100644
index 0000000..cf0197b
--- /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 tokenIn, uint32 period, uint32 ago, uint256 amountIn)
+ public
+ view
+ returns (uint256 price)
+ {
+ require(period != 0, "BP");
+
+ uint32[] memory secondsAgos = new uint32[](2);
+ secondsAgos[0] = period + ago;
+ secondsAgos[1] = ago;
+
+ (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 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 = tokenOut > tokenIn
+ ? FullMath.mulDiv(ratioX192, amountIn, 1 << 192)
+ : FullMath.mulDiv(1 << 192, amountIn, ratioX192);
+ } else {
+ uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
+ 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
new file mode 100644
index 0000000..fe0701d
--- /dev/null
+++ b/src/oracles/VeloTwapMixin.sol
@@ -0,0 +1,224 @@
+// 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 tokenIn, uint32 period, uint32 ago, uint256 amountIn)
+ public
+ view
+ returns (uint256 price)
+ {
+ IVeloPair pair = IVeloPair(source);
+ Cumulatives memory end;
+ Cumulatives memory start;
+ uint256 observationLength = pair.observationLength();
+
+ 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);
+ }
+ }
+
+ 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, and additional comments for clarification.
+ */
+ struct GetAmountOutLocalVars {
+ uint256 decimals0;
+ uint256 decimals1;
+ 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,
+ 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);
+ }
+ }
+
+ // 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
+ 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
+ }
+ }
+
+ // 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);
+ 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);
+ }
+
+ // _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
+ 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");
+ }
+
+}
diff --git a/test/OracleAggregatorTest.t.sol b/test/OracleAggregatorTest.t.sol
new file mode 100644
index 0000000..2cb02a0
--- /dev/null
+++ b/test/OracleAggregatorTest.t.sol
@@ -0,0 +1,78 @@
+// 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 5% 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_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._getValidatedMeanPrice(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._getValidatedMeanPrice(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._getValidatedMeanPrice(testCase.prices, maxMadRelativeToMedianBPS, maxScoreBPS);
+
+ assertEq(result, testCase.expected, "Unexpected result");
+ }
+ }
+}
diff --git a/test/OraclesForkTests.t.sol b/test/OraclesForkTests.t.sol
new file mode 100644
index 0000000..ab90cf5
--- /dev/null
+++ b/test/OraclesForkTests.t.sol
@@ -0,0 +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);
+ }
+
+ 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);
+ }
+}
diff --git a/test/ReaperStrategyStabilityPool.t.sol b/test/ReaperStrategyStabilityPool.t.sol
index bd4febb..4f66dcb 100644
--- a/test/ReaperStrategyStabilityPool.t.sol
+++ b/test/ReaperStrategyStabilityPool.t.sol
@@ -14,11 +14,13 @@ 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";
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,8 +30,10 @@ contract ReaperStrategyStabilityPoolTest is Test {
// Registry
address public treasuryAddress = 0xeb9C9b785aA7818B2EBC8f9842926c4B9f707e4B;
- address public stabilityPoolAddress = 0x8B147A2d4Fc3598079C64b8BF9Ad2f776786CFed;
- address public priceFeedAddress = 0xC6b3Eea38Cbe0123202650fB49c59ec41a406427;
+ address public stabilityPoolAddress = 0xD839A111598d5e27BD8f7A1A18ce9Bf079F0c0a2;
+ address public communityIssuanceOwner = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C;
+ address public communityIssuanceAddress = 0x323A9C4CB4Be7A3c9d31209B0a2bAd56276bbf89;
+ address public priceFeedAddress = 0xadd6F326a395629926D9a535d809B5e3d8c7FE8d;
address public priceFeedOwnerAddress = 0xf1a717766c1b2Ed3f63b602E6482dD699ce1C79C;
address public troveManager = 0xd584A5E956106DB2fE74d56A0B14a9d64BE8DC93;
address public veloRouter = 0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858;
@@ -38,7 +42,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;
@@ -50,17 +54,17 @@ 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 opAddress = 0x4200000000000000000000000000000000000042;
+ address public usdcAddress = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85;
+ address public oathAddress = 0x00e1724885473B63bCE08a9f0a52F35b0979e35A;
+ address public wstethAddress = 0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb;
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;
+ address public wstethHolder = 0x583f5777a69830fCB6F811a1b8e781D545D37923;
bytes32 public balErnPoolId = 0x1d95129c18a8c91c464111fdf7d0eb241b37a9850002000000000000000000c1;
bytes32 public oatsAndGrainPoolId = 0x1cc3e990b23a09fc9715aaf7ccf21c212a9cbc160001000000000000000000bd;
@@ -69,7 +73,7 @@ contract ReaperStrategyStabilityPoolTest is Test {
AggregatorV3Interface wbtcAggregator;
AggregatorV3Interface wethAggregator;
- AggregatorV3Interface opAggregator;
+ AggregatorV3Interface wstethAggregator;
AggregatorV3Interface usdcAggregator;
address[] keepers = [
@@ -104,6 +108,7 @@ contract ReaperStrategyStabilityPoolTest is Test {
ReaperStrategyStabilityPool public implementation;
ERC1967Proxy public proxy;
ReaperStrategyStabilityPool public wrappedProxy;
+ OracleAggregator public oracleAggregator;
ISwapper public swapper;
@@ -113,18 +118,18 @@ contract ReaperStrategyStabilityPoolTest is Test {
function setUp() public {
// Forking
string memory rpc = vm.envString("RPC");
- optimismFork = vm.createSelectFork(rpc, 107994026);
+ optimismFork = vm.createSelectFork(rpc, 118425007 /*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);
+ vault = new ReaperVaultV2(
+ wantAddress, vaultName, vaultSymbol, vaultTvlCap, treasuryAddress, strategists, multisigRoles
+ );
implementation = new ReaperStrategyStabilityPool();
proxy = new ERC1967Proxy(address(implementation), "");
wrappedProxy = ReaperStrategyStabilityPool(address(proxy));
@@ -135,10 +140,6 @@ contract ReaperStrategyStabilityPoolTest is Test {
exchangeSettings.uniV3Router = uniV3Router;
exchangeSettings.uniV2Router = uniV2Router;
- ReaperStrategyStabilityPool.Pools memory pools;
- pools.stabilityPool = stabilityPoolAddress;
- pools.uniV3UsdcErnPool = uniV3UsdcErnPool;
-
address[] memory usdcErnPath = new address[](2);
usdcErnPath[0] = usdcAddress;
usdcErnPath[1] = wantAddress;
@@ -147,7 +148,20 @@ contract ReaperStrategyStabilityPoolTest is Test {
tokens.want = wantAddress;
tokens.usdc = usdcAddress;
- 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, windowOrDecimalOffset: 3600, kind: OracleKind.Velo});
+
+ OracleRoute memory _uniV3Oracle;
+ _uniV3Oracle.oracles = new Oracle[](1);
+ _uniV3Oracle.oracles[0] =
+ Oracle({source: uniV3UsdcErnPool, tokenIn: usdcAddress, windowOrDecimalOffset: 3600, kind: OracleKind.UniV3});
+
+ _ernForUsdcAllOracles[0] = _veloOracle;
+ _ernForUsdcAllOracles[1] = _uniV3Oracle;
wrappedProxy.initialize(
address(vault),
@@ -156,9 +170,10 @@ contract ReaperStrategyStabilityPoolTest is Test {
multisigRoles,
keepers,
priceFeedAddress,
- uniV3TWAP,
+ address(new OracleAggregator()),
+ _ernForUsdcAllOracles,
exchangeSettings,
- pools,
+ stabilityPoolAddress,
tokens
);
@@ -187,7 +202,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] =
@@ -205,17 +220,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;
@@ -235,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,
@@ -278,17 +295,17 @@ 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
});
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;
@@ -313,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);
}
@@ -647,6 +670,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);
@@ -779,7 +803,8 @@ contract ReaperStrategyStabilityPoolTest is Test {
console.log("poolBalanceBefore: ", poolBalanceBefore);
console.log("poolBalanceAfter: ", poolBalanceAfter);
- uint32 currentUniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod();
+ /// @TODO
+ /* uint32 currentUniV3TWAPPeriod = wrappedProxy.uniV3TWAPPeriod();
address[] memory pools = new address[](1);
pools[0] = address(uniV3UsdcErnPool);
@@ -788,7 +813,7 @@ contract ReaperStrategyStabilityPoolTest is Test {
);
// 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;
@@ -823,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();
@@ -839,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);
@@ -873,7 +899,8 @@ 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 twapPeriod = wrappedProxy.uniV3TWAPPeriod();
console.log("twapPeriod: ", twapPeriod);
@@ -887,7 +914,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);
@@ -917,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);
@@ -968,7 +996,8 @@ 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;
@@ -1051,7 +1080,7 @@ contract ReaperStrategyStabilityPoolTest is Test {
priceQuoteSpot = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, 0);
console.log("priceQuote9: ", priceQuote);
console.log("priceQuoteSpot9: ", priceQuoteSpot);
- }
+ } */
function testUniV3TWAPSingleSwap() public {
uint32 period = 3600;
@@ -1078,7 +1107,8 @@ contract ReaperStrategyStabilityPoolTest is Test {
// _skipBlockAndTime(1);
// }
- uint256 priceQuote = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, period);
+ /// @TODO
+ /* uint256 priceQuote = wrappedProxy.getErnAmountForUsdcUniV3(usdcUnit, period);
console.log("priceQuote: ", priceQuote);
@@ -1102,10 +1132,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);
@@ -1124,9 +1155,10 @@ 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);
@@ -1156,7 +1188,7 @@ contract ReaperStrategyStabilityPoolTest is Test {
wrappedProxy.updateUniV3TWAPPeriod(newPeriod);
vm.stopPrank();
wrappedProxy.updateUniV3TWAPPeriod(oldPeriod);
- }
+ } */
function liquidateTroves(address asset) internal {
ITroveManager(troveManager).liquidateTroves(asset, 100);
@@ -1189,7 +1221,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..77029a9
--- /dev/null
+++ b/test/test_cases.json
@@ -0,0 +1,49 @@
+{
+ "testCases": [
+ {
+ "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