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})