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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/interfaces/IBalancer2TokensPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.13;

import "../interfaces/IBalancerTwapOracle.sol";

interface IBalancer2TokensPool is IBalancerTwapOracle {
function getNormalizedWeights() external view returns (uint256[] memory);
}
9 changes: 9 additions & 0 deletions src/interfaces/IBalancerTwapOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

pragma solidity ^0.8.0;

import {IVault} from "../interfaces/IBalancerVault.sol";

/**
* @dev Interface for querying historical data from a Pool that can be used as a Price Oracle.
*
Expand All @@ -25,6 +27,13 @@ pragma solidity ^0.8.0;
* is not older than the largest safe query window.
*/
interface IBalancerTwapOracle {
/**
* @notice Returns the Balancer Vault
*/
function getVault() external view returns (IVault);

function getPoolId() external view returns (bytes32);

// The three values that can be queried:
//
// - PAIR_PRICE: the price of the tokens in the Pool, expressed as the price of the second token in units of the
Expand Down
177 changes: 177 additions & 0 deletions src/interfaces/IBalancerVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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 <http://www.gnu.org/licenses/>.


pragma experimental ABIEncoderV2;

pragma solidity >=0.7.0 <0.9.0;

/**
* @dev This is an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero
* address sentinel value). We're just relying on the fact that `interface` can be used to declare new address-like
* types.
*
* This concept is unrelated to a Pool's Asset Managers.
*/
interface IAsset {
// solhint-disable-previous-line no-empty-blocks
}

/**
* @dev Minimal interface for interacting with Balancer's vault.
*/
interface IVault {

/**
* @dev Called by users to join a Pool, which transfers tokens from `sender` into the Pool's balance. This will
* trigger custom Pool behavior, which will typically grant something in return to `recipient` - often tokenized
* Pool shares.
*
* If the caller is not `sender`, it must be an authorized relayer for them.
*
* The `assets` and `maxAmountsIn` arrays must have the same length, and each entry indicates the maximum amount
* to send for each asset. The amounts to send are decided by the Pool and not the Vault: it just enforces
* these maximums.
*
* If joining a Pool that holds WETH, it is possible to send ETH directly: the Vault will do the wrapping. To enable
* this mechanism, the IAsset sentinel value (the zero address) must be passed in the `assets` array instead of the
* WETH address. Note that it is not possible to combine ETH and WETH in the same join. Any excess ETH will be sent
* back to the caller (not the sender, which is important for relayers).
*
* `assets` must have the same length and order as the array returned by `getPoolTokens`. This prevents issues when
* interacting with Pools that register and deregister tokens frequently. If sending ETH however, the array must be
* sorted *before* replacing the WETH address with the ETH sentinel value (the zero address), which means the final
* `assets` array might not be sorted. Pools with no registered tokens cannot be joined.
*
* If `fromInternalBalance` is true, the caller's Internal Balance will be preferred: ERC20 transfers will only
* be made for the difference between the requested amount and Internal Balance (if any). Note that ETH cannot be
* withdrawn from Internal Balance: attempting to do so will trigger a revert.
*
* This causes the Vault to call the `IBasePool.onJoinPool` hook on the Pool's contract, where Pools implement
* their own custom logic. This typically requires additional information from the user (such as the expected number
* of Pool shares). This can be encoded in the `userData` argument, which is ignored by the Vault and passed
* directly to the Pool's contract, as is `recipient`.
*
* Emits a `PoolBalanceChanged` event.
*/
function joinPool(
bytes32 poolId,
address sender,
address recipient,
JoinPoolRequest memory request
) external payable;

struct JoinPoolRequest {
IAsset[] assets;
uint256[] maxAmountsIn;
bytes userData;
bool fromInternalBalance;
}

enum PoolSpecialization { GENERAL, MINIMAL_SWAP_INFO, TWO_TOKEN }

/**
* @dev Returns a Pool's contract address and specialization setting.
*/
function getPool(bytes32 poolId) external view returns (address, PoolSpecialization);

/**
* @dev Returns a Pool's registered tokens, the total balance for each, and the latest block when *any* of
* the tokens' `balances` changed.
*
* The order of the `tokens` array is the same order that will be used in `joinPool`, `exitPool`, as well as in all
* Pool hooks (where applicable). Calls to `registerTokens` and `deregisterTokens` may change this order.
*
* If a Pool only registers tokens once, and these are sorted in ascending order, they will be stored in the same
* order as passed to `registerTokens`.
*
* Total balances include both tokens held by the Vault and those withdrawn by the Pool's Asset Managers. These are
* the amounts used by joins, exits and swaps. For a detailed breakdown of token balances, use `getPoolTokenInfo`
* instead.
*/
function getPoolTokens(bytes32 poolId)
external
view
returns (
address[] memory tokens,
uint256[] memory,
uint256
);


/**
* @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the
* `recipient` account.
*
* If the caller is not `sender`, it must be an authorized relayer for them.
*
* If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20
* transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender`
* must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of
* `joinPool`.
*
* If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of
* transferred. This matches the behavior of `exitPool`.
*
* Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a
* revert.
*/
struct FundManagement {
address sender;
bool fromInternalBalance;
address payable recipient;
bool toInternalBalance;
}

enum SwapKind { GIVEN_IN, GIVEN_OUT }

/**
* @dev Performs a swap with a single Pool.
*
* If the swap is 'given in' (the number of tokens to send to the Pool is known), it returns the amount of tokens
* taken from the Pool, which must be greater than or equal to `limit`.
*
* If the swap is 'given out' (the number of tokens to take from the Pool is known), it returns the amount of tokens
* sent to the Pool, which must be less than or equal to `limit`.
*
* Internal Balance usage and the recipient are determined by the `funds` struct.
*
* Emits a `Swap` event.
*/
function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
) external payable returns (uint256);

/**
* @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on
* the `kind` value.
*
* `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address).
* Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault.
*
* The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be
* used to extend swap behavior.
*/
struct SingleSwap {
bytes32 poolId;
SwapKind kind;
IAsset assetIn;
IAsset assetOut;
uint256 amount;
bytes userData;
}
}
34 changes: 26 additions & 8 deletions src/oracles/BalancerOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import {Owned} from "solmate/auth/Owned.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";

import {IOracle} from "../interfaces/IOracle.sol";
import {IVault} from "../interfaces/IBalancerVault.sol";
import {IBalancerTwapOracle} from "../interfaces/IBalancerTwapOracle.sol";

/// @title Oracle using Balancer TWAP oracle as data source
/// @author zefram.eth
/// @notice The oracle contract that provides the current price to purchase
/// the underlying token while exercising options. Uses Balancer TWAP oracle
/// as data source, and then applies a multiplier & lower bound.
/// @dev IMPORTANT: The Balancer pool must use the payment token of the options
/// token as the first token and the underlying token as the second token, due to
/// how the Balancer oracle represents the price.
/// Furthermore, the payment token and the underlying token must use 18 decimals.
/// @dev IMPORTANT: The payment token and the underlying token must use 18 decimals.
/// This is because the Balancer oracle returns the TWAP value in 18 decimals
/// and the OptionsToken contract also expects 18 decimals.
contract BalancerOracle is IOracle, Owned {
Expand All @@ -30,6 +28,7 @@ contract BalancerOracle is IOracle, Owned {
/// -----------------------------------------------------------------------

error BalancerOracle__TWAPOracleNotReady();
error BalancerOracle__BelowMinPrice();

/// -----------------------------------------------------------------------
/// Events
Expand Down Expand Up @@ -71,19 +70,29 @@ contract BalancerOracle is IOracle, Owned {
/// price to mitigate potential attacks on the TWAP oracle.
uint128 public minPrice;

/// @notice Whether the price should be returned in terms of token0.
/// If false, the price is returned in terms of token1.
bool public isToken0;

/// -----------------------------------------------------------------------
/// Constructor
/// -----------------------------------------------------------------------

constructor(
IBalancerTwapOracle balancerTwapOracle_,
address token,
address owner_,
uint16 multiplier_,
uint56 secs_,
uint56 ago_,
uint128 minPrice_
) Owned(owner_) {
balancerTwapOracle = balancerTwapOracle_;

IVault vault = balancerTwapOracle.getVault();
(address[] memory poolTokens,,) = vault.getPoolTokens(balancerTwapOracle_.getPoolId());
isToken0 = poolTokens[0] == token;

multiplier = multiplier_;
secs = secs_;
ago = ago_;
Expand Down Expand Up @@ -132,11 +141,16 @@ contract BalancerOracle is IOracle, Owned {
price = balancerTwapOracle.getTimeWeightedAverage(queries)[0];
}

if (isToken0) {
// convert price to token0
price = uint256(1e18).divWadDown(price);
}

// apply min price
if (price < minPrice_) revert BalancerOracle__BelowMinPrice();

// apply multiplier to price
price = price.mulDivUp(multiplier_, MULTIPLIER_DENOM);

// bound price above minPrice
price = price < minPrice_ ? minPrice_ : price;
}

/// -----------------------------------------------------------------------
Expand All @@ -151,7 +165,11 @@ contract BalancerOracle is IOracle, Owned {
/// would be (block.timestamp - secs - ago, block.timestamp - ago].
/// @param minPrice_ The minimum value returned by getPrice(). Maintains a floor for the
/// price to mitigate potential attacks on the TWAP oracle.
function setParams(uint16 multiplier_, uint56 secs_, uint56 ago_, uint128 minPrice_) external onlyOwner {
function setParams(address token, uint16 multiplier_, uint56 secs_, uint56 ago_, uint128 minPrice_) external onlyOwner {
IVault vault = balancerTwapOracle.getVault();
(address[] memory poolTokens,,) = vault.getPoolTokens(balancerTwapOracle.getPoolId());
isToken0 = poolTokens[0] == token;

multiplier = multiplier_;
secs = secs_;
ago = ago_;
Expand Down
Loading