diff --git a/contracts/feeds/OverlayV1Feed.sol b/contracts/feeds/OverlayV1Feed.sol
index 5adc6671..513f969f 100644
--- a/contracts/feeds/OverlayV1Feed.sol
+++ b/contracts/feeds/OverlayV1Feed.sol
@@ -18,12 +18,13 @@ abstract contract OverlayV1Feed is IOverlayV1Feed {
feedFactory = msg.sender;
}
- /// @dev returns freshest possible data from oracle
+ /// @return Freshest possible data from oracle
function latest() external view returns (Oracle.Data memory) {
return _fetch();
}
- /// @dev fetches data from oracle. should be implemented differently
- /// @dev for each feed type
+ /// @dev Fetches data from oracle.
+ /// @dev Should be implemented differently for every Oracle's feed type
+ /// @return Freshest possible data from oracle
function _fetch() internal view virtual returns (Oracle.Data memory);
}
diff --git a/contracts/feeds/OverlayV1FeedFactory.sol b/contracts/feeds/OverlayV1FeedFactory.sol
index 3bb0cd59..2afbd279 100644
--- a/contracts/feeds/OverlayV1FeedFactory.sol
+++ b/contracts/feeds/OverlayV1FeedFactory.sol
@@ -8,11 +8,14 @@ abstract contract OverlayV1FeedFactory is IOverlayV1FeedFactory {
uint256 public immutable microWindow;
uint256 public immutable macroWindow;
- // registry of deployed feeds by factory
+ /// Registry of deployed feeds by factory
mapping(address => bool) public isFeed;
+ /// Event emitted on newly deployed feed
event FeedDeployed(address indexed user, address feed);
+ /// @param _microWindow Micro window to define TWAP over (typically 600s)
+ /// @param _macroWindow Macro window to define TWAP over (typically 3600s)
constructor(uint256 _microWindow, uint256 _macroWindow) {
// sanity checks on micro and macroWindow
require(_microWindow > 0, "OVLV1: microWindow == 0");
diff --git a/contracts/feeds/balancerv2/OverlayV1BalancerV2Factory.sol b/contracts/feeds/balancerv2/OverlayV1BalancerV2Factory.sol
new file mode 100644
index 00000000..b9a20c8f
--- /dev/null
+++ b/contracts/feeds/balancerv2/OverlayV1BalancerV2Factory.sol
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.10;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import "../../interfaces/feeds/balancerv2/IOverlayV1BalancerV2FeedFactory.sol";
+
+import "../../libraries/balancer/balancer-v2-monorepo/BalancerV2PoolInfo.sol";
+import "../OverlayV1FeedFactory.sol";
+import "./OverlayV1BalancerV2Feed.sol";
+
+contract OverlayV1BalancerV2Factory is IOverlayV1BalancerV2FeedFactory, OverlayV1FeedFactory {
+ using BalancerV2PoolInfo for BalancerV2PoolInfo.Pool;
+ address public immutable ovlWethPool;
+ address public immutable ovl;
+
+ // registry of feeds; for a given (pool, base, quote, amount) pair, returns associated feed
+ mapping(address => mapping(address => mapping(address => mapping(uint128 => address))))
+ public getFeed;
+
+ /// @notice Constructs a new OverlayV1UniswapV3Factory contract for used to deploy new
+ /// @notice UniswapV3 feeds to be offered as a market
+ /// @param _ovlWethPool Address of OVL <-> WETH pool being offered as a market
+ /// @param _ovl Address of OVL tokens
+ /// @param _microWindow Micro window to define TWAP over (typically 600s)
+ /// @param _macroWindow Macro window to define TWAP over (typically 3600s)
+ constructor(
+ address _ovlWethPool,
+ address _ovl,
+ uint256 _microWindow,
+ uint256 _macroWindow
+ ) OverlayV1FeedFactory(_microWindow, _macroWindow) {
+ ovlWethPool = _ovlWethPool;
+ ovl = _ovl;
+ }
+
+ /// @notice Deploys a new OverlayV1BalancerV2Feed contract
+ /// @param marketPool Address of pool being offered as a market
+ /// @param marketBaseToken The base token address of the pool
+ /// @param marketQuoteToken The quote token address of the pool
+ /// @param marketBaseAmount TODO
+ function deployFeed(
+ address vault,
+ address marketPool,
+ address marketBaseToken,
+ address marketQuoteToken,
+ uint128 marketBaseAmount
+ ) external returns (address feed_) {
+ require(
+ getFeed[marketPool][marketBaseToken][marketQuoteToken][marketBaseAmount] == address(0),
+ "OVLV1: feed already exists"
+ );
+
+ BalancerV2PoolInfo.Pool memory balancerV2Pool = BalancerV2PoolInfo.Pool(
+ vault,
+ marketPool,
+ ovlWethPool,
+ ovl,
+ marketBaseToken,
+ marketQuoteToken,
+ marketBaseAmount
+ );
+
+ // Use the CREATE2 opcode to deploy a new Feed contract.
+ // Will revert if feed which accepts (marketPool, marketBaseToken, marketQuoteToken,
+ // marketBaseAmount) in its constructor has already been deployed since salt would be the
+ // same and can't deploy with it twice.
+ feed_ = address(
+ new OverlayV1BalancerV2Feed{
+ salt: keccak256(
+ abi.encode(marketPool, marketBaseToken, marketQuoteToken, marketBaseAmount)
+ )
+ }(balancerV2Pool, microWindow, macroWindow)
+ );
+
+ // store feed registry record for (marketPool, marketBaseToken, marketQuoteToken) combo
+ // and record address as deployed feed
+ getFeed[marketPool][marketBaseToken][marketQuoteToken][marketBaseAmount] = feed_;
+ isFeed[feed_] = true;
+ emit FeedDeployed(msg.sender, feed_);
+ }
+}
diff --git a/contracts/feeds/balancerv2/OverlayV1BalancerV2Feed.sol b/contracts/feeds/balancerv2/OverlayV1BalancerV2Feed.sol
new file mode 100644
index 00000000..b4dd027f
--- /dev/null
+++ b/contracts/feeds/balancerv2/OverlayV1BalancerV2Feed.sol
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+pragma solidity 0.8.10;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "../OverlayV1Feed.sol";
+import "../../interfaces/feeds/balancerv2/IBalancerV2Pool.sol";
+import "../../interfaces/feeds/balancerv2/IBalancerV2Vault.sol";
+import "../../interfaces/feeds/balancerv2/IBalancerV2PriceOracle.sol";
+import "../../interfaces/feeds/balancerv2/IOverlayV1BalancerV2Feed.sol";
+import "../../libraries/balancer/balancer-v2-monorepo/BalancerV2PoolInfo.sol";
+import "../../libraries/FixedPoint.sol";
+
+contract OverlayV1BalancerV2Feed is IOverlayV1BalancerV2Feed, OverlayV1Feed {
+ using FixedPoint for uint256;
+
+ // TODO: Do not hardcode. Follow `ovlXPool` implementation in UniswapV3 feed
+ address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
+
+ address public immutable marketPool;
+ address public immutable ovlWethPool;
+ address public immutable ovl;
+
+ address public immutable marketToken0;
+ address public immutable marketToken1;
+
+ address public immutable ovlWethToken0;
+ address public immutable ovlWethToken1;
+
+ address public immutable marketBaseToken;
+ address public immutable marketQuoteToken;
+
+ constructor(
+ BalancerV2PoolInfo.Pool memory balancerV2Pool,
+ uint256 _microWindow,
+ uint256 _macroWindow
+ ) OverlayV1Feed(_microWindow, _macroWindow) {
+ bytes32 marketPoolId = getPoolId(balancerV2Pool.marketPool);
+ bytes32 ovlWethPoolId = getPoolId(balancerV2Pool.ovlWethPool);
+ (IERC20[] memory marketTokens, , ) = getPoolTokens(balancerV2Pool.vault, marketPoolId);
+ (IERC20[] memory ovlWethTokens, , ) = getPoolTokens(balancerV2Pool.vault, ovlWethPoolId);
+
+ address _marketToken0 = address(marketTokens[0]); // DAI
+ address _marketToken1 = address(marketTokens[1]); // WETH
+
+ require(_marketToken0 == WETH || _marketToken1 == WETH, "OVLV1Feed: marketToken != WETH");
+ marketToken0 = _marketToken0;
+ marketToken1 = _marketToken1;
+
+ require(
+ _marketToken0 == balancerV2Pool.marketBaseToken ||
+ _marketToken1 == balancerV2Pool.marketBaseToken,
+ "OVLV1Feed: marketToken != marketBaseToken"
+ );
+ require(
+ _marketToken0 == balancerV2Pool.marketQuoteToken ||
+ _marketToken1 == balancerV2Pool.marketQuoteToken,
+ "OVLV1Feed: marketToken != marketQuoteToken"
+ );
+
+ marketBaseToken = balancerV2Pool.marketBaseToken;
+ marketQuoteToken = balancerV2Pool.marketQuoteToken;
+
+ // need OVL/WETH pool for ovl vs ETH price to make reserve conversion from ETH => OVL
+ address _ovlWethToken0 = address(ovlWethTokens[0]);
+ address _ovlWethToken1 = address(ovlWethTokens[1]);
+
+ require(
+ _ovlWethToken0 == WETH || _ovlWethToken1 == WETH,
+ "OVLV1Feed: ovlWethToken != WETH"
+ );
+ require(
+ _ovlWethToken0 == balancerV2Pool.ovl || _ovlWethToken1 == balancerV2Pool.ovl,
+ "OVLV1Feed: ovlWethToken != OVL"
+ );
+ ovlWethToken0 = _ovlWethToken0;
+ ovlWethToken1 = _ovlWethToken1;
+
+ marketPool = balancerV2Pool.marketPool;
+ ovlWethPool = balancerV2Pool.ovlWethPool;
+ ovl = balancerV2Pool.ovl;
+ }
+
+ /// @notice Returns the OracleAverageQuery struct containing information for a TWAP query
+ /// @dev Builds the OracleAverageQuery struct required to retrieve TWAPs from the
+ /// @dev getTimeWeightedAverage function
+ /// @param variable Queryable values pertinent to this contract: PAIR_PRICE and INVARIANT
+ /// @param secs Duration of TWAP in seconds
+ /// @param ago End of TWAP in seconds
+ /// @return query Information for a TWAP query
+ function getOracleAverageQuery(
+ IBalancerV2PriceOracle.Variable variable,
+ uint256 secs,
+ uint256 ago
+ ) public view returns (IBalancerV2PriceOracle.OracleAverageQuery memory) {
+ return IBalancerV2PriceOracle.OracleAverageQuery(variable, secs, ago);
+ }
+
+ /// @notice Returns the time average weighted price corresponding to each of queries
+ /// @dev Prices are represented as 18 decimal fixed point values.
+ /// @dev Interfaces with the WeightedPool2Tokens contract and calls getTimeWeightedAverage
+ /// @param pool Pool address
+ /// @param queries Information for a time weighted average query
+ /// @return twaps_ Time weighted average price corresponding to each query
+ function getTimeWeightedAverage(
+ address pool,
+ IBalancerV2PriceOracle.OracleAverageQuery[] memory queries
+ ) public view returns (uint256[] memory twaps_) {
+ IBalancerV2PriceOracle priceOracle = IBalancerV2PriceOracle(pool);
+ twaps_ = priceOracle.getTimeWeightedAverage(queries);
+ }
+
+ /// @notice Returns the TWAI (time weighted average invariant) corresponding to a single query
+ /// @notice for the value of the pool's
+ /// @notice invariant, which is a measure of its liquidity
+ /// @dev Prices are dev represented as 18 decimal fixed point values
+ /// @dev Variable.INVARIANT is used to construct OracleAverageQuery struct
+ /// @param pool Pool address
+ /// @param secs Duration of TWAP in seconds
+ /// @param ago End of TWAP in seconds
+ /// @return result_ TWAP of inverse of tokens in pool
+ function getTimeWeightedAverageInvariant(
+ address pool,
+ uint256 secs,
+ uint256 ago
+ ) public view returns (uint256 result_) {
+ IBalancerV2PriceOracle.Variable variable = IBalancerV2PriceOracle.Variable.INVARIANT;
+
+ IBalancerV2PriceOracle.OracleAverageQuery[]
+ memory queries = new IBalancerV2PriceOracle.OracleAverageQuery[](1);
+ IBalancerV2PriceOracle.OracleAverageQuery memory query = IBalancerV2PriceOracle
+ .OracleAverageQuery(variable, secs, ago);
+ queries[0] = query;
+
+ uint256[] memory results = getTimeWeightedAverage(pool, queries);
+ result_ = results[0];
+ }
+
+ /// @notice Returns pool token information given a pool id
+ /// @dev Interfaces the WeightedPool2Tokens contract and calls getPoolTokens
+ /// @param balancerV2PoolId pool id
+ /// @return The pool's registered tokens
+ /// @return Total balances of each token in the pool
+ /// @return Most recent block in which any of the pool tokens were updated (never used)
+ function getPoolTokens(address vault, bytes32 balancerV2PoolId)
+ public
+ view
+ returns (
+ IERC20[] memory,
+ uint256[] memory,
+ uint256
+ )
+ {
+ IBalancerV2Vault vault = IBalancerV2Vault(vault);
+ return vault.getPoolTokens(balancerV2PoolId);
+ }
+
+ /// @notice Returns the pool id corresponding to the given pool address
+ /// @dev Interfaces with WeightedPool2Tokens contract and calls getPoolId
+ /// @param pool Pool address
+ /// @return poolId_ pool id corresponding to the given pool address
+ function getPoolId(address pool) public view returns (bytes32 poolId_) {
+ poolId_ = IBalancerV2Pool(pool).getPoolId();
+ }
+
+ /// @notice Returns the normalized weight of the token
+ /// @dev Weights are fixed point numbers that sum to FixedPoint.ONE
+ /// @dev Ex: a 60 WETH/40 DAI pool returns [400000000000000000 DAI, 600000000000000000 WETH]
+ /// @dev Interfaces with the WeightedPool2Tokens contract and calls getNormalizedWeights
+ /// @param pool Pool address
+ /// @return weights_ Normalized pool weights
+ function getNormalizedWeights(address pool) public view returns (uint256[] memory weights_) {
+ weights_ = IBalancerV2Pool(pool).getNormalizedWeights();
+ }
+
+ /// @dev V = B1 ** w1 * B2 ** w2
+ /// @param priceOverMicroWindow price TWAP, P = (B2 / B1) * (w1 / w2)
+ function getReserve(uint256 priceOverMicroWindow) public view returns (uint256 reserve_) {
+ uint256 twav = getTimeWeightedAverageInvariant(marketPool, microWindow, 0);
+ uint256 reserveInWeth = getReserveInWeth(twav, priceOverMicroWindow); // units WETH
+
+ uint256 ovlWethPairPrice = getPairPriceOvlWeth();
+ if (ovlWethToken0 == WETH) {
+ // if OVL is base (token0) then OVL/WETH price is #weth/#ovl
+ reserve_ = reserveInWeth.divUp(ovlWethPairPrice);
+ } else if (ovlWethToken1 == WETH) {
+ // if OVL is quote (token1) then OVL/WETH price is #ovl/#weth
+ reserve_ = reserveInWeth.mulUp(ovlWethPairPrice);
+ } else {
+ revert("OVLV1Feed: WETH not quote or base token");
+ }
+ }
+
+ /// @notice Gets reserve in WETH
+ /// @dev https://oips.overlay.market/notes/note-10
+ /// @dev k: token index, only two-token pools are used in the Balancer V2 WeightedPool2Tokens
+ /// @dev contract
+ /// @dev B_k: number of k type tokens in the pool
+ /// @dev w_k: weight for k type tokens in the pool, Σ_k w_k = 1
+ /// @dev Invariant for the spot pool for a two token pool: V = (B_0 ** w_0) * (B_1 ** w_1)
+ /// @dev Spot price inferred from the relative number of tokens in pool:
+ /// @dev P = (B_1 / B_0) * (w_0 / w_1)
+ /// @dev Assume: (w_0 + w_1) = 1 => so (1 / (w_0 + w_1)) = 1
+ /// @dev If the base token (marketToken0) is WETH, solve for B_0 using equations V and P:
+ /// @dev B_0 = [ twav * (priceOverMicroWindow * w_0 / w_1) ** w_1 ] ** (1 / (w_0 + w_1))
+ /// @dev B_0 = [ twav * (priceOverMicroWindow * w_0 / w_1) ** w_1 ] ** (1 / 1)
+ /// @dev If the quote token (marketToken1) is WETH, solve for B_1 using equations V and P
+ /// @dev B_1 = [ twav / (priceOverMicroWindow * w_1 / w_0) ** w_1 ] ** (1 / (w_0 + w_1))
+ /// @dev B_1 = [ twav / (priceOverMicroWindow * w_1 / w_0) ** w_1 ] ** (1 / 1)
+ /// @param twav Time weighted average invariant
+ /// @param priceOverMicroWindow TWAP over last micro window
+ /// @return reserveInWeth_ reserve in WETH
+ function getReserveInWeth(uint256 twav, uint256 priceOverMicroWindow)
+ public
+ view
+ returns (uint256 reserveInWeth_)
+ {
+ // Retrieve pool weights
+ // Ex: a 60 WETH/40 DAI pool returns [400000000000000000 DAI, 600000000000000000 WETH]
+ uint256[] memory normalizedWeights = getNormalizedWeights(marketPool);
+
+ // WeightedPool2Tokens contract only ever has two pools
+ uint256 weightToken0 = normalizedWeights[0]; // DAI
+ uint256 weightToken1 = normalizedWeights[1]; // WETH
+ uint256 denominator;
+ // If marketToken0 (the base token) is WETH, solve for B0
+ // If marketToken1 (the quote token) is WETH, solve for B1
+ if (marketToken0 == WETH) {
+ /// B_0 = [ twav * (priceOverMicroWindow * w_0 / w_1) ** w_0 ]
+ denominator = (priceOverMicroWindow.mulUp(weightToken0).divUp(weightToken1)).powUp(
+ weightToken0
+ );
+ } else if (marketToken1 == WETH) {
+ /// B_1 = [ twav / (priceOverMicroWindow * w_1 / w_0) ** w_1 ]
+ denominator = (priceOverMicroWindow.mulUp(weightToken1).divUp(weightToken0)).powUp(
+ weightToken1
+ );
+ } else {
+ revert("OVLV1Feed: WETH not a market token");
+ }
+ uint256 power = uint256(1).divUp(weightToken0.add(weightToken1));
+ reserveInWeth_ = twav.divUp(denominator).powUp(power);
+ }
+
+ /// @notice Market pool only (not reserve)
+ function getPairPriceOvlWeth() public view returns (uint256 twap_) {
+ /* Pair Price Calculations */
+ IBalancerV2PriceOracle.Variable variablePairPrice = IBalancerV2PriceOracle
+ .Variable
+ .PAIR_PRICE;
+
+ IBalancerV2PriceOracle.OracleAverageQuery[]
+ memory queries = new IBalancerV2PriceOracle.OracleAverageQuery[](1);
+
+ // [Variable enum, seconds, ago]
+ queries[0] = getOracleAverageQuery(variablePairPrice, microWindow, 0);
+
+ uint256[] memory twaps = getTimeWeightedAverage(ovlWethPool, queries);
+ twap_ = twaps[0];
+ }
+
+ /// @notice Market pool only (not reserve)
+ function getPairPrices() public view returns (uint256[] memory twaps_) {
+ /* Pair Price Calculations */
+ IBalancerV2PriceOracle.Variable variablePairPrice = IBalancerV2PriceOracle
+ .Variable
+ .PAIR_PRICE;
+
+ IBalancerV2PriceOracle.OracleAverageQuery[]
+ memory queries = new IBalancerV2PriceOracle.OracleAverageQuery[](3);
+
+ // [Variable enum, seconds, ago]
+ queries[0] = getOracleAverageQuery(variablePairPrice, microWindow, 0);
+ queries[1] = getOracleAverageQuery(variablePairPrice, macroWindow, 0);
+ queries[2] = getOracleAverageQuery(variablePairPrice, macroWindow, macroWindow);
+
+ twaps_ = getTimeWeightedAverage(marketPool, queries);
+ }
+
+ function _fetch() internal view virtual override returns (Oracle.Data memory) {
+ /* Pair Price Calculations */
+ uint256[] memory twaps = getPairPrices();
+ uint256 priceOverMicroWindow = twaps[0];
+ uint256 priceOverMacroWindow = twaps[1];
+ uint256 priceOneMacroWindowAgo = twaps[2];
+
+ /* Reserve Calculations */
+ uint256 reserve = getReserve(priceOverMicroWindow);
+
+ return
+ Oracle.Data({
+ timestamp: block.timestamp,
+ microWindow: microWindow,
+ macroWindow: macroWindow,
+ priceOverMicroWindow: priceOverMicroWindow, // secondsAgos = _microWindow
+ priceOverMacroWindow: priceOverMacroWindow, // secondsAgos = _macroWindow
+ priceOneMacroWindowAgo: priceOneMacroWindowAgo, // secondsAgos = _macroWindow * 2
+ reserveOverMicroWindow: reserve,
+ hasReserve: true // only time false if not using a spot AMM (like for chainlink)
+ });
+ }
+}
diff --git a/contracts/feeds/uniswapv3/OverlayV1UniswapV3Feed.sol b/contracts/feeds/uniswapv3/OverlayV1UniswapV3Feed.sol
index 8c68c0a7..9fad4298 100644
--- a/contracts/feeds/uniswapv3/OverlayV1UniswapV3Feed.sol
+++ b/contracts/feeds/uniswapv3/OverlayV1UniswapV3Feed.sol
@@ -27,8 +27,8 @@ contract OverlayV1UniswapV3Feed is IOverlayV1UniswapV3Feed, OverlayV1Feed {
uint128 public immutable marketBaseAmount;
// ovlXPool tokens
- // @dev X is the common token between marketPool and ovlXPool
address public immutable ovl;
+ // X is the common token between marketPool and ovlXPool
address public immutable x;
constructor(
diff --git a/contracts/interfaces/feeds/balancerv2/IBalancerV2Pool.sol b/contracts/interfaces/feeds/balancerv2/IBalancerV2Pool.sol
new file mode 100644
index 00000000..170eec2c
--- /dev/null
+++ b/contracts/interfaces/feeds/balancerv2/IBalancerV2Pool.sol
@@ -0,0 +1,22 @@
+// 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 solidity 0.8.10;
+pragma experimental ABIEncoderV2;
+
+interface IBalancerV2Pool {
+ function getPoolId() external view returns (bytes32);
+
+ function getNormalizedWeights() external view returns (uint256[] memory);
+}
diff --git a/contracts/interfaces/feeds/balancerv2/IBalancerV2PriceOracle.sol b/contracts/interfaces/feeds/balancerv2/IBalancerV2PriceOracle.sol
new file mode 100644
index 00000000..504958bb
--- /dev/null
+++ b/contracts/interfaces/feeds/balancerv2/IBalancerV2PriceOracle.sol
@@ -0,0 +1,94 @@
+// 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 solidity 0.8.10;
+pragma experimental ABIEncoderV2;
+
+interface IBalancerV2PriceOracle {
+ // 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/contracts/interfaces/feeds/balancerv2/IBalancerV2Vault.sol b/contracts/interfaces/feeds/balancerv2/IBalancerV2Vault.sol
new file mode 100644
index 00000000..f8f13d97
--- /dev/null
+++ b/contracts/interfaces/feeds/balancerv2/IBalancerV2Vault.sol
@@ -0,0 +1,44 @@
+// 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 solidity 0.8.10;
+pragma experimental ABIEncoderV2;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+interface IBalancerV2Vault {
+ /**
+ * @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 (
+ IERC20[] memory tokens,
+ uint256[] memory balances,
+ uint256 lastChangeBlock
+ );
+}
diff --git a/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2Feed.sol b/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2Feed.sol
new file mode 100644
index 00000000..3a5da82e
--- /dev/null
+++ b/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2Feed.sol
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.10;
+
+import "../IOverlayV1Feed.sol";
+import "./IBalancerV2PriceOracle.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+interface IOverlayV1BalancerV2Feed is IOverlayV1Feed {
+ function marketPool() external view returns (address);
+
+ function ovlWethPool() external view returns (address);
+
+ function ovl() external view returns (address);
+
+ function marketToken0() external view returns (address);
+
+ function marketToken1() external view returns (address);
+
+ function ovlWethToken0() external view returns (address);
+
+ function ovlWethToken1() external view returns (address);
+
+ function marketBaseToken() external view returns (address);
+
+ function marketQuoteToken() external view returns (address);
+
+ /// @notice Returns the OracleAverageQuery struct containing information for a TWAP query
+ function getOracleAverageQuery(
+ IBalancerV2PriceOracle.Variable variable,
+ uint256 secs,
+ uint256 ago
+ ) external view returns (IBalancerV2PriceOracle.OracleAverageQuery memory);
+
+ /// @notice Returns the time average weighted price corresponding to each of queries.
+ function getTimeWeightedAverage(
+ address pool,
+ IBalancerV2PriceOracle.OracleAverageQuery[] memory queries
+ ) external view returns (uint256[] memory twaps_);
+
+ /// @notice Returns the TWAI (time weighted average invariant) corresponding to a single query
+ /// @notice for the value of the pool's
+ /// @notice invariant, which is a measure of its liquidity
+ function getTimeWeightedAverageInvariant(
+ address pool,
+ uint256 secs,
+ uint256 ago
+ ) external view returns (uint256 result_);
+
+ /// @notice Returns pool token information given a pool id
+ function getPoolTokens(address vault, bytes32 balancerV2PoolId)
+ external
+ view
+ returns (
+ IERC20[] memory,
+ uint256[] memory,
+ uint256
+ );
+
+ /// @notice Returns the pool id corresponding to the given pool address
+ function getPoolId(address pool) external view returns (bytes32 _poolId);
+
+ /// @notice Returns the normalized weight of the token
+ function getNormalizedWeights(address pool) external view returns (uint256[] memory weights_);
+
+ function getReserve(uint256 priceOverMicroWindow) external view returns (uint256 reserve_);
+
+ /// @notice Virtual balance of WETH in the pool
+ function getReserveInWeth(uint256 twav, uint256 priceOverMicroWindow)
+ external
+ view
+ returns (uint256 reserveInWeth_);
+
+ /// @notice ovlWeth pool only (not reserve)
+ function getPairPriceOvlWeth() external view returns (uint256 twap_);
+
+ /// @notice Market pool only (not reserve)
+ function getPairPrices() external view returns (uint256[] memory twaps_);
+}
diff --git a/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2FeedFactory.sol b/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2FeedFactory.sol
new file mode 100644
index 00000000..8e02bc75
--- /dev/null
+++ b/contracts/interfaces/feeds/balancerv2/IOverlayV1BalancerV2FeedFactory.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.10;
+
+import "../IOverlayV1FeedFactory.sol";
+
+interface IOverlayV1BalancerV2FeedFactory is IOverlayV1FeedFactory {
+ // immutables
+ function ovlWethPool() external view returns (address);
+
+ function ovl() external view returns (address);
+
+ // registry of feeds; for a given (pool, base, quote, amount) pair, returns associated feed
+ function getFeed(
+ address marketPool,
+ address marketBaseToken,
+ address marketQuoteToken,
+ uint128 marketBaseAmount
+ ) external view returns (address feed_);
+
+ /// @dev deploys a new feed contract
+ /// @return feed_ address of the new feed
+ function deployFeed(
+ address vault,
+ address marketPool,
+ address marketBaseToken,
+ address marketQuoteToken,
+ uint128 marketBaseAmount
+ ) external returns (address feed_);
+}
diff --git a/contracts/libraries/Oracle.sol b/contracts/libraries/Oracle.sol
index e8fff6a6..3044a3b3 100644
--- a/contracts/libraries/Oracle.sol
+++ b/contracts/libraries/Oracle.sol
@@ -2,6 +2,8 @@
pragma solidity 0.8.10;
library Oracle {
+ /// @notice Formatted price and liquidity data fetched from the oracle provider
+ /// @dev Every Oracle's data must be formatted to conform to this struct.
struct Data {
uint256 timestamp;
uint256 microWindow;
diff --git a/contracts/libraries/balancer/balancer-v2-monorepo/BalancerV2PoolInfo.sol b/contracts/libraries/balancer/balancer-v2-monorepo/BalancerV2PoolInfo.sol
new file mode 100644
index 00000000..a0e7eb93
--- /dev/null
+++ b/contracts/libraries/balancer/balancer-v2-monorepo/BalancerV2PoolInfo.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.10;
+
+library BalancerV2PoolInfo {
+ struct Pool {
+ address vault;
+ address marketPool;
+ address ovlWethPool;
+ address ovl;
+ address marketBaseToken;
+ address marketQuoteToken;
+ uint128 marketBaseAmount;
+ }
+}
diff --git a/tests/factories/feed/balancerv2/__init__.py b/tests/factories/feed/balancerv2/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/factories/feed/balancerv2/conftest.py b/tests/factories/feed/balancerv2/conftest.py
new file mode 100644
index 00000000..801ab120
--- /dev/null
+++ b/tests/factories/feed/balancerv2/conftest.py
@@ -0,0 +1,165 @@
+import pytest
+from brownie import Contract, OverlayV1BalancerV2Factory, convert
+
+
+@pytest.fixture(scope="module")
+def gov(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for the Governor Role
+ '''
+ yield accounts[0]
+
+
+@pytest.fixture(scope="module")
+def alice(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for Alice the trader
+ '''
+ yield accounts[1]
+
+
+@pytest.fixture(scope="module")
+def bob(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for Bob the trader
+ '''
+ yield accounts[2]
+
+
+@pytest.fixture(scope="module")
+def dai():
+ '''
+ Outputs:
+ [Contract]: DAI token contract instance
+
+ https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F
+ '''
+ yield Contract.from_explorer("0x6B175474E89094C44Da98b954EedeAC495271d0F")
+
+
+@pytest.fixture(scope="module")
+def weth():
+ '''
+ Outputs:
+ [Contract]: WETH token contract instance
+
+ https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
+ '''
+ yield Contract.from_explorer("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
+
+
+@pytest.fixture(scope="module")
+def bal():
+ '''
+ Outputs:
+ [Contract]: BalancerGovernanceToken token contract instance
+
+ https://etherscan.io/address/0xba100000625a3754423978a60c9317c58a424e3D
+ '''
+ yield Contract.from_explorer("0xba100000625a3754423978a60c9317c58a424e3D")
+
+
+@pytest.fixture(scope="module")
+def pool_daiweth():
+ '''
+ Outputs:
+ [Contract]: Balancer V2 WeightedPool2Tokens contract instance for the
+ DAI/WETH pool
+
+
+ https://etherscan.io/address/0x0b09deA16768f0799065C475bE02919503cB2a35
+ https://app.balancer.fi/#/pool/0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a # noqa: E501
+ '''
+ yield Contract.from_explorer("0x0b09deA16768f0799065C475bE02919503cB2a35")
+
+
+@pytest.fixture(scope="module")
+def pool_balweth():
+ '''
+ Outputs:
+ [Contract]: Balancer V2 WeightedPool2Tokens contract instance for the
+ BAL/WETH pool, used as an example OVL/WETH pair.
+
+ https://etherscan.io/address/0x5c6ee304399dbdb9c8ef030ab642b10820db8f56
+ https://app.balancer.fi/#/pool/0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 # noqa: E501
+ '''
+ yield Contract.from_explorer("0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56")
+
+
+@pytest.fixture(scope="module")
+def balweth_poolid():
+ '''
+ Output:
+ [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool contract pool id
+ '''
+ yield convert.to_bytes(
+ '0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a',
+ 'bytes32')
+
+
+@pytest.fixture(scope="module")
+def balv2_tokens(balweth_poolid):
+ '''
+ Returns the BalancerV2Tokens struct fields for a DAI/WETH market token pair
+ using the BAL/WETH token pair to simulate the OVL/WETH token pair for
+ testing. The BalancerV2Tokens struct is an input argument to the
+ constructor of the OverlayV1BalancerV2Feed contract so that token addresses
+ can be retrieved from the BalancerV2 Vault contract.
+
+ Inputs:
+ balweth_poolid [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool
+ contract pool id, representing the market token
+ pair
+ Outputs:
+ (
+ [Contract]: BalancerV2Vault contract instance
+ [bytes32]: BAL/WETH Balancer V2 OracleWeighted contract pool id,
+ representing the OVL/WETH token pair
+ [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool contract pool id,
+ representing the market token pair
+ )
+ '''
+ # BalancerV2Vault contract address is BalancerV2Tokens.vault
+ # https://etherscan.io/address/0xBA12222222228d8Ba445958a75a0704d566BF2C8
+ vault = Contract.from_explorer(
+ '0xBA12222222228d8Ba445958a75a0704d566BF2C8')
+
+ ovlweth_poolid = convert.to_bytes(
+ '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014',
+ 'bytes32')
+ yield (vault, ovlweth_poolid, balweth_poolid)
+
+
+@pytest.fixture(scope="module", params=[(600, 3600)])
+def factory(gov, weth, bal, pool_balweth, request):
+ '''
+ Successfully deploys the OverlayV1BalancerV2Feed contract for a DAI/WETH
+ market pool. The OVL/WETH pool is simulated using the BAL/WETH pool.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ weth [Contract]: WETH token contract instance
+ bal [Contract]: BAL token contract instance representing the OVL
+ token
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ request [dict]: Params specified in fixture
+ [int]: Micro window, 600 seconds
+ [int]: Macro window, 3600 seconds
+ '''
+ micro_window, macro_window = request.param
+ ovl = bal.address
+
+ return gov.deploy(OverlayV1BalancerV2Factory, pool_balweth.address, ovl,
+ micro_window, macro_window)
diff --git a/tests/factories/feed/balancerv2/test_conftest.py b/tests/factories/feed/balancerv2/test_conftest.py
new file mode 100644
index 00000000..52641f3e
--- /dev/null
+++ b/tests/factories/feed/balancerv2/test_conftest.py
@@ -0,0 +1,17 @@
+def test_factory_fixture(factory, weth, bal, pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Factory contract sets the expected
+ variables correctly.
+
+ Inputs:
+ factory [Contract]: OverlayV1BalancerV2Factory contract instance
+ weth [Contract]: WETH token contract instance
+ dai [Contract]: DAI token contract instance
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ assert factory.ovlWethPool() == pool_balweth
+ assert factory.ovl() == bal # BAL acts as ovl for testing
+ assert factory.microWindow() == 600
+ assert factory.macroWindow() == 3600
diff --git a/tests/factories/feed/balancerv2/test_deploy_feed.py b/tests/factories/feed/balancerv2/test_deploy_feed.py
new file mode 100644
index 00000000..a0c37cc0
--- /dev/null
+++ b/tests/factories/feed/balancerv2/test_deploy_feed.py
@@ -0,0 +1,148 @@
+from brownie import interface, reverts
+from collections import OrderedDict
+
+
+def test_deploy_feed_creates_quanto_feed(factory, weth, bal, dai, alice,
+ pool_balweth, pool_daiweth,
+ balv2_tokens):
+ '''
+ Tests that the deployFeed function in the OverlayV1BalancerV2Factory
+ contract deploys a new feed successfully.
+
+ Inputs:
+ factory [Contract]: OverlayV1BalancerV2Factory contract instance
+ weth [Contract]: WETH token contract instance
+ bal [Contract]: BAL token contract instance representing the OVL
+ token
+ dai [Contract]: DAI token contract instance
+ alice [Account]: Brownie provided eth account address for Alice
+ the trader
+ pool_balweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the BAL/WETH pool, representing the
+ OVL/WETH token pair
+ pool_daiweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the DAI/WETH pool
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_balweth
+ ovl = bal
+ market_base_token = weth
+ market_quote_token = dai
+ market_base_amount = 1000000000000000000 # 1e18
+
+ # TODO: check is not feed prior to deploy
+
+ tx = factory.deployFeed(market_pool, market_base_token, market_quote_token,
+ market_base_amount, balv2_tokens, {"from": alice})
+ actual_feed = tx.return_value
+
+ # check feed is added to registry
+ assert factory.getFeed(market_pool, market_base_token, market_quote_token,
+ market_base_amount) == actual_feed
+ assert factory.isFeed(actual_feed) is True
+
+ # check event emitted
+ assert 'FeedDeployed' in tx.events
+ expect_event = OrderedDict({"user": alice.address, "feed": actual_feed})
+ actual_event = tx.events['FeedDeployed']
+ assert actual_event == expect_event
+
+ # check contract deployed with correct constructor params
+ feed_contract = interface.IOverlayV1BalancerV2Feed(actual_feed)
+ assert feed_contract.marketPool() == market_pool
+ assert feed_contract.ovlWethPool() == ovlweth_pool
+ assert feed_contract.ovl() == ovl
+ assert feed_contract.marketBaseToken() == market_base_token
+ assert feed_contract.marketQuoteToken() == market_quote_token
+ assert feed_contract.marketBaseAmount() == market_base_amount
+
+
+def test_deploy_feed_creates_inverse_feed(factory, weth, bal, alice,
+ pool_balweth, balv2_tokens):
+ '''
+ Tests that the deployFeed function in the OverlayV1BalancerV2Factory
+ contract deploys a new inverse feed successfully.
+
+ Inputs:
+ factory [Contract]: OverlayV1BalancerV2Factory contract instance
+ weth [Contract]: WETH token contract instance
+ bal [Contract]: BAL token contract instance representing the OVL
+ token
+ alice [Account]: Brownie provided eth account address for Alice
+ the trader
+ pool_balweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the BAL/WETH pool, representing the
+ OVL/WETH token pair
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ '''
+ market_pool = pool_balweth
+ ovlweth_pool = pool_balweth
+ ovl = bal
+ market_base_token = weth
+ market_quote_token = bal
+ market_base_amount = 1000000000000000000 # 1e18
+
+ # TODO: check is not feed prior to deploy
+
+ balv2_tokens = (balv2_tokens[0], balv2_tokens[1], balv2_tokens[1])
+
+ tx = factory.deployFeed(market_pool, market_base_token, market_quote_token,
+ market_base_amount, balv2_tokens, {"from": alice})
+ actual_feed = tx.return_value
+
+ # check feed is added to registry
+ assert factory.getFeed(market_pool, market_base_token, market_quote_token,
+ market_base_amount) == actual_feed
+ assert factory.isFeed(actual_feed) is True
+
+ # check event emitted
+ assert 'FeedDeployed' in tx.events
+ expect_event = OrderedDict({"user": alice.address, "feed": actual_feed})
+ actual_event = tx.events['FeedDeployed']
+ assert actual_event == expect_event
+
+ # check contract deployed with correct constructor params
+ feed_contract = interface.IOverlayV1BalancerV2Feed(actual_feed)
+ assert feed_contract.marketPool() == market_pool
+ assert feed_contract.ovlWethPool() == ovlweth_pool
+ assert feed_contract.ovl() == ovl
+ assert feed_contract.marketBaseToken() == market_base_token
+ assert feed_contract.marketQuoteToken() == market_quote_token
+ assert feed_contract.marketBaseAmount() == market_base_amount
+
+
+def test_deploy_feed_reverts_when_feed_already_exits(factory, weth, bal, dai,
+ alice, pool_daiweth,
+ balv2_tokens):
+ '''
+ Tests that the deployFeed function in the OverlayV1BalancerV2Factory
+ contract reverts if it tries to deploy an already existing feed.
+
+ Inputs:
+ factory [Contract]: OverlayV1BalancerV2Factory contract instance
+ weth [Contract]: WETH token contract instance
+ bal [Contract]: BAL token contract instance representing the OVL
+ token
+ dai [Contract]: DAI token contract instance
+ alice [Account]: Brownie provided eth account address for Alice
+ the trader
+ pool_daiweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the DAI/WETH pool
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ '''
+ market_pool = pool_daiweth
+ market_base_token = weth
+ market_quote_token = dai
+ market_base_amount = 1000000000000000000 # 1e18
+
+ # Check feed already exists first from prior balt test above
+ feed = factory.getFeed(market_pool, market_base_token, market_quote_token,
+ market_base_amount)
+ assert factory.isFeed(feed) is True
+
+ # check reverts when attempt to deploy again
+ with reverts("OVLV1: feed already exists"):
+ _ = factory.deployFeed(market_pool, market_base_token,
+ market_quote_token, market_base_amount,
+ balv2_tokens, {"from": alice})
diff --git a/tests/factories/feed/univ3/conftest.py b/tests/factories/feed/univ3/conftest.py
index cd61d214..ad203908 100644
--- a/tests/factories/feed/univ3/conftest.py
+++ b/tests/factories/feed/univ3/conftest.py
@@ -19,16 +19,32 @@ def bob(accounts):
@pytest.fixture(scope="module")
def dai():
+ '''
+ Returns the DAI token contract instance used to simulate OVL for testing
+ purposes.
+
+ https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F
+ '''
yield Contract.from_explorer("0x6B175474E89094C44Da98b954EedeAC495271d0F")
@pytest.fixture(scope="module")
def weth():
+ '''
+ Returns the Wrapped Ether (WETH) token contract instance.
+
+ https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
+ '''
yield Contract.from_explorer("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
@pytest.fixture(scope="module")
def uni():
+ '''
+ Returns the Uniswap V3 Uni token contract instance.
+
+ https://etherscan.io/address/0xba100000625a3754423978a60c9317c58a424e3D
+ '''
# to be used as example ovl
yield Contract.from_explorer("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984")
@@ -40,12 +56,24 @@ def uni_factory():
@pytest.fixture(scope="module")
def pool_daiweth_30bps():
+ '''
+ Returns the Uniswap V3 DAI/ETH pool contract instance.
+
+ https://etherscan.io/address/0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
+ https://info.uniswap.org/#/pools/0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8
+ '''
yield Contract.from_explorer("0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8")
@pytest.fixture(scope="module")
def pool_uniweth_30bps():
- # to be used as example ovlweth pool
+ '''
+ Returns the Uniswap V3 UNI/ETH pool contract instance, intended to be used
+ as an example OVL/WETH pool.
+
+ https://etherscan.io/address/0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801
+ https://info.uniswap.org/#/pools/0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801
+ '''
yield Contract.from_explorer("0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801")
diff --git a/tests/feeds/balancerv2/__init__.py b/tests/feeds/balancerv2/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/feeds/balancerv2/conftest.py b/tests/feeds/balancerv2/conftest.py
new file mode 100644
index 00000000..4a5a0df2
--- /dev/null
+++ b/tests/feeds/balancerv2/conftest.py
@@ -0,0 +1,221 @@
+import pytest
+from brownie import Contract, convert, OverlayV1BalancerV2Feed
+
+
+@pytest.fixture(scope="module")
+def gov(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for the Governor Role
+ '''
+ yield accounts[0]
+
+
+@pytest.fixture(scope="module")
+def alice(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for Alice the trader
+ '''
+ yield accounts[1]
+
+
+@pytest.fixture(scope="module")
+def bob(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for Bob the trader
+ '''
+ yield accounts[2]
+
+
+@pytest.fixture(scope="module")
+def rando(accounts):
+ '''
+ Inputs:
+ accounts [Accounts]: List of brownie provided eth account addresses
+ Outputs:
+ [Account]: Brownie provided eth account address for a random account
+ '''
+ yield accounts[3]
+
+
+@pytest.fixture(scope="module")
+def dai():
+ '''
+ Outputs:
+ [Contract]: DAI token contract instance
+
+ https://etherscan.io/address/0x6B175474E89094C44Da98b954EedeAC495271d0F
+ '''
+ yield Contract.from_explorer("0x6B175474E89094C44Da98b954EedeAC495271d0F")
+
+
+@pytest.fixture(scope="module")
+def weth():
+ '''
+ Outputs:
+ [Contract]: WETH token contract instance
+
+ https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
+ '''
+ yield Contract.from_explorer("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
+
+
+@pytest.fixture(scope="module")
+def balancer():
+ '''
+ Outputs:
+ [Contract]: BalancerGovernanceToken token contract instance
+
+ https://etherscan.io/address/0xba100000625a3754423978a60c9317c58a424e3D
+ '''
+ yield Contract.from_explorer("0xba100000625a3754423978a60c9317c58a424e3D")
+
+
+@pytest.fixture(scope="module")
+def pool_daiweth():
+ '''
+ Outputs:
+ [Contract]: Balancer V2 WeightedPool2Tokens contract instance for the
+ DAI/WETH pool
+
+
+ https://etherscan.io/address/0x0b09deA16768f0799065C475bE02919503cB2a35
+ https://app.balancer.fi/#/pool/0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a # noqa: E501
+ '''
+ yield Contract.from_explorer("0x0b09deA16768f0799065C475bE02919503cB2a35")
+
+
+@pytest.fixture(scope="module")
+def pool_balweth():
+ '''
+ Outputs:
+ [Contract]: Balancer V2 WeightedPool2Tokens contract instance for the
+ BAL/WETH pool, representing the OVL/WETH token pair
+
+ https://etherscan.io/address/0x5c6ee304399dbdb9c8ef030ab642b10820db8f56
+ https://app.balancer.fi/#/pool/0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 # noqa: E501
+ '''
+ yield Contract.from_explorer("0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56")
+
+
+@pytest.fixture(scope="module")
+def pool_parusdc():
+ '''
+ Outputs:
+ [Contract] Balancer V2 WeightedPool2Tokens contract instance for the
+ PAR/USDC pool
+
+
+ https://etherscan.io/address/0x5d6e3d7632D6719e04cA162be652164Bec1EaA6b
+ '''
+ yield Contract.from_explorer("0x5d6e3d7632D6719e04cA162be652164Bec1EaA6b")
+
+
+@pytest.fixture(scope="module")
+def balweth_poolid():
+ '''
+ 80% BAL/ 20% WETH Oracle Weighted Pool contract pool id. Used to simulate
+ the OVL/WETH pair.
+ https://app.balancer.fi/#/pool/0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014 # noqa: E501
+
+ Output:
+ [bytes32]: BAL/WETH Balancer V2 OracleWeightedPool contract pool id
+ '''
+ yield convert.to_bytes(
+ '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014',
+ 'bytes32')
+
+
+@pytest.fixture(scope="module")
+def daiweth_poolid():
+ '''
+ 40% DAI/60% WETH Oracle Weighted Pool contract pool id.
+ https://app.balancer.fi/#/pool/0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a # noqa: E501
+
+ Output:
+ [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool contract pool id
+ '''
+ yield convert.to_bytes(
+ '0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a',
+ 'bytes32')
+
+
+@pytest.fixture(scope="module")
+def vault():
+ '''
+ Outputs:
+ [Contract]: BalancerV2Vault contract instance
+
+ https://etherscan.io/address/0xBA12222222228d8Ba445958a75a0704d566BF2C8
+ '''
+ return Contract.from_explorer('0xBA12222222228d8Ba445958a75a0704d566BF2C8')
+
+
+@pytest.fixture(scope="module")
+def balv2_pool(vault, balancer, weth, dai, pool_daiweth, pool_balweth):
+ '''
+ Returns the BalancerV2PoolInfo struct fields for a DAI/WETH market token
+ pair using the BAL/WETH token pair to simulate the OVL/WETH token pair for
+ testing. The BalancerV2PoolInfo struct is an input argument to the
+ constructor of the OverlayV1BalancerV2Feed contract that passes in the
+ relevant market and OvlWeth data.
+
+ Inputs:
+ vault [Contract]: BalancerV2Vault contract instance
+ balancer [Contract]: BalancerGovernanceToken token contract
+ instance
+ weth [Contract]: WETH token contract instance
+ dai [Contract]: DAI token contract instance
+ pool_balweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the BAL/WETH pool, representing
+ the OVL/WETH token pair
+ pool_balweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the DAI/WETH pool
+ Outputs:
+ (
+ vault [Contract]: BalancerV2Vault contract instance
+ marketPool [Contract]: Market pool (DAI/WETH) contract instance
+ ovlWethPool [Contract]: OvlWeth pool (BAL/WETH) contract instance
+ ovl [Contract]: OVL (BAL) token contract instance
+ marketBaseToken [Contract]: Market base token (WETH) contract
+ instance
+ marketQuoteToken [Contract]: Market quote token (DAI) contract
+ instance
+ marketBaseAmount [uint128]: Market base amount
+ )
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_balweth
+ ovl = balancer
+ market_base_token = weth
+ market_quote_token = dai
+ market_base_amount = 1 * 10 ** weth.decimals()
+
+ return (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+
+@pytest.fixture(scope="module")
+def feed(gov, balv2_pool):
+ '''
+ Successfully deploys the OverlayV1BalancerV2Feed contract for a DAI/WETH
+ market pool. The OVL/WETH pool is simulated using the BAL/WETH pool.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ balv2_pool (BalancerV2PoolInfo): BalancerV2PoolInfo struct
+ '''
+ micro_window = 600
+ macro_window = 3600
+
+ yield gov.deploy(OverlayV1BalancerV2Feed, balv2_pool, micro_window,
+ macro_window)
diff --git a/tests/feeds/balancerv2/test_deploy.py b/tests/feeds/balancerv2/test_deploy.py
new file mode 100644
index 00000000..99c5208e
--- /dev/null
+++ b/tests/feeds/balancerv2/test_deploy.py
@@ -0,0 +1,252 @@
+import pytest
+from brownie import Contract, reverts, OverlayV1BalancerV2Feed
+
+
+@pytest.fixture
+def usdc():
+ '''
+ Returns the USDC token contract instance.
+
+ https://etherscan.io/address/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
+ '''
+ yield Contract.from_explorer("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
+
+
+@pytest.fixture
+def par():
+ '''
+ Returns the PAR token contract instance used for testing the require
+ statements of the OverlayV1BalancerV2Feed contract constructor function.
+
+ https://etherscan.io/address/0x68037790A0229e9Ce6EaA8A99ea92964106C4703
+ '''
+ yield Contract.from_explorer("0x68037790A0229e9Ce6EaA8A99ea92964106C4703")
+
+
+@pytest.fixture
+def pool_usdcbal():
+ '''
+ Returns the Balancer V2 WeightedPool2Tokens USDC/BAL contract instance.
+
+ https://app.balancer.fi/#/pool/0x9c08c7a7a89cfd671c79eacdc6f07c1996277ed5000200000000000000000025 # noqa: E501
+ '''
+ yield Contract.from_explorer("0x9c08C7a7a89cfD671c79eacdc6F07c1996277eD5")
+
+
+def test_deploy_feed_reverts_on_market_token_not_weth(gov, balancer, vault,
+ par, usdc, pool_parusdc,
+ pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract deploy function reverts
+ when the market token pair does NOT include WETH. To generate the expected
+ failure, the market token pair must not contain WETH. The PAR/USDC pool is
+ used to simulate this failing market token pair.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ balancer [Contract]: BAL token contract instance representing the
+ OVL token
+ usdc [Contract]: USDC token contract instance representing the
+ market quote token
+ par [Contract]: PAR token contract instance representing the
+ market base token
+ pool_parusdc [Contract]: PAR/USDC Balancer V2 WeightedPool2Tokens
+ contract instance representing the market
+ token pair
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ # Market token pair does NOT contain WETH which causes the constructor to
+ # revert
+ market_pool = pool_parusdc
+ ovlweth_pool = pool_balweth
+ ovl = balancer
+ market_base_token = par
+ market_quote_token = usdc
+ market_base_amount = 1000000000000000000
+
+ balv2_pool = (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+ micro_window = 600
+ macro_window = 3600
+
+ with reverts("OVLV1Feed: marketToken != WETH"):
+ gov.deploy(OverlayV1BalancerV2Feed, balv2_pool,
+ micro_window, macro_window)
+
+
+def test_deploy_feed_reverts_on_market_token_not_base(gov, vault, weth, rando,
+ balancer, pool_daiweth,
+ pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract deploy function reverts
+ when the market token pair does NOT include the market base token.
+
+ To generate the expected failure, the DAI/WETH market token pair must not
+ contain the market base token. The market base token is defined as a random
+ address instead of WETH.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ weth [Contract]: WETH token contract instance
+ rando [Account]: Random eth address representing the market base
+ token causing the expected failure
+ balancer [Contract]: BAL token contract instance representing the OVL
+ token
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ pool_daiweth [Contract]: DAI/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the market token
+ pair
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_balweth
+ ovl = balancer
+ market_base_token = rando
+ market_quote_token = weth
+ market_base_amount = 1000000
+
+ balv2_pool = (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+ micro_window = 600
+ macro_window = 3600
+
+ with reverts("OVLV1Feed: marketToken != marketBaseToken"):
+ gov.deploy(OverlayV1BalancerV2Feed, balv2_pool,
+ micro_window, macro_window)
+
+
+def test_deploy_feed_reverts_on_market_token_not_quote(gov, rando, dai, vault,
+ balancer, pool_daiweth,
+ pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract deploy function reverts
+ when the market token pair does NOT include the market quote token.
+
+ To generate the expected failure, the DAI/WETH market token pair must not
+ contain the market quote token. The market quote token is defined as a
+ random address instead of WETH.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ rando [Account]: Random eth address representing the market quote
+ token causing the expected failure
+ balancer [Contract]: BAL token contract instance representing the OVL
+ token
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ pool_daiweth [Contract]: DAI/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the market token
+ pair
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_balweth
+ ovl = balancer
+ market_base_token = dai
+ market_quote_token = rando
+ market_base_amount = 1000000
+
+ balv2_pool = (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+ micro_window = 600
+ macro_window = 3600
+
+ with reverts("OVLV1Feed: marketToken != marketQuoteToken"):
+ gov.deploy(OverlayV1BalancerV2Feed, balv2_pool,
+ micro_window, macro_window)
+
+
+def test_deploy_feed_reverts_on_weth_not_in_ovlweth_pool(gov, weth, vault, dai,
+ par, pool_daiweth,
+ pool_parusdc):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract deploy function reverts
+ when the OVL/WETH token pair does NOT include WETH.
+
+ To generate the expected failure, the OVL/WETH token pair must not contain
+ the WETH token and the PAR/USDC is used instead.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ weth [Contract]: WETH token contract instance
+ dai [Contract]: DAI token contract instance
+ par [Contract]: PAR token contract instance representing the OVL
+ token
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ pool_daiweth [Contract]: DAI/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the market token
+ pair
+ pool_parusdc [Contract]: PAR/USDC Balancer V2 WeightedPool2Tokens
+ contract instance representing the market token
+ pair
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_parusdc
+ ovl = par
+ market_base_token = dai
+ market_quote_token = weth
+ market_base_amount = 1000000
+
+ balv2_pool = (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+ micro_window = 600
+ macro_window = 3600
+
+ with reverts("OVLV1Feed: ovlWethToken != WETH"):
+ gov.deploy(OverlayV1BalancerV2Feed, balv2_pool,
+ micro_window, macro_window)
+
+
+def test_deploy_feed_reverts_on_ovl_not_in_ovlweth_pool(gov, weth, vault, dai,
+ pool_daiweth,
+ pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract deploy function reverts
+ when the OVL/WETH token pair does NOT include OVL.
+
+ The OVL/WETH token pair is represented by BAL/WETH for testing purposes,
+ therefore, to generate the expected failure, the BAL/WETH token pair must
+ not contain the BAL token.
+
+ Inputs:
+ gov [Account]: Governor role account deploys the
+ OverlayV1BalancerV2Feed contract
+ weth [Contract]: WETH token contract instance
+ dai [Contract]: DAI token contract instance
+ balv2_tokens [tuple]: BalancerV2Tokens struct field variables
+ pool_daiweth [Contract]: DAI/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the market
+ token pair
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ market_pool = pool_daiweth
+ ovlweth_pool = pool_balweth
+ ovl = dai
+ market_base_token = dai
+ market_quote_token = weth
+ market_base_amount = 1000000
+
+ balv2_pool = (vault, market_pool, ovlweth_pool, ovl, market_base_token,
+ market_quote_token, market_base_amount)
+
+ micro_window = 600
+ macro_window = 3600
+
+ with reverts("OVLV1Feed: ovlWethToken != OVL"):
+ gov.deploy(OverlayV1BalancerV2Feed, balv2_pool,
+ micro_window, macro_window)
diff --git a/tests/feeds/balancerv2/test_latest.py b/tests/feeds/balancerv2/test_latest.py
new file mode 100644
index 00000000..157f3d7e
--- /dev/null
+++ b/tests/feeds/balancerv2/test_latest.py
@@ -0,0 +1,27 @@
+from brownie import chain
+
+
+def test_latest_updates_data_on_first_call_for_quanto_feed(pool_daiweth,
+ pool_balweth,
+ feed):
+ macro_window = feed.macroWindow()
+ micro_window = feed.microWindow()
+ timestamp = chain[-1]['timestamp']
+ pool_market = feed.marketPool()
+
+ variable = 0 # PAIR_PRICE
+ ago = 0
+ query = feed.getOracleAverageQuery(variable, micro_window, ago)
+ price_over_micro_window = feed.getTimeWeightedAverage(pool_market,
+ [query])[0]
+
+ prices = feed.getPairPrices()
+ reserve = feed.getReserve(price_over_micro_window)
+ has_reserve = True
+
+ expect = (timestamp, micro_window, macro_window, prices[0], prices[1],
+ prices[2], reserve, has_reserve)
+
+ actual = feed.latest()
+
+ assert expect == actual
diff --git a/tests/feeds/balancerv2/test_views.py b/tests/feeds/balancerv2/test_views.py
new file mode 100644
index 00000000..4342b16d
--- /dev/null
+++ b/tests/feeds/balancerv2/test_views.py
@@ -0,0 +1,226 @@
+from brownie.test import given, strategy
+
+
+def test_get_reserve_in_weth(feed, pool_daiweth):
+ pool_market = pool_daiweth
+ variable = 0 # PAIR_PRICE
+ micro_window = 600
+ ago = 0
+
+ query = feed.getOracleAverageQuery(variable, micro_window, ago)
+ price_over_micro_window = feed.getTimeWeightedAverage(pool_market,
+ [query])[0]
+ twav = feed.getTimeWeightedAverageInvariant(pool_market, micro_window, ago)
+
+ actual = feed.getReserveInWeth(twav, price_over_micro_window) # noqa: F841
+ assert 1 == 1
+
+ # SN TODO: error on denominator: OverflowError: (34, 'Result too large')
+ # normalized_weights = feed.getNormalizedWeights(pool_market)
+ # weight_token_0 = normalized_weights[0]
+ # weight_token_1 = normalized_weights[1]
+ #
+ # denominator = ((price_over_micro_window * weight_token_1) / weight_token_0) ** weight_token_1; # noqa: E501
+ # power = 1 / (weight_token_0 + weight_token_1)
+ # expect = (twav / denominator) ** power;
+
+ # assert actual == expect
+
+
+# SN TODO
+def test_get_pair_price_ovl_weth(feed):
+ res = feed.getPairPriceOvlWeth() # noqa: F841
+
+ assert 1 == 1
+
+
+# SN TODO
+def test_get_reserve(feed, pool_daiweth):
+ micro_window = 600
+ ago = 0
+ variable = 0
+ query = feed.getOracleAverageQuery(variable, micro_window, ago)
+ price_over_micro_window = feed.getTimeWeightedAverage(pool_daiweth,
+ [query])
+ price_over_micro_window = price_over_micro_window[0]
+
+ res = feed.getReserve(price_over_micro_window) # noqa: F841
+
+ assert 1 == 1
+
+
+def test_get_time_weighted_average_pair_price(feed):
+ res = feed.getPairPrices()
+ print()
+ print('getPairPrices RES', res)
+ print()
+ assert (1 == 1)
+
+
+def test_get_time_weighted_average_invariant(feed, pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract
+ getTimeWeightedAverageInvariant function returns the expected TWAP.
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ secs = 1800
+ ago = 0
+
+ variable = 2 # Variable.INVARIANT
+ query = feed.getOracleAverageQuery(variable, secs, ago)
+ expect = feed.getTimeWeightedAverage(pool_balweth, [query]) # noqa: F841
+ expect = expect[0]
+
+ actual = feed.getTimeWeightedAverageInvariant(pool_balweth, secs, ago)
+ assert (actual == expect)
+
+
+def test_get_time_weighted_average(feed, pool_balweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract getTimeWeightedAverage
+ function returns the expected TWAP.
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ '''
+ secs = 1800
+ ago = 0
+ variable = 0 # Variable.PAIR_PRICE
+ query = feed.getOracleAverageQuery(variable, secs, ago)
+ actual = feed.getTimeWeightedAverage(pool_balweth, [query]) # noqa: F841
+ print('ACTUAL GET TWAP', actual)
+ # SN TODO
+ assert 1 == 1
+
+
+@given(secs=strategy('int', min_value=0, max_value=3600))
+def test_get_oracle_average_query(feed, secs):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract getOracleAverageQuery
+ function returns the OracleAverageQuery struct.
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ '''
+ ago = [0, 1800]
+
+ variable = 0 # Variable.PAIR_PRICE
+ actual = feed.getOracleAverageQuery(variable, secs, ago[0])
+ expect = (variable, secs, ago[0])
+ assert expect == actual
+
+ variable = 2 # Variable.INVARIANT
+ actual = feed.getOracleAverageQuery(variable, secs, ago[0])
+ expect = (variable, secs, ago[0])
+ assert expect == actual
+
+ variable = 0 # Variable.PAIR_PRICE
+ actual = feed.getOracleAverageQuery(variable, secs, ago[1])
+ expect = (variable, secs, ago[1])
+ assert expect == actual
+
+ variable = 2 # Variable.INVARIANT
+ actual = feed.getOracleAverageQuery(variable, secs, ago[1])
+ expect = (variable, secs, ago[1])
+ assert expect == actual
+
+
+def test_get_normalized_weights(feed, pool_balweth, pool_daiweth):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract getNormalizedWeights
+ function returns the normalized weights of the pool tokens.
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ pool_daiweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the DAI/WETH pool
+ '''
+ # OVL/WETH (represented by BAL/WETH)
+ actual = feed.getNormalizedWeights(pool_balweth)
+ expect = (800000000000000000, 200000000000000000)
+ assert expect == actual
+
+ # DAI/WETH (the market pool)
+ actual = feed.getNormalizedWeights(pool_daiweth)
+ expect = (400000000000000000, 600000000000000000)
+ assert expect == actual
+
+
+def test_get_pool_tokens(feed, vault, weth, balancer, dai, balweth_poolid,
+ daiweth_poolid):
+ '''
+ Tests that the OverlayV1BalancerV2Feed contract getPoolTokens function
+ returns the expected token pair when given the associated pool id.
+
+ Two pool ids are tested:
+ 1. The OVL/WETH pool id which is simulated by BAL/WETH for testing
+ 2. The DAI/WETH pool id which often represents the market token pair for
+ testing
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ vault [Contract]: BalancerV2Vault contract instance
+ weth [Contract]: WETH token contract instance
+ balancer [Contract]: BalancerGovernanceToken token contract
+ instance
+ dai [Contract]: DAI token contract instance
+ balweth_poolid [bytes32]: BAL/WETH Balancer V2 OracleWeightedPool
+ contract pool id
+ daiweth_poolid [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool
+ contract pool id
+ '''
+ # OVL/WETH (represented by BAL/WETH)
+ actual = feed.getPoolTokens(vault, balweth_poolid.hex())
+ actual_tokens = actual[0]
+ expect_tokens = (balancer, weth)
+
+ # DAI/WETH (the market pool)
+ actual = feed.getPoolTokens(vault, daiweth_poolid.hex())
+ actual_tokens = actual[0]
+ expect_tokens = (dai, weth)
+ assert expect_tokens == actual_tokens
+
+
+def test_get_pool_id(feed, pool_balweth, pool_daiweth, balweth_poolid,
+ daiweth_poolid):
+ '''
+ Test when given a valid pool address, the getPoolId function in
+ OverlayV1BalancerV2Feed contract returns the expected pool id.
+
+ Two pool ids are tested:
+ 1. The OVL/WETH pool id which is often simulated by BAL/WETH for testing
+ 2. The DAI/WETH pool id which often represents the market token pair for
+ testing
+
+ Inputs:
+ feed [Contract]: OverlayV1BalancerV2Feed contract instance
+ pool_balweth [Contract]: BAL/WETH Balancer V2 WeightedPool2Tokens
+ contract instance representing the OVL/WETH
+ token pair
+ pool_daiweth [Contract]: Balancer V2 WeightedPool2Tokens contract
+ instance for the DAI/WETH pool
+ balweth_poolid [bytes32]: BAL/WETH Balancer V2 OracleWeightedPool
+ contract pool id
+ daiweth_poolid [bytes32]: DAI/WETH Balancer V2 OracleWeightedPool
+ contract pool id
+ '''
+ # OVL/WETH (represented by BAL/WETH)
+ expect = balweth_poolid.hex()
+ actual = feed.getPoolId(pool_balweth).hex()
+ assert expect == actual
+
+ # DAI/WETH (the market pool)
+ expect = daiweth_poolid.hex()
+ actual = feed.getPoolId(pool_daiweth).hex()
+ assert expect == actual
diff --git a/tests/feeds/uniswapv3/test_deploy.py b/tests/feeds/uniswapv3/test_deploy.py
index e3927042..23b90904 100644
--- a/tests/feeds/uniswapv3/test_deploy.py
+++ b/tests/feeds/uniswapv3/test_deploy.py
@@ -4,11 +4,22 @@
@pytest.fixture
def usdc():
+ '''
+ Returns the USDC token contract instance.
+
+ https://etherscan.io/address/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
+ '''
yield Contract.from_explorer("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
@pytest.fixture
def pool_daiusdc_5bps():
+ '''
+ Returns the DAI token contract instance used to simulate OVL for testing
+ purposes.
+
+ https://etherscan.io/address/0x6c6Bc977E13Df9b0de53b251522280BB72383700
+ '''
yield Contract.from_explorer("0x6c6Bc977E13Df9b0de53b251522280BB72383700")
diff --git a/tests/markets/conftest.py b/tests/markets/conftest.py
index a850693e..fd74c29e 100644
--- a/tests/markets/conftest.py
+++ b/tests/markets/conftest.py
@@ -146,8 +146,6 @@ def feed_factory(create_feed_factory):
yield create_feed_factory()
-# TODO: params for different OverlayV1Feed types ... (to test BalancerV2
-# and UniswapV3 in same test run)
@pytest.fixture(scope="module")
def create_feed(gov, feed_factory, pool_daiweth_30bps, pool_uniweth_30bps,
uni, dai, weth, request):
diff --git a/tests/markets/test_update.py b/tests/markets/test_update.py
index 88b1dda5..a63de198 100644
--- a/tests/markets/test_update.py
+++ b/tests/markets/test_update.py
@@ -5,6 +5,8 @@
from .utils import RiskParameter
+# SN TODO: copy this w balancerv2 feed to get latest gas estimates
+#
def test_update_fetches_from_feed(market, feed, rando):
# call update
tx = market.update({"from": rando})