diff --git a/src/interfaces/IBalancer2TokensPool.sol b/src/interfaces/IBalancer2TokensPool.sol new file mode 100644 index 0000000..0f8b086 --- /dev/null +++ b/src/interfaces/IBalancer2TokensPool.sol @@ -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); +} diff --git a/src/interfaces/IBalancerTwapOracle.sol b/src/interfaces/IBalancerTwapOracle.sol index dec77ba..5044d79 100644 --- a/src/interfaces/IBalancerTwapOracle.sol +++ b/src/interfaces/IBalancerTwapOracle.sol @@ -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. * @@ -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 diff --git a/src/interfaces/IBalancerVault.sol b/src/interfaces/IBalancerVault.sol new file mode 100644 index 0000000..2628237 --- /dev/null +++ b/src/interfaces/IBalancerVault.sol @@ -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 . + + +pragma experimental ABIEncoderV2; + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @dev This is an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero + * address sentinel value). We're just relying on the fact that `interface` can be used to declare new address-like + * types. + * + * This concept is unrelated to a Pool's Asset Managers. + */ +interface IAsset { + // solhint-disable-previous-line no-empty-blocks +} + +/** + * @dev Minimal interface for interacting with Balancer's vault. + */ +interface IVault { + +/** + * @dev Called by users to join a Pool, which transfers tokens from `sender` into the Pool's balance. This will + * trigger custom Pool behavior, which will typically grant something in return to `recipient` - often tokenized + * Pool shares. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * The `assets` and `maxAmountsIn` arrays must have the same length, and each entry indicates the maximum amount + * to send for each asset. The amounts to send are decided by the Pool and not the Vault: it just enforces + * these maximums. + * + * If joining a Pool that holds WETH, it is possible to send ETH directly: the Vault will do the wrapping. To enable + * this mechanism, the IAsset sentinel value (the zero address) must be passed in the `assets` array instead of the + * WETH address. Note that it is not possible to combine ETH and WETH in the same join. Any excess ETH will be sent + * back to the caller (not the sender, which is important for relayers). + * + * `assets` must have the same length and order as the array returned by `getPoolTokens`. This prevents issues when + * interacting with Pools that register and deregister tokens frequently. If sending ETH however, the array must be + * sorted *before* replacing the WETH address with the ETH sentinel value (the zero address), which means the final + * `assets` array might not be sorted. Pools with no registered tokens cannot be joined. + * + * If `fromInternalBalance` is true, the caller's Internal Balance will be preferred: ERC20 transfers will only + * be made for the difference between the requested amount and Internal Balance (if any). Note that ETH cannot be + * withdrawn from Internal Balance: attempting to do so will trigger a revert. + * + * This causes the Vault to call the `IBasePool.onJoinPool` hook on the Pool's contract, where Pools implement + * their own custom logic. This typically requires additional information from the user (such as the expected number + * of Pool shares). This can be encoded in the `userData` argument, which is ignored by the Vault and passed + * directly to the Pool's contract, as is `recipient`. + * + * Emits a `PoolBalanceChanged` event. + */ + function joinPool( + bytes32 poolId, + address sender, + address recipient, + JoinPoolRequest memory request + ) external payable; + + struct JoinPoolRequest { + IAsset[] assets; + uint256[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; + } + + enum PoolSpecialization { GENERAL, MINIMAL_SWAP_INFO, TWO_TOKEN } + + /** + * @dev Returns a Pool's contract address and specialization setting. + */ + function getPool(bytes32 poolId) external view returns (address, PoolSpecialization); + + /** + * @dev Returns a Pool's registered tokens, the total balance for each, and the latest block when *any* of + * the tokens' `balances` changed. + * + * The order of the `tokens` array is the same order that will be used in `joinPool`, `exitPool`, as well as in all + * Pool hooks (where applicable). Calls to `registerTokens` and `deregisterTokens` may change this order. + * + * If a Pool only registers tokens once, and these are sorted in ascending order, they will be stored in the same + * order as passed to `registerTokens`. + * + * Total balances include both tokens held by the Vault and those withdrawn by the Pool's Asset Managers. These are + * the amounts used by joins, exits and swaps. For a detailed breakdown of token balances, use `getPoolTokenInfo` + * instead. + */ + function getPoolTokens(bytes32 poolId) + external + view + returns ( + address[] memory tokens, + uint256[] memory, + uint256 + ); + + + /** + * @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the + * `recipient` account. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20 + * transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender` + * must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of + * `joinPool`. + * + * If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of + * transferred. This matches the behavior of `exitPool`. + * + * Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a + * revert. + */ + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + enum SwapKind { GIVEN_IN, GIVEN_OUT } + + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is 'given in' (the number of tokens to send to the Pool is known), it returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is 'given out' (the number of tokens to take from the Pool is known), it returns the amount of tokens + * sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` struct. + * + * Emits a `Swap` event. + */ + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); + + /** + * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on + * the `kind` value. + * + * `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address). + * Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IAsset assetIn; + IAsset assetOut; + uint256 amount; + bytes userData; + } +} diff --git a/src/oracles/BalancerOracle.sol b/src/oracles/BalancerOracle.sol index d5f04b4..2f57251 100644 --- a/src/oracles/BalancerOracle.sol +++ b/src/oracles/BalancerOracle.sol @@ -5,6 +5,7 @@ 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 @@ -12,10 +13,7 @@ import {IBalancerTwapOracle} from "../interfaces/IBalancerTwapOracle.sol"; /// @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 { @@ -30,6 +28,7 @@ contract BalancerOracle is IOracle, Owned { /// ----------------------------------------------------------------------- error BalancerOracle__TWAPOracleNotReady(); + error BalancerOracle__BelowMinPrice(); /// ----------------------------------------------------------------------- /// Events @@ -71,12 +70,17 @@ 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_, @@ -84,6 +88,11 @@ contract BalancerOracle is IOracle, Owned { 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_; @@ -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; } /// ----------------------------------------------------------------------- @@ -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_; diff --git a/test/BalancerOracle.t.sol b/test/BalancerOracle.t.sol new file mode 100644 index 0000000..450c494 --- /dev/null +++ b/test/BalancerOracle.t.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {BalancerOracle} from "../src/oracles/BalancerOracle.sol"; +import {IBalancerTwapOracle} from "../src/interfaces/IBalancerTwapOracle.sol"; +import {IVault, IAsset} from "../src/interfaces/IBalancerVault.sol"; +import {IBalancer2TokensPool} from "../src/interfaces/IBalancer2TokensPool.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +struct Params { + IBalancerTwapOracle pair; + address token; + address owner; + uint16 multiplier; + uint32 secs; + uint32 ago; + uint128 minPrice; +} + +contract BalancerOracleTest is Test { + using stdStorage for StdStorage; + using FixedPointMathLib for uint256; + + string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + uint32 FORK_BLOCK = 18764758; + + address TOKEN_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address PAYMENT_ADDRESS = 0xfd0205066521550D7d7AB19DA8F72bb004b4C341; + address POOL_ADDRESS = 0x9232a548DD9E81BaC65500b5e0d918F8Ba93675C; + + uint MULTIPLIER_DENOM = 10000; + + uint256 opFork; + + Params _default; + + function setUp() public { + _default = Params(IBalancerTwapOracle(POOL_ADDRESS), TOKEN_ADDRESS, address(this), 10000, 30 minutes, 0, 1000); + opFork = vm.createSelectFork(MAINNET_RPC_URL, FORK_BLOCK); + } + + function test_priceWithinAcceptableRange() public { + + BalancerOracle oracle = new BalancerOracle( + _default.pair, + _default.token, + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + uint oraclePrice = oracle.getPrice(); + + uint256 spotPrice = getSpotPrice(address(_default.pair), _default.token); + assertApproxEqRel(oraclePrice, spotPrice, 0.01 ether, "Price delta too big"); // 1% + } + + function test_priceToken1() public { + IVault vault = _default.pair.getVault(); + (address[] memory poolTokens,,) = vault.getPoolTokens(_default.pair.getPoolId()); + + BalancerOracle oracleToken0 = new BalancerOracle( + _default.pair, + poolTokens[0], + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + BalancerOracle oracleToken1 = new BalancerOracle( + _default.pair, + poolTokens[1], + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + uint priceToken0 = oracleToken0.getPrice(); + uint priceToken1 = oracleToken1.getPrice(); + + assertEq(priceToken1, uint256(1e18).divWadDown(priceToken0), "incorrect price"); // 1% + } + + function test_priceMultiplier(uint multiplier) public { + multiplier = bound(multiplier, 0, 10000); + + BalancerOracle oracle0 = new BalancerOracle( + _default.pair, + _default.token, + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + BalancerOracle oracle1 = new BalancerOracle( + _default.pair, + _default.token, + _default.owner, + uint16(multiplier), + _default.secs, + _default.ago, + _default.minPrice + ); + + uint price0 = oracle0.getPrice(); + uint price1 = oracle1.getPrice(); + + uint expectedPrice = price0.mulDivUp(multiplier, MULTIPLIER_DENOM); + + assertEq(price1, expectedPrice, "incorrect price multiplier"); + } + + function test_singleBlockManipulation() public { + IVault vault = _default.pair.getVault(); + BalancerOracle oracle = new BalancerOracle( + _default.pair, + _default.token, + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + address manipulator1 = makeAddr("manipulator"); + deal(TOKEN_ADDRESS, manipulator1, 1000000 ether); + + vm.startPrank(manipulator1); + IERC20(TOKEN_ADDRESS).approve(address(vault), 1000000 ether); + + (address[] memory tokens, uint256[] memory reserves,) = vault.getPoolTokens(_default.pair.getPoolId()); + + // swap 1 token to update oracle to latest block + swap(address(_default.pair), tokens[0], tokens[1], 1, manipulator1); + + // register initial oracle price + uint256 price_1 = oracle.getPrice(); + + swap(address(_default.pair), tokens[0], tokens[1], reserves[0] / 10, manipulator1); + + vm.stopPrank(); + + // check price variation + assertEq(oracle.getPrice(), price_1, "single block price variation"); + } + + function test_priceManipulation(uint256 skipTime) public { + skipTime = bound(skipTime, 1, _default.secs); + IVault vault = _default.pair.getVault(); + BalancerOracle oracle = new BalancerOracle( + _default.pair, + _default.token, + _default.owner, + _default.multiplier, + _default.secs, + _default.ago, + _default.minPrice + ); + + address manipulator1 = makeAddr("manipulator"); + deal(TOKEN_ADDRESS, manipulator1, 1000000 ether); + + vm.startPrank(manipulator1); + + // swap 1 token to update oracle to latest block + IERC20(TOKEN_ADDRESS).approve(address(vault), 1000000 ether); + swap(address(_default.pair), TOKEN_ADDRESS, PAYMENT_ADDRESS, 1, manipulator1); + + // register initial oracle price + uint256 price_1 = oracle.getPrice(); + + // perform a large swap (25% of reserves) + (address[] memory tokens, uint256[] memory reserves,) = vault.getPoolTokens(_default.pair.getPoolId()); + swap(address(_default.pair), tokens[0], tokens[1], reserves[0] / 4, manipulator1); + + vm.stopPrank(); + + // wait + skip(skipTime); + // update block + vm.roll(block.number + 1); + + // oracle price is only updated on swaps + assertEq(price_1, oracle.getPrice(), "price updated"); + + // perform additional, smaller swap + address manipulator2 = makeAddr("manipulator2"); + deal(PAYMENT_ADDRESS, manipulator2, 1); + vm.startPrank(manipulator2); + IERC20(PAYMENT_ADDRESS).approve(address(vault), 1); + swap(address(_default.pair), tokens[1], tokens[0], 1, manipulator2); + vm.stopPrank(); + + // weighted average of the first recorded oracle price and the current spot price + // weighted by the time since the last update + uint256 spotAverage = ((price_1 * (_default.secs - skipTime)) + (getSpotPrice(address(_default.pair), _default.token) * skipTime)) / _default.secs; + + assertApproxEqRel(spotAverage, oracle.getPrice(), 0.01 ether, "price variance too large"); + } + + function getSpotPrice(address pool, address token) internal view returns (uint256 price) { + IVault vault = IBalancerTwapOracle(pool).getVault(); + bytes32 poolId = IBalancerTwapOracle(pool).getPoolId(); + (address[] memory poolTokens,,) = vault.getPoolTokens(poolId); + + bool isToken0 = token == poolTokens[0]; + (,uint[] memory balances, ) = vault.getPoolTokens(poolId); + uint[] memory weights = IBalancer2TokensPool(pool).getNormalizedWeights(); + + price = isToken0 ? + (balances[1] * weights[0]).divWadDown(balances[0] * weights[1]) : + (balances[0] * weights[1]).divWadDown(balances[1] * weights[0]); + } + + function swap(address pool, address tokenIn, address tokenOut, uint amountIn, address sender) internal returns (uint amountOut) { + bytes32 poolId = IBalancerTwapOracle(pool).getPoolId(); + IVault.SingleSwap memory singleSwap = IVault.SingleSwap( + poolId, + IVault.SwapKind.GIVEN_IN, + IAsset(tokenIn), + IAsset(tokenOut), + amountIn, + "" + ); + + IVault.FundManagement memory funds = IVault.FundManagement( + sender, + false, + payable(sender), + false + ); + + return IVault(IBalancer2TokensPool(pool).getVault()).swap( + singleSwap, + funds, + 0, + type(uint256).max + ); + } + +} diff --git a/test/OptionsToken.t.sol b/test/OptionsToken.t.sol index 666756f..69af8a4 100644 --- a/test/OptionsToken.t.sol +++ b/test/OptionsToken.t.sol @@ -43,13 +43,18 @@ contract OptionsTokenTest is Test { treasury = makeAddr("treasury"); // deploy contracts - balancerTwapOracle = new MockBalancerTwapOracle(); - oracle = - new BalancerOracle(balancerTwapOracle, owner, ORACLE_MULTIPLIER, ORACLE_SECS, ORACLE_AGO, ORACLE_MIN_PRICE); paymentToken = new TestERC20(); underlyingToken = address(new TestERC20()); optionsToken = new OptionsToken("TIT Call Option Token", "oTIT", owner, tokenAdmin); + address[] memory tokens = new address[](2); + tokens[0] = underlyingToken; + tokens[1] = address(paymentToken); + + balancerTwapOracle = new MockBalancerTwapOracle(tokens); + oracle = + new BalancerOracle(balancerTwapOracle, underlyingToken, owner, ORACLE_MULTIPLIER, ORACLE_SECS, ORACLE_AGO, ORACLE_MIN_PRICE); + exerciser = new DiscountExercise(optionsToken, owner, paymentToken, ERC20(underlyingToken), oracle, treasury); TestERC20(underlyingToken).mint(address(exerciser), 1e20 ether); @@ -113,7 +118,7 @@ contract OptionsTokenTest is Test { } function test_exerciseMinPrice(uint256 amount, address recipient) public { - amount = bound(amount, 0, MAX_SUPPLY); + amount = bound(amount, 1, MAX_SUPPLY); // mint options tokens vm.prank(tokenAdmin); @@ -127,23 +132,9 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens - DiscountExerciseParams memory params = DiscountExerciseParams({ - maxPaymentAmount: expectedPaymentAmount, - deadline: type(uint256).max - }); - (uint paymentAmount,,,) = optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); - - // verify options tokens were transferred - assertEqDecimal(optionsToken.balanceOf(address(this)), 0, 18, "user still has options tokens"); - assertEqDecimal(optionsToken.balanceOf(address(0)), amount, 18, "address(0) didn't get options tokens"); - assertEqDecimal(optionsToken.totalSupply(), amount, 18, "total supply changed"); - - // verify payment tokens were transferred - assertEqDecimal(paymentToken.balanceOf(address(this)), 0, 18, "user still has payment tokens"); - assertEqDecimal( - paymentToken.balanceOf(treasury), expectedPaymentAmount, 18, "treasury didn't receive payment tokens" - ); - assertEqDecimal(paymentAmount, expectedPaymentAmount, 18, "exercise returned wrong value"); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + vm.expectRevert(bytes4(keccak256("BalancerOracle__BelowMinPrice()"))); + optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); } function test_exerciseHighSlippage(uint256 amount, address recipient) public { @@ -183,7 +174,7 @@ contract OptionsTokenTest is Test { // such that the TWAP window becomes (block.timestamp - ORACLE_LARGEST_SAFETY_WINDOW - ORACLE_SECS, block.timestamp - ORACLE_LARGEST_SAFETY_WINDOW] // which is outside of the largest safety window vm.prank(owner); - oracle.setParams(ORACLE_MULTIPLIER, ORACLE_SECS, ORACLE_LARGEST_SAFETY_WINDOW, ORACLE_MIN_PRICE); + oracle.setParams(address(0), ORACLE_MULTIPLIER, ORACLE_SECS, ORACLE_LARGEST_SAFETY_WINDOW, ORACLE_MIN_PRICE); // exercise options tokens which should fail DiscountExerciseParams memory params = DiscountExerciseParams({ diff --git a/test/mocks/MockBalancerTwapOracle.sol b/test/mocks/MockBalancerTwapOracle.sol index 0ace245..1f31322 100644 --- a/test/mocks/MockBalancerTwapOracle.sol +++ b/test/mocks/MockBalancerTwapOracle.sol @@ -2,39 +2,89 @@ pragma solidity ^0.8.11; import {IBalancerTwapOracle} from "../../src/interfaces/IBalancerTwapOracle.sol"; +import {IVault} from "../../src/interfaces/IBalancerTwapOracle.sol"; -contract MockBalancerTwapOracle is IBalancerTwapOracle { - uint256 twapValue; +contract MockVault is IVault { + address[] tokens = new address[](2); - function setTwapValue(uint256 value) external { - twapValue = value; + constructor (address[] memory _tokens) { + tokens = _tokens; } - function getTimeWeightedAverage(IBalancerTwapOracle.OracleAverageQuery[] memory queries) + function joinPool( + bytes32 poolId, + address sender, + address recipient, + JoinPoolRequest memory request + ) external payable override {} + + function getPool( + bytes32 poolId + ) external view override returns (address, PoolSpecialization) {} + + function getPoolTokens( + bytes32 poolId + ) external view override - returns (uint256[] memory results) + returns (address[] memory tokens, uint256[] memory, uint256) { - queries; - results = new uint256[](1); - results[0] = twapValue; + tokens = new address[](2); + tokens[0] = tokens[0]; + tokens[1] = tokens[1]; } - function getLatest(IBalancerTwapOracle.Variable variable) external view override returns (uint256) { - // not implemented + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable override returns (uint256) {} +} + +contract MockBalancerTwapOracle is IBalancerTwapOracle { + uint256 twapValue; + IVault mockVault; + + constructor (address[] memory tokens) { + mockVault = new MockVault(tokens); } - function getLargestSafeQueryWindow() external pure override returns (uint256) { - return 24 hours; // simulates an oracle that can look back at most 24 hours + function setTwapValue(uint256 value) external { + twapValue = value; + } + + function getTimeWeightedAverage( + IBalancerTwapOracle.OracleAverageQuery[] memory queries + ) external view override returns (uint256[] memory results) { + queries; + results = new uint256[](1); + results[0] = twapValue; } - function getPastAccumulators(IBalancerTwapOracle.OracleAccumulatorQuery[] memory queries) + function getLargestSafeQueryWindow() external - view + pure override - returns (int256[] memory results) + returns (uint256) { - // not implemented + return 24 hours; // simulates an oracle that can look back at most 24 hours } + + function getPastAccumulators( + IBalancerTwapOracle.OracleAccumulatorQuery[] memory queries + ) external view override returns (int256[] memory results) { + } + + function getLatest( + IBalancerTwapOracle.Variable variable + ) external view override returns (uint256) { + } + + function getVault() external view override returns (IVault) { + return mockVault; + } + + function getPoolId() external view override returns (bytes32) {} }