diff --git a/contracts/interfaces/IExchangeAdapter.sol b/contracts/interfaces/IExchangeAdapter.sol index f6dcf07bd..c2d1e9fd2 100644 --- a/contracts/interfaces/IExchangeAdapter.sol +++ b/contracts/interfaces/IExchangeAdapter.sol @@ -30,4 +30,9 @@ interface IExchangeAdapter { external view returns (address, uint256, bytes memory); + function generateDataParam( + address _sellComponent, + address _buyComponent, + bool _fixIn + ) external view returns (bytes memory); } \ No newline at end of file diff --git a/contracts/protocol/integration/BalancerV1ExchangeAdapter.sol b/contracts/protocol/integration/BalancerV1ExchangeAdapter.sol new file mode 100644 index 000000000..844e40717 --- /dev/null +++ b/contracts/protocol/integration/BalancerV1ExchangeAdapter.sol @@ -0,0 +1,124 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +/** + * @title BalancerV1ExchangeAdapter + * @author Set Protocol + * + * A Balancer exchange adapter that returns calldata for trading. + */ +contract BalancerV1ExchangeAdapter { + + /* ============ Constants ============ */ + + // Amount of pools examined when fetching quote + uint256 private constant BALANCER_POOL_LIMIT = 3; + + /* ============ State Variables ============ */ + + // Address of Uniswap V2 Router02 contract + address public immutable balancerProxy; + // Balancer proxy function string for swapping exact tokens for a minimum of receive tokens + string internal constant EXACT_IN = "smartSwapExactIn(address,address,uint256,uint256,uint256)"; + // Balancer proxy function string for swapping tokens for an exact amount of receive tokens + string internal constant EXACT_OUT = "smartSwapExactOut(address,address,uint256,uint256,uint256)"; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _balancerProxy Balancer exchange proxy address + */ + constructor(address _balancerProxy) public { + balancerProxy = _balancerProxy; + } + + /* ============ External Getter Functions ============ */ + + /** + * Return calldata for Balancer Proxy. Bool to select trade function is encoded in the arbitrary data parameter. + * + * @param _sourceToken Address of source token to be sold + * @param _destinationToken Address of destination token to buy + * @param _destinationAddress Address that assets should be transferred to + * @param _sourceQuantity Fixed/Max amount of source token to sell + * @param _destinationQuantity Min/Fixed amount of destination tokens to receive + * @param _data Arbitrary bytes containing bool to determine function string + * + * @return address Target contract address + * @return uint256 Call value + * @return bytes Trade calldata + */ + function getTradeCalldata( + address _sourceToken, + address _destinationToken, + address _destinationAddress, + uint256 _sourceQuantity, + uint256 _destinationQuantity, + bytes memory _data + ) + external + view + returns (address, uint256, bytes memory) + { + ( + bool shouldSwapFixedInputAmount + ) = abi.decode(_data, (bool)); + + bytes memory callData = abi.encodeWithSignature( + shouldSwapFixedInputAmount ? EXACT_IN : EXACT_OUT, + _sourceToken, + _destinationToken, + shouldSwapFixedInputAmount ? _sourceQuantity : _destinationQuantity, + shouldSwapFixedInputAmount ? _destinationQuantity : _sourceQuantity, + BALANCER_POOL_LIMIT + ); + + return (balancerProxy, 0, callData); + } + + /** + * Generate data parameter to be passed to `getTradeCallData`. Returns encoded bool to select trade function. + * + * @param _sellComponent Address of the token to be sold + * @param _buyComponent Address of the token to be bought + * @param _fixIn Boolean representing if input tokens amount is fixed + * + * @return bytes Data parameter to be passed to `getTradeCallData` + */ + function generateDataParam(address _sellComponent, address _buyComponent, bool _fixIn) + external + view + returns (bytes memory) + { + return abi.encode(_fixIn); + } + + /** + * Returns the address to approve source tokens to for trading. This is the Balancer proxy address + * + * @return address Address of the contract to approve tokens to + */ + function getSpender() external view returns (address) { + return balancerProxy; + } +} \ No newline at end of file diff --git a/contracts/protocol/integration/UniswapV2ExchangeAdapterV2.sol b/contracts/protocol/integration/UniswapV2ExchangeAdapterV2.sol index 3bfd85754..f48be35e3 100644 --- a/contracts/protocol/integration/UniswapV2ExchangeAdapterV2.sol +++ b/contracts/protocol/integration/UniswapV2ExchangeAdapterV2.sol @@ -23,12 +23,13 @@ pragma experimental "ABIEncoderV2"; * @title UniswapV2ExchangeAdapterV2 * @author Set Protocol * - * A Uniswap Router02 exchange adapter that returns calldata for trading. Includes option for 2 different trade types on Uniswap + * A Uniswap Router02 exchange adapter that returns calldata for trading. Includes option for 2 different trade types on Uniswap. * * CHANGE LOG: * - Add helper that encodes path and boolean into bytes * - Generalized ability to choose whether to swap an exact amount of source token for a min amount of receive token or swap a max amount of source token for * an exact amount of receive token + * - Add helper to generate data parameter for `getTradeCallData` * */ contract UniswapV2ExchangeAdapterV2 { @@ -64,8 +65,8 @@ contract UniswapV2ExchangeAdapterV2 { * @param _sourceToken Address of source token to be sold * @param _destinationToken Address of destination token to buy * @param _destinationAddress Address that assets should be transferred to - * @param _sourceQuantity Amount of source token to sell - * @param _minDestinationQuantity Min amount of destination token to buy + * @param _sourceQuantity Fixed/Max amount of source token to sell + * @param _destinationQuantity Min/Fixed amount of destination token to buy * @param _data Arbitrary bytes containing trade path and bool to determine function string * * @return address Target contract address @@ -77,7 +78,7 @@ contract UniswapV2ExchangeAdapterV2 { address _destinationToken, address _destinationAddress, uint256 _sourceQuantity, - uint256 _minDestinationQuantity, + uint256 _destinationQuantity, bytes memory _data ) external @@ -86,22 +87,40 @@ contract UniswapV2ExchangeAdapterV2 { { ( address[] memory path, - bool shouldSwapTokensForExactTokens + bool shouldSwapExactTokensForTokens ) = abi.decode(_data, (address[],bool)); - // If shouldSwapTokensForExactTokens, use appropriate function string and flip source and destination quantities to conform with Uniswap interface bytes memory callData = abi.encodeWithSignature( - shouldSwapTokensForExactTokens ? SWAP_TOKENS_FOR_EXACT_TOKENS : SWAP_EXACT_TOKENS_FOR_TOKENS, - shouldSwapTokensForExactTokens ? _minDestinationQuantity : _sourceQuantity, - shouldSwapTokensForExactTokens ? _sourceQuantity : _minDestinationQuantity, + shouldSwapExactTokensForTokens ? SWAP_EXACT_TOKENS_FOR_TOKENS : SWAP_TOKENS_FOR_EXACT_TOKENS, + shouldSwapExactTokensForTokens ? _sourceQuantity : _destinationQuantity, + shouldSwapExactTokensForTokens ? _destinationQuantity : _sourceQuantity, path, _destinationAddress, block.timestamp ); - return (router, 0, callData); } + /** + * Generate data parameter to be passed to `getTradeCallData`. Returns encoded trade paths and bool to select trade function. + * + * @param _sellComponent Address of the token to be sold + * @param _buyComponent Address of the token to be bought + * @param _fixIn Boolean representing if input tokens amount is fixed + * + * @return bytes Data parameter to be passed to `getTradeCallData` + */ + function generateDataParam(address _sellComponent, address _buyComponent, bool _fixIn) + external + view + returns (bytes memory) + { + address[] memory path = new address[](2); + path[0] = _sellComponent; + path[1] = _buyComponent; + return abi.encode(path, _fixIn); + } + /** * Returns the address to approve source tokens to for trading. This is the Uniswap router address * @@ -116,7 +135,7 @@ contract UniswapV2ExchangeAdapterV2 { * * @return bytes Encoded data used for trading on Uniswap */ - function getUniswapExchangeData(address[] memory _path, bool _shouldSwapTokensForExactTokens) external view returns (bytes memory) { - return abi.encode(_path, _shouldSwapTokensForExactTokens); + function getUniswapExchangeData(address[] memory _path, bool _shouldSwapExactTokensForTokens) external view returns (bytes memory) { + return abi.encode(_path, _shouldSwapExactTokensForTokens); } } \ No newline at end of file diff --git a/contracts/protocol/modules/GeneralIndexModule.sol b/contracts/protocol/modules/GeneralIndexModule.sol new file mode 100644 index 000000000..3ff5a4bfa --- /dev/null +++ b/contracts/protocol/modules/GeneralIndexModule.sol @@ -0,0 +1,887 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Math } from "@openzeppelin/contracts/math/Math.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { AddressArrayUtils } from "../../lib/AddressArrayUtils.sol"; +import { IController } from "../../interfaces/IController.sol"; +import { IExchangeAdapter } from "../../interfaces/IExchangeAdapter.sol"; +import { Invoke } from "../lib/Invoke.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IWETH } from "../../interfaces/external/IWETH.sol"; +import { ModuleBase } from "../lib/ModuleBase.sol"; +import { Position } from "../lib/Position.sol"; +import { PreciseUnitMath } from "../../lib/PreciseUnitMath.sol"; +import { Uint256ArrayUtils } from "../../lib/Uint256ArrayUtils.sol"; + +/** + * @title GeneralIndexModule + * @author Set Protocol + * + * Smart contract that facilitates rebalances for indices. Manager can set target unit amounts, max trade sizes, the + * exchange to trade on, and the cool down period between trades (on a per asset basis). + * + * SECURITY ASSUMPTION: + * - Works with following modules: StreamingFeeModule, BasicIssuanceModule (any other module additions to Sets using + this module need to be examined separately) + */ +contract GeneralIndexModule is ModuleBase, ReentrancyGuard { + using SafeCast for int256; + using SafeCast for uint256; + using SafeMath for uint256; + using Position for uint256; + using Math for uint256; + using Position for ISetToken; + using Invoke for ISetToken; + using AddressArrayUtils for address[]; + using AddressArrayUtils for IERC20[]; + using Uint256ArrayUtils for uint256[]; + + /* ============ Struct ============ */ + + struct TradeExecutionParams { + uint256 targetUnit; // Target unit of component for Set + uint256 maxSize; // Max trade size in precise units + uint256 coolOffPeriod; // Required time between trades for the asset + uint256 lastTradeTimestamp; // Timestamp of last trade + string exchangeName; // Name of exchange adapter + } + + struct TradePermissionInfo { + bool anyoneTrade; // Boolean indicating if anyone can execute a trade + mapping(address => bool) tradeAllowList; // Mapping indicating which addresses are allowed to execute trade + } + + struct RebalanceInfo { + uint256 positionMultiplier; // Position multiplier at the beginning of rebalance + uint256 raiseTargetPercentage; // Amount to raise all unit targets by if allowed (in precise units) + address[] rebalanceComponents; // Array of components involved in rebalance + } + + struct TradeInfo { + ISetToken setToken; // Instance of SetToken + IExchangeAdapter exchangeAdapter; // Instance of Exchange Adapter + address sendToken; // Address of token being sold + address receiveToken; // Address of token being bought + bool isSendTokenFixed; // Boolean indicating fixed asset is send token + uint256 setTotalSupply; // Total supply of Set (in precise units) + uint256 totalFixedQuantity; // Total quanity of fixed asset being traded + uint256 floatingQuantityLimit; // Max/min amount of floating token spent/received during trade + uint256 preTradeSendTokenBalance; // Total initial balance of token being sold + uint256 preTradeReceiveTokenBalance; // Total initial balance of token being bought + } + + /* ============ Events ============ */ + + event TargetUnitsUpdated(ISetToken indexed _setToken, address indexed _component, uint256 _newUnit, uint256 _positionMultiplier); + event TradeMaximumUpdated(ISetToken indexed _setToken, address indexed _component, uint256 _newMaximum); + event AssetExchangeUpdated(ISetToken indexed _setToken, address indexed _component, string _newExchangeName); + event CoolOffPeriodUpdated(ISetToken indexed _setToken, address indexed _component, uint256 _newCoolOffPeriod); + event RaiseTargetPercentageUpdated(ISetToken indexed _setToken, uint256 indexed _raiseTargetPercentage); + + event AnyoneTradeUpdated(ISetToken indexed _setToken, bool indexed _status); + event TraderStatusUpdated(ISetToken indexed _setToken, address indexed _trader, bool _status); + + event TradeExecuted( + ISetToken indexed _setToken, + address indexed _sellComponent, + address indexed _buyComponent, + IExchangeAdapter _exchangeAdapter, + address _executor, + uint256 _amountSold, + uint256 _netAmountBought, + uint256 _protocolFee + ); + + /* ============ Constants ============ */ + + uint256 private constant GENERAL_INDEX_MODULE_PROTOCOL_FEE_INDEX = 0; + + /* ============ State Variables ============ */ + + mapping(ISetToken => mapping(IERC20 => TradeExecutionParams)) public executionInfo; // Mapping of SetToken to execution parameters of each asset on SetToken + mapping(ISetToken => TradePermissionInfo) public permissionInfo; // Mapping of SetToken to trading permissions + mapping(ISetToken => RebalanceInfo) public rebalanceInfo; // Mapping of SetToken to relevant data for current rebalance + IWETH public weth; // Weth contract address + + /* ============ Modifiers ============ */ + + modifier onlyAllowedTrader(ISetToken _setToken, address _caller) { + require(_isAllowedTrader(_setToken, _caller), "Address not permitted to trade"); + _; + } + + modifier onlyEOAIfUnrestricted(ISetToken _setToken) { + if(permissionInfo[_setToken].anyoneTrade) { + require(msg.sender == tx.origin, "Caller must be EOA Address"); + } + _; + } + + /* ============ Constructor ============ */ + + constructor(IController _controller, IWETH _weth) public ModuleBase(_controller) { + weth = _weth; + } + + /* ============ External Functions ============ */ + + /** + * MANAGER ONLY: Set new target units, zeroing out any units for components being removed from index. Log position multiplier to + * adjust target units in case fees are accrued. Validate that every oldComponent has a targetUnit and that no components have been duplicated. + * + * @param _setToken Address of the SetToken to be rebalanced + * @param _newComponents Array of new components to add to allocation + * @param _newComponentsTargetUnits Array of target units at end of rebalance for new components, maps to same index of _newComponents array + * @param _oldComponentsTargetUnits Array of target units at end of rebalance for old component, maps to same index of + * _setToken.getComponents() array, if component being removed set to 0. + * @param _positionMultiplier Position multiplier when target units were calculated, needed in order to adjust target units + * if fees accrued + */ + function startRebalance( + ISetToken _setToken, + address[] calldata _newComponents, + uint256[] calldata _newComponentsTargetUnits, + uint256[] calldata _oldComponentsTargetUnits, + uint256 _positionMultiplier + ) + external + onlyManagerAndValidSet(_setToken) + { + // Don't use validate arrays because empty arrays are valid + require(_newComponents.length == _newComponentsTargetUnits.length, "Array length mismatch"); + + address[] memory currentComponents = _setToken.getComponents(); + require( + currentComponents.length == _oldComponentsTargetUnits.length, + "New allocation must have target for all old components" + ); + + address[] memory aggregateComponents = currentComponents.extend(_newComponents); + uint256[] memory aggregateTargetUnits = _oldComponentsTargetUnits.extend(_newComponentsTargetUnits); + + require(!aggregateComponents.hasDuplicate(), "Cannot duplicate components"); + + for (uint256 i = 0; i < aggregateComponents.length; i++) { + + executionInfo[_setToken][IERC20(aggregateComponents[i])].targetUnit = aggregateTargetUnits[i]; + + emit TargetUnitsUpdated(_setToken, aggregateComponents[i], aggregateTargetUnits[i], _positionMultiplier); + } + + rebalanceInfo[_setToken].rebalanceComponents = aggregateComponents; + rebalanceInfo[_setToken].positionMultiplier = _positionMultiplier; + } + + /** + * ACCESS LIMITED: Only approved addresses can call if anyoneTrade is false. Determines trade size + * and direction and swaps into or out of WETH on exchange specified by manager. + * + * @param _setToken Address of the SetToken + * @param _component Address of SetToken component to trade + * @param _ethQuantityLimit Max/min amount of ETH spent/received during trade + */ + function trade( + ISetToken _setToken, + IERC20 _component, + uint256 _ethQuantityLimit + ) + external + nonReentrant + onlyAllowedTrader(_setToken, msg.sender) + onlyEOAIfUnrestricted(_setToken) + virtual + { + + _validateTradeParameters(_setToken, _component); + + TradeInfo memory tradeInfo = _createTradeInfo(_setToken, _component, _ethQuantityLimit); + + _executeTrade(tradeInfo); + + uint256 protocolFee = _accrueProtocolFee(tradeInfo); + + (uint256 sellAmount, uint256 netBuyAmount) = _updatePositionState(tradeInfo); + + executionInfo[_setToken][_component].lastTradeTimestamp = block.timestamp; + + emit TradeExecuted( + tradeInfo.setToken, + tradeInfo.sendToken, + tradeInfo.receiveToken, + tradeInfo.exchangeAdapter, + msg.sender, + sellAmount, + netBuyAmount, + protocolFee + ); + } + + /** + * ACCESS LIMITED: Only approved addresses can call if anyoneTrade is false. Only callable when 1) there are no + * more components to be sold and, 2) entire remaining WETH amount can be traded such that resulting inflows won't + * exceed components maxTradeSize nor overshoot the target unit. To be used near the end of rebalances when a + * component's calculated trade size is greater in value than remaining WETH. + * + * @param _setToken Address of the SetToken + * @param _component Address of the SetToken component to trade + * @param _componentQuantityLimit Min amount of component received during trade + */ + function tradeRemainingWETH( + ISetToken _setToken, + IERC20 _component, + uint256 _componentQuantityLimit + ) + external + nonReentrant + onlyAllowedTrader(_setToken, msg.sender) + onlyEOAIfUnrestricted(_setToken) + virtual + { + + require(_noTokensToSell(_setToken), "Sell other set components first"); + require( + executionInfo[_setToken][weth].targetUnit < _setToken.getDefaultPositionRealUnit(address(weth)).toUint256(), + "WETH is below target unit and can not be traded" + ); + + _validateTradeParameters(_setToken, _component); + + TradeInfo memory tradeInfo = _createTradeRemainingInfo(_setToken, _component, _componentQuantityLimit); + + _executeTrade(tradeInfo); + + uint256 protocolFee = _accrueProtocolFee(tradeInfo); + + (uint256 sellAmount, uint256 netBuyAmount) = _updatePositionState(tradeInfo); + + require( + netBuyAmount.add(protocolFee) < executionInfo[_setToken][_component].maxSize, + "Trade amount exceeds max allowed trade size" + ); + + _validateComponentPositionUnit(_setToken, _component); + + executionInfo[_setToken][_component].lastTradeTimestamp = block.timestamp; + + emit TradeExecuted( + tradeInfo.setToken, + tradeInfo.sendToken, + tradeInfo.receiveToken, + tradeInfo.exchangeAdapter, + msg.sender, + sellAmount, + netBuyAmount, + protocolFee + ); + } + + /** + * ACCESS LIMITED: For situation where all target units met and remaining WETH, uniformly raise targets by same + * percentage in order to allow further trading. Can be called multiple times if necessary, increase should be + * small in order to reduce tracking error. + * + * @param _setToken Address of the SetToken + */ + function raiseAssetTargets(ISetToken _setToken) external onlyAllowedTrader(_setToken, msg.sender) virtual { + require( + _allTargetsMet(_setToken) + && _setToken.getDefaultPositionRealUnit(address(weth)).toUint256() > _getNormalizedTargetUnit(_setToken, weth), + "Targets must be met and ETH remaining in order to raise target" + ); + + rebalanceInfo[_setToken].positionMultiplier = rebalanceInfo[_setToken].positionMultiplier.preciseDiv( + PreciseUnitMath.preciseUnit().add(rebalanceInfo[_setToken].raiseTargetPercentage) + ); + } + + /** + * MANAGER ONLY: Set trade maximums for passed components of the SetToken + * + * @param _setToken Address of the SetToken + * @param _components Array of components + * @param _tradeMaximums Array of trade maximums mapping to correct component + */ + function setTradeMaximums( + ISetToken _setToken, + address[] calldata _components, + uint256[] calldata _tradeMaximums + ) + external + onlyManagerAndValidSet(_setToken) + { + _validateUintArrays(_components, _tradeMaximums); + + for (uint256 i = 0; i < _components.length; i++) { + executionInfo[_setToken][IERC20(_components[i])].maxSize = _tradeMaximums[i]; + emit TradeMaximumUpdated(_setToken, _components[i], _tradeMaximums[i]); + } + } + + /** + * MANAGER ONLY: Set exchange for passed components of the SetToken + * + * @param _setToken Address of the SetToken + * @param _components Array of components + * @param _exchangeNames Array of exchange names mapping to correct component + */ + function setExchanges( + ISetToken _setToken, + address[] calldata _components, + string[] calldata _exchangeNames + ) + external + onlyManagerAndValidSet(_setToken) + { + require(_components.length == _exchangeNames.length, "Array length mismatch"); + require(_components.length > 0, "Array length must be > 0"); + require(!_components.hasDuplicate(), "Cannot duplicate components"); + + for (uint256 i = 0; i < _components.length; i++) { + if (_components[i] != address(weth)) { + require(bytes(_exchangeNames[i]).length != 0, "Exchange name can not be an empty string"); + executionInfo[_setToken][IERC20(_components[i])].exchangeName = _exchangeNames[i]; + emit AssetExchangeUpdated(_setToken, _components[i], _exchangeNames[i]); + } + } + } + + /** + * MANAGER ONLY: Set cool off periods for passed components of the SetToken + * + * @param _setToken Address of the SetToken + * @param _components Array of components + * @param _coolOffPeriods Array of cool off periods to correct component + */ + function setCoolOffPeriods( + ISetToken _setToken, + address[] calldata _components, + uint256[] calldata _coolOffPeriods + ) + external + onlyManagerAndValidSet(_setToken) + { + _validateUintArrays(_components, _coolOffPeriods); + + for (uint256 i = 0; i < _components.length; i++) { + executionInfo[_setToken][IERC20(_components[i])].coolOffPeriod = _coolOffPeriods[i]; + emit CoolOffPeriodUpdated(_setToken, _components[i], _coolOffPeriods[i]); + } + } + + /** + * MANAGER ONLY: Set amount by which all component's targets units would be raised + * + * @param _setToken Address of the SetToken + * @param _raiseTargetPercentage Amount to raise all component's unit targets by (in precise units) + */ + function updateRaiseTargetPercentage( + ISetToken _setToken, + uint256 _raiseTargetPercentage + ) + external + onlyManagerAndValidSet(_setToken) + { + require(_raiseTargetPercentage > 0, "Target percentage must be > 0"); + rebalanceInfo[_setToken].raiseTargetPercentage = _raiseTargetPercentage; + emit RaiseTargetPercentageUpdated(_setToken, _raiseTargetPercentage); + } + + /** + * MANAGER ONLY: Toggle ability for passed addresses to trade. + * + * @param _setToken Address of the SetToken + * @param _traders Array trader addresses to toggle status + * @param _statuses Booleans indicating if matching trader can trade + */ + function updateTraderStatus( + ISetToken _setToken, + address[] calldata _traders, + bool[] calldata _statuses + ) + external + onlyManagerAndValidSet(_setToken) + { + require(_traders.length == _statuses.length, "Array length mismatch"); + require(_traders.length > 0, "Array length must be > 0"); + require(!_traders.hasDuplicate(), "Cannot duplicate traders"); + + for (uint256 i = 0; i < _traders.length; i++) { + permissionInfo[_setToken].tradeAllowList[_traders[i]] = _statuses[i]; + emit TraderStatusUpdated(_setToken, _traders[i], _statuses[i]); + } + } + + /** + * MANAGER ONLY: Toggle whether anyone can trade, bypassing the traderAllowList + * + * @param _setToken Address of the SetToken + * @param _status Boolean indicating if anyone can trade + */ + function updateAnyoneTrade(ISetToken _setToken, bool _status) external onlyManagerAndValidSet(_setToken) { + permissionInfo[_setToken].anyoneTrade = _status; + emit AnyoneTradeUpdated(_setToken, _status); + } + + /** + * MANAGER ONLY: Set target units to current units and last trade to zero. Initialize module. + * + * @param _setToken Address of the Set Token + */ + function initialize(ISetToken _setToken) + external + onlySetManager(_setToken, msg.sender) + onlyValidAndPendingSet(_setToken) + { + ISetToken.Position[] memory positions = _setToken.getPositions(); + + for (uint256 i = 0; i < positions.length; i++) { + ISetToken.Position memory position = positions[i]; + executionInfo[_setToken][IERC20(position.component)].targetUnit = position.unit.toUint256(); + executionInfo[_setToken][IERC20(position.component)].lastTradeTimestamp = 0; + } + + rebalanceInfo[_setToken].positionMultiplier = _setToken.positionMultiplier().toUint256(); + _setToken.initializeModule(); + } + + /** + * Called by a SetToken to notify that this module was removed from the SetToken. + * Clears the state of the calling SetToken. + */ + function removeModule() external override { + delete rebalanceInfo[ISetToken(msg.sender)]; + delete permissionInfo[ISetToken(msg.sender)]; + } + + /* ============ External View Functions ============ */ + + /** + * Get the array of SetToken components involved in rebalance. + * + * @param _setToken Address of the SetToken + * + * @return address[] Array of _setToken components involved in rebalance + */ + function getRebalanceComponents(ISetToken _setToken) external view returns (address[] memory) { + return rebalanceInfo[_setToken].rebalanceComponents; + } + + /** + * Calculates the amount of a component is going to be traded and whether the component is being bought + * or sold. If currentUnit and targetUnit are the same, function will revert. + * + * @param _setToken Instance of the SetToken to rebalance + * @param _component IERC20 component to trade + * + * @return isSell Boolean indicating if component is being sold + * @return componentQuantity Amount of component being traded + */ + function getComponentTradeQuantityAndDirection( + ISetToken _setToken, + IERC20 _component + ) + external + view + returns (bool, uint256) + { + uint256 totalSupply = _setToken.totalSupply(); + return _calculateTradeSizeAndDirection(_setToken, _component, totalSupply); + } + + + /** + * Get if a given address is an allowed trader. + * + * @param _setToken Address of the SetToken + * @param _trader Address of the trader + * + * @return bool True if _trader is allowed to trade, else false + */ + function getIsAllowedTrader(ISetToken _setToken, address _trader) external view returns (bool) { + return _isAllowedTrader(_setToken, _trader); + } + + /* ============ Internal Functions ============ */ + + /** + * Validate that component is a valid component and enough time has elapsed since component's last trade. + * + * @param _setToken Instance of the SetToken + * @param _component IERC20 component to be validated + */ + function _validateTradeParameters(ISetToken _setToken, IERC20 _component) internal view virtual { + require(address(_component) != address(weth), "Can not explicitly trade WETH"); + require( + rebalanceInfo[_setToken].rebalanceComponents.contains(address(_component)), + "Passed component not included in rebalance" + ); + + TradeExecutionParams memory componentInfo = executionInfo[_setToken][_component]; + require( + componentInfo.lastTradeTimestamp.add(componentInfo.coolOffPeriod) <= block.timestamp, + "Cool off period has not elapsed." + ); + } + + /** + * Create and return TradeInfo struct. This function reverts if the target has already been met. + * + * @param _setToken Instance of the SetToken to rebalance + * @param _component IERC20 component to trade + * @param _ethQuantityLimit Max/min amount of weth spent/received during trade + * + * @return TradeInfo Struct containing data for trade + */ + function _createTradeInfo( + ISetToken _setToken, + IERC20 _component, + uint256 _ethQuantityLimit + ) + internal + view + virtual + returns (TradeInfo memory) + { + + uint256 totalSupply = _setToken.totalSupply(); + + TradeInfo memory tradeInfo; + tradeInfo.setToken = _setToken; + + tradeInfo.exchangeAdapter = IExchangeAdapter(getAndValidateAdapter(executionInfo[_setToken][_component].exchangeName)); + + ( + tradeInfo.isSendTokenFixed, + tradeInfo.totalFixedQuantity + ) = _calculateTradeSizeAndDirection(_setToken, _component, totalSupply); + + tradeInfo.sendToken = tradeInfo.isSendTokenFixed ? address(_component) : address(weth); + + tradeInfo.receiveToken = tradeInfo.isSendTokenFixed ? address(weth): address(_component); + + tradeInfo.preTradeSendTokenBalance = IERC20(tradeInfo.sendToken).balanceOf(address(_setToken)); + tradeInfo.preTradeReceiveTokenBalance = IERC20(tradeInfo.receiveToken).balanceOf(address(_setToken)); + + tradeInfo.floatingQuantityLimit = tradeInfo.isSendTokenFixed + ? _ethQuantityLimit + : _ethQuantityLimit.min(tradeInfo.preTradeSendTokenBalance); + + tradeInfo.setTotalSupply = totalSupply; + + return tradeInfo; + } + + /** + * Create and return TradeInfo struct. This function does NOT check if the WETH target has been met. + * + * @param _setToken Instance of the SetToken to rebalance + * @param _component IERC20 component to trade + * @param _componentQuantityLimit Min amount of component received during trade + * + * @return TradeInfo Struct containing data for trade + */ + function _createTradeRemainingInfo( + ISetToken _setToken, + IERC20 _component, + uint256 _componentQuantityLimit + ) + internal + view + returns (TradeInfo memory) + { + + uint256 totalSupply = _setToken.totalSupply(); + + uint256 currentUnit = _setToken.getDefaultPositionRealUnit(address(weth)).toUint256(); + uint256 targetUnit = _getNormalizedTargetUnit(_setToken, weth); + + uint256 currentNotional = totalSupply.getDefaultTotalNotional(currentUnit); + uint256 targetNotional = totalSupply.preciseMulCeil(targetUnit); + + TradeInfo memory tradeInfo; + tradeInfo.setToken = _setToken; + + tradeInfo.exchangeAdapter = IExchangeAdapter(getAndValidateAdapter(executionInfo[_setToken][_component].exchangeName)); + + tradeInfo.isSendTokenFixed = true; + + tradeInfo.sendToken = address(weth); + tradeInfo.receiveToken = address(_component); + + tradeInfo.setTotalSupply = totalSupply; + + tradeInfo.totalFixedQuantity = currentNotional.sub(targetNotional); + tradeInfo.floatingQuantityLimit = _componentQuantityLimit; + + tradeInfo.preTradeSendTokenBalance = weth.balanceOf(address(_setToken)); + tradeInfo.preTradeReceiveTokenBalance = _component.balanceOf(address(_setToken)); + return tradeInfo; + } + + /** + * Invoke approve for send token, get method data and invoke trade in the context of the SetToken. + * + * @param _tradeInfo Struct containing trade information used in internal functions + */ + function _executeTrade(TradeInfo memory _tradeInfo) internal virtual { + + _tradeInfo.setToken.invokeApprove( + _tradeInfo.sendToken, + _tradeInfo.exchangeAdapter.getSpender(), + _tradeInfo.isSendTokenFixed ? _tradeInfo.totalFixedQuantity : _tradeInfo.floatingQuantityLimit + ); + + bytes memory tradeData = _tradeInfo.exchangeAdapter.generateDataParam( + _tradeInfo.sendToken, + _tradeInfo.receiveToken, + _tradeInfo.isSendTokenFixed + ); + + ( + address targetExchange, + uint256 callValue, + bytes memory methodData + ) = _tradeInfo.exchangeAdapter.getTradeCalldata( + _tradeInfo.sendToken, + _tradeInfo.receiveToken, + address(_tradeInfo.setToken), + _tradeInfo.isSendTokenFixed ? _tradeInfo.totalFixedQuantity : _tradeInfo.floatingQuantityLimit, + _tradeInfo.isSendTokenFixed ? _tradeInfo.floatingQuantityLimit : _tradeInfo.totalFixedQuantity, + tradeData + ); + + _tradeInfo.setToken.invoke(targetExchange, callValue, methodData); + } + + /** + * Retrieve fee from controller and calculate total protocol fee and send from SetToken to protocol recipient. + * The protocol fee is collected from the receiving token in the trade. + * + * @param _tradeInfo Struct containing trade information used in internal functions + * + * @return protocolFee Amount of receive token taken as protocol fee + */ + function _accrueProtocolFee(TradeInfo memory _tradeInfo) internal returns (uint256 protocolFee) { + + uint256 exchangedQuantity = IERC20(_tradeInfo.receiveToken).balanceOf(address(_tradeInfo.setToken)).sub(_tradeInfo.preTradeReceiveTokenBalance); + + protocolFee = getModuleFee(GENERAL_INDEX_MODULE_PROTOCOL_FEE_INDEX, exchangedQuantity); + + payProtocolFeeFromSetToken(_tradeInfo.setToken, _tradeInfo.receiveToken, protocolFee); + } + + /** + * Update SetToken positions. If this function is called after the fees have been accrued, + * it returns net amount of bought tokens. + * + * @param _tradeInfo Struct containing trade information used in internal functions + * + * @return sellAmount Amount of sendTokens used in the trade + * @return netBuyAmount Amount of receiveTokens received in the trade (net of fees) + */ + function _updatePositionState(TradeInfo memory _tradeInfo) internal returns (uint256 sellAmount, uint256 netBuyAmount) { + (uint256 postTradeSendTokenBalance,,) = _tradeInfo.setToken.calculateAndEditDefaultPosition( + _tradeInfo.sendToken, + _tradeInfo.setTotalSupply, + _tradeInfo.preTradeSendTokenBalance + ); + (uint256 postTradeReceiveTokenBalance,,) = _tradeInfo.setToken.calculateAndEditDefaultPosition( + _tradeInfo.receiveToken, + _tradeInfo.setTotalSupply, + _tradeInfo.preTradeReceiveTokenBalance + ); + + sellAmount = _tradeInfo.preTradeSendTokenBalance.sub(postTradeSendTokenBalance); + netBuyAmount = postTradeReceiveTokenBalance.sub(_tradeInfo.preTradeReceiveTokenBalance); + } + + /** + * Calculates the amount of a component is going to be traded and whether the component is being bought + * or sold. If currentUnit and targetUnit are the same, function will revert. + * + * @param _setToken Instance of the SetToken to rebalance + * @param _component IERC20 component to trade + * @param _totalSupply Total supply of _setToken + * + * @return isSendTokenFixed Boolean indicating if sendToken is fixed (if component is being sold) + * @return totalFixedQuantity Amount of fixed token to send or receive + */ + function _calculateTradeSizeAndDirection( + ISetToken _setToken, + IERC20 _component, + uint256 _totalSupply + ) + internal + view + returns (bool isSendTokenFixed, uint256 totalFixedQuantity) + { + uint256 protocolFee = controller.getModuleFee(address(this), GENERAL_INDEX_MODULE_PROTOCOL_FEE_INDEX); + uint256 componentMaxSize = executionInfo[_setToken][_component].maxSize; + + uint256 currentUnit = _setToken.getDefaultPositionRealUnit(address(_component)).toUint256(); + uint256 targetUnit = _getNormalizedTargetUnit(_setToken, _component); + + require(currentUnit != targetUnit, "Target already met"); + + uint256 currentNotional = _totalSupply.getDefaultTotalNotional(currentUnit); + uint256 targetNotional = _totalSupply.preciseMulCeil(targetUnit); + + isSendTokenFixed = targetNotional < currentNotional; + + totalFixedQuantity = isSendTokenFixed + ? componentMaxSize.min(currentNotional.sub(targetNotional)) + : componentMaxSize.min(targetNotional.sub(currentNotional).preciseDiv(PreciseUnitMath.preciseUnit().sub(protocolFee))); + } + + /** + * Check if there are any more tokens to sell. + * + * @param _setToken Instance of the SetToken to be rebalanced + * + * @return bool True if there is not any component that can be sold, otherwise false + */ + function _noTokensToSell(ISetToken _setToken) internal view returns (bool) { + uint256 positionMultiplier = rebalanceInfo[_setToken].positionMultiplier; + uint256 currentPositionMultiplier = _setToken.positionMultiplier().toUint256(); + address[] memory rebalanceComponents = rebalanceInfo[_setToken].rebalanceComponents; + for (uint256 i = 0; i < rebalanceComponents.length; i++) { + address component = rebalanceComponents[i]; + if (component != address(weth)) { + uint256 normalizedTargetUnit = _normalizeTargetUnit(_setToken, IERC20(component), currentPositionMultiplier, positionMultiplier); + bool canSell = normalizedTargetUnit < _setToken.getDefaultPositionRealUnit(component).toUint256(); + if (canSell) { return false; } + } + } + return true; + } + + /** + * Check if all targets are met. Due to small rounding errors converting between virtual and real unit on SetToken we allow + * for a 1 wei buffer. + * + * @param _setToken Instance of the SetToken to be rebalanced + * + * @return bool True if all component's target units have been met, otherwise false + */ + function _allTargetsMet(ISetToken _setToken) internal view returns (bool) { + uint256 positionMultiplier = rebalanceInfo[_setToken].positionMultiplier; + uint256 currentPositionMultiplier = _setToken.positionMultiplier().toUint256(); + + address[] memory rebalanceComponents = rebalanceInfo[_setToken].rebalanceComponents; + for (uint256 i = 0; i < rebalanceComponents.length; i++) { + address component = rebalanceComponents[i]; + if (component != address(weth)) { + uint256 normalizedTargetUnit = _normalizeTargetUnit(_setToken, IERC20(component), currentPositionMultiplier, positionMultiplier); + uint256 currentUnit = _setToken.getDefaultPositionRealUnit(component).toUint256(); + + bool targetUnmet; + if (normalizedTargetUnit > 0) { + targetUnmet = (normalizedTargetUnit.sub(1) > currentUnit || normalizedTargetUnit.add(1) < currentUnit); + } else { + targetUnmet = normalizedTargetUnit != _setToken.getDefaultPositionRealUnit(component).toUint256(); + } + + if (targetUnmet) { return false; } + } + } + return true; + } + + /** + * Normalize target unit to current position multiplier in case fees have been accrued. + * + * @param _setToken Instance of the SetToken to be rebalanced + * @param _component IERC20 component whose normalized target unit is required + * + * @return uint256 Normalized target unit of the component + */ + function _getNormalizedTargetUnit(ISetToken _setToken, IERC20 _component) internal view returns(uint256) { + uint256 currentPositionMultiplier = _setToken.positionMultiplier().toUint256(); + uint256 positionMultiplier = rebalanceInfo[_setToken].positionMultiplier; + return _normalizeTargetUnit(_setToken, _component, currentPositionMultiplier, positionMultiplier); + } + + /** + * Calculates the normalized target unit value. + * + * @param _setToken Instance of the SettToken to be rebalanced + * @param _component IERC20 component whose normalized target unit is required + * @param _currentPositionMultiplier Current position multiplier value + * @param _positionMultiplier Position multiplier value when rebalance started + * + * @return uint256 Normalized target unit of the component + */ + function _normalizeTargetUnit( + ISetToken _setToken, + IERC20 _component, + uint256 _currentPositionMultiplier, + uint256 _positionMultiplier + ) + internal + view + returns (uint256) + { + return executionInfo[_setToken][_component].targetUnit.mul(_currentPositionMultiplier).div(_positionMultiplier); + } + + /** + * Validate component position unit has not exceeded it's target unit. + * + * @param _setToken Instance of the SetToken + * @param _component IERC20 component whose position units are to be validated + */ + function _validateComponentPositionUnit(ISetToken _setToken, IERC20 _component) internal view { + uint256 currentUnit = _setToken.getDefaultPositionRealUnit(address(_component)).toUint256(); + uint256 targetUnit = _getNormalizedTargetUnit(_setToken, _component); + require(currentUnit <= targetUnit, "Can not exceed target unit"); + } + + /** + * Determine if passed address is allowed to call trade for the SetToken. + * If anyoneTrade set to true anyone can call otherwise needs to be approved. + * + * @param _setToken Instance of SetToken to be rebalanced + * @param _caller Address of the trader who called contract function + * + * @return bool True if caller is an approved trader for the SetToken + */ + function _isAllowedTrader(ISetToken _setToken, address _caller) internal view returns (bool) { + TradePermissionInfo storage permissions = permissionInfo[_setToken]; + return permissions.anyoneTrade || permissions.tradeAllowList[_caller]; + } + + /** + * Validate arrays are of equal length and not empty. + * + * @param _components Array of components + * @param _data Array of uint256 values + */ + function _validateUintArrays(address[] calldata _components, uint256[] calldata _data) internal pure { + require(_components.length == _data.length, "Array length mismatch"); + require(_components.length > 0, "Array length must be > 0"); + require(!_components.hasDuplicate(), "Cannot duplicate components"); + } +} \ No newline at end of file diff --git a/test/protocol/integration/uniswapV2ExchangeAdapterV2.spec.ts b/test/protocol/integration/uniswapV2ExchangeAdapterV2.spec.ts index 922b25977..00b6f4eca 100644 --- a/test/protocol/integration/uniswapV2ExchangeAdapterV2.spec.ts +++ b/test/protocol/integration/uniswapV2ExchangeAdapterV2.spec.ts @@ -92,22 +92,22 @@ describe("UniswapV2ExchangeAdapterV2", () => { describe("getUniswapExchangeData", async () => { let subjectPath: Address[]; - let subjectShouldTradeForExact: boolean; + let subjectShouldTradeExactTokensForTokens: boolean; beforeEach(async () => { subjectPath = [setup.weth.address, setup.wbtc.address, setup.dai.address]; - subjectShouldTradeForExact = true; + subjectShouldTradeExactTokensForTokens = true; }); async function subject(): Promise { - return await uniswapV2ExchangeAdapter.getUniswapExchangeData(subjectPath, subjectShouldTradeForExact); + return await uniswapV2ExchangeAdapter.getUniswapExchangeData(subjectPath, subjectShouldTradeExactTokensForTokens); } it("should return the correct data", async () => { const uniswapData = await subject(); const expectedData = defaultAbiCoder.encode( ["address[]", "bool"], - [subjectPath, subjectShouldTradeForExact] + [subjectPath, subjectShouldTradeExactTokensForTokens] ); expect(uniswapData).to.eq(expectedData); @@ -152,13 +152,13 @@ describe("UniswapV2ExchangeAdapterV2", () => { ); } - describe("when boolean to swap for exact tokens is false", async () => { + describe("when boolean to swap exact tokens for tokens is true", async () => { beforeEach(async () => { const path = [sourceAddress, setup.weth.address, destinationAddress]; - const shouldTradeForExact = false; + const shouldTradeExactTokensForTokens = true; subjectData = defaultAbiCoder.encode( ["address[]", "bool"], - [path, shouldTradeForExact] + [path, shouldTradeExactTokensForTokens] ); }); @@ -176,13 +176,13 @@ describe("UniswapV2ExchangeAdapterV2", () => { }); }); - describe("when boolean to swap for exact tokens is true", async () => { + describe("when boolean to swap exact tokens for tokens is false", async () => { beforeEach(async () => { const path = [sourceAddress, setup.weth.address, destinationAddress]; - const shouldTradeForExact = true; + const shouldTradeExactTokensForTokens = false; subjectData = defaultAbiCoder.encode( ["address[]", "bool"], - [path, shouldTradeForExact] + [path, shouldTradeExactTokensForTokens] ); }); diff --git a/test/protocol/modules/generalIndexModule.spec.ts b/test/protocol/modules/generalIndexModule.spec.ts new file mode 100644 index 000000000..fa6ada501 --- /dev/null +++ b/test/protocol/modules/generalIndexModule.spec.ts @@ -0,0 +1,2054 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { Address, StreamingFeeState } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO, MAX_UINT_256, PRECISE_UNIT, THREE, ZERO, ONE_DAY_IN_SECONDS } from "@utils/constants"; +import { BalancerV1ExchangeAdapter, ContractCallerMock, GeneralIndexModule, SetToken, UniswapV2ExchangeAdapterV2 } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + bitcoin, + ether, + preciseDiv, + preciseMul, + preciseMulCeil +} from "@utils/index"; +import { + cacheBeforeEach, + increaseTimeAsync, + getAccounts, + getBalancerFixture, + getLastBlockTimestamp, + getRandomAccount, + getRandomAddress, + getSystemFixture, + getUniswapFixture, + getWaffleExpect +} from "@utils/test/index"; +import { BalancerFixture, SystemFixture, UniswapFixture } from "@utils/fixtures"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("GeneralIndexModule", () => { + let owner: Account; + let trader: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let uniswapSetup: UniswapFixture; + let sushiswapSetup: UniswapFixture; + let balancerSetup: BalancerFixture; + + let index: SetToken; + let indexWithWeth: SetToken; + let indexModule: GeneralIndexModule; + + let balancerExchangeAdapter: BalancerV1ExchangeAdapter; + let balancerAdapterName: string; + let sushiswapExchangeAdapter: UniswapV2ExchangeAdapterV2; + let sushiswapAdapterName: string; + let uniswapExchangeAdapter: UniswapV2ExchangeAdapterV2; + let uniswapAdapterName: string; + + let indexComponents: Address[]; + let indexUnits: BigNumber[]; + let indexWithWethComponents: Address[]; + let indexWithWethUnits: BigNumber[]; + + const ONE_MINUTE_IN_SECONDS: BigNumber = BigNumber.from(60); + + before(async () => { + [ + owner, + trader, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + uniswapSetup = getUniswapFixture(owner.address); + sushiswapSetup = getUniswapFixture(owner.address); + balancerSetup = getBalancerFixture(owner.address); + + await setup.initialize(); + await uniswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address); + await sushiswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address); + await balancerSetup.initialize(owner, setup.weth, setup.wbtc, setup.dai); + + indexModule = await deployer.modules.deployGeneralIndexModule( + setup.controller.address, + setup.weth.address + ); + await setup.controller.addModule(indexModule.address); + + balancerExchangeAdapter = await deployer.modules.deployBalancerV1ExchangeAdapter(balancerSetup.exchange.address); + sushiswapExchangeAdapter = await deployer.modules.deployUniswapV2ExchangeAdapterV2(sushiswapSetup.router.address); + uniswapExchangeAdapter = await deployer.modules.deployUniswapV2ExchangeAdapterV2(uniswapSetup.router.address); + + balancerAdapterName = "BALANCER"; + sushiswapAdapterName = "SUSHISWAP"; + uniswapAdapterName = "UNISWAP"; + + + await setup.integrationRegistry.batchAddIntegration( + [indexModule.address, indexModule.address, indexModule.address], + [balancerAdapterName, sushiswapAdapterName, uniswapAdapterName], + [ + balancerExchangeAdapter.address, + sushiswapExchangeAdapter.address, + uniswapExchangeAdapter.address, + ] + ); + }); + + cacheBeforeEach(async () => { + indexComponents = [uniswapSetup.uni.address, setup.wbtc.address, setup.dai.address]; + indexUnits = [ether(86.9565217), bitcoin(.01111111), ether(100)]; + index = await setup.createSetToken( + indexComponents, + indexUnits, // $100 of each + [setup.issuanceModule.address, setup.streamingFeeModule.address, indexModule.address], + ); + + const feeSettings = { + feeRecipient: owner.address, + maxStreamingFeePercentage: ether(.1), + streamingFeePercentage: ether(.01), + lastStreamingFeeTimestamp: ZERO, + } as StreamingFeeState; + + await setup.streamingFeeModule.initialize(index.address, feeSettings); + await setup.issuanceModule.initialize(index.address, ADDRESS_ZERO); + + indexWithWethComponents = [uniswapSetup.uni.address, setup.wbtc.address, setup.dai.address, setup.weth.address]; + indexWithWethUnits = [ether(86.9565217), bitcoin(.01111111), ether(100), ether(0.434782609)]; + indexWithWeth = await setup.createSetToken( + indexWithWethComponents, + indexWithWethUnits, // $100 of each + [setup.issuanceModule.address, setup.streamingFeeModule.address, indexModule.address], + ); + + const feeSettingsForIndexWithWeth = { + feeRecipient: owner.address, + maxStreamingFeePercentage: ether(.1), + streamingFeePercentage: ether(.01), + lastStreamingFeeTimestamp: ZERO, + } as StreamingFeeState; + + await setup.streamingFeeModule.initialize(indexWithWeth.address, feeSettingsForIndexWithWeth); + await setup.issuanceModule.initialize(indexWithWeth.address, ADDRESS_ZERO); + + await setup.weth.connect(owner.wallet).approve(uniswapSetup.router.address, ether(2000)); + await uniswapSetup.uni.connect(owner.wallet).approve(uniswapSetup.router.address, ether(400000)); + await uniswapSetup.router.connect(owner.wallet).addLiquidity( + setup.weth.address, + uniswapSetup.uni.address, + ether(2000), + ether(400000), + ether(1485), + ether(173000), + owner.address, + MAX_UINT_256 + ); + + await setup.weth.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(1000)); + await setup.wbtc.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(26)); + await sushiswapSetup.router.addLiquidity( + setup.weth.address, + setup.wbtc.address, + ether(1000), + bitcoin(25.5555), + ether(999), + ether(25.3), + owner.address, + MAX_UINT_256 + ); + }); + + describe("#constructor", async () => { + it("should set all the parameters correctly", async () => { + const weth = await indexModule.weth(); + const controller = await indexModule.controller(); + + expect(weth).to.eq(setup.weth.address); + expect(controller).to.eq(setup.controller.address); + }); + }); + + describe("#initialize", async () => { + let subjectSetToken: SetToken; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + }); + + async function subject(): Promise { + indexModule = indexModule.connect(subjectCaller.wallet); + return indexModule.initialize(subjectSetToken.address); + } + + it("should enable the Module on the SetToken", async () => { + await subject(); + const isModuleEnabled = await subjectSetToken.isInitializedModule(indexModule.address); + expect(isModuleEnabled).to.eq(true); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the module is not pending", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.dai.address], + [ether(1)], + [indexModule.address], + owner.address + ); + + subjectSetToken = nonEnabledSetToken; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be controller-enabled SetToken"); + }); + }); + + describe("when set has weth as component", async () => { + beforeEach(async () => { + subjectSetToken = indexWithWeth; + }); + + it("should enable the Module on the SetToken", async () => { + await subject(); + const isModuleEnabled = await subjectSetToken.isInitializedModule(indexModule.address); + expect(isModuleEnabled).to.eq(true); + }); + }); + }); + + describe("when module is initalized", async () => { + let subjectSetToken: SetToken; + let subjectCaller: Account; + + let newComponents: Address[]; + let newTargetUnits: BigNumber[]; + let oldTargetUnits: BigNumber[]; + let issueAmount: BigNumber; + + async function initSetToken( + setToken: SetToken, components: Address[], tradeMaximums: BigNumber[], exchanges: string[], coolOffPeriods: BigNumber[] + ) { + await indexModule.initialize(setToken.address); + await indexModule.setTradeMaximums(setToken.address, components, tradeMaximums); + await indexModule.setExchanges(setToken.address, components, exchanges); + await indexModule.setCoolOffPeriods(setToken.address, components, coolOffPeriods); + await indexModule.updateTraderStatus(setToken.address, [trader.address], [true]); + } + + cacheBeforeEach(async () => { + // initialize indexModule on both SetTokens + await initSetToken( + index, + [uniswapSetup.uni.address, setup.wbtc.address, setup.dai.address, sushiswapSetup.uni.address], + [ether(800), bitcoin(.1), ether(1000), ether(500)], + [uniswapAdapterName, sushiswapAdapterName, balancerAdapterName, sushiswapAdapterName], + [ONE_MINUTE_IN_SECONDS.mul(3), ONE_MINUTE_IN_SECONDS, ONE_MINUTE_IN_SECONDS.mul(2), ONE_MINUTE_IN_SECONDS] + ); + + await initSetToken( + indexWithWeth, + [uniswapSetup.uni.address, setup.wbtc.address, setup.dai.address, setup.weth.address, sushiswapSetup.uni.address], + [ether(800), bitcoin(.1), ether(1000), ether(10000), ether(500)], + [uniswapAdapterName, sushiswapAdapterName, balancerAdapterName, "", sushiswapAdapterName], + [ONE_MINUTE_IN_SECONDS.mul(3), ONE_MINUTE_IN_SECONDS, ONE_MINUTE_IN_SECONDS.mul(2), ZERO, ONE_MINUTE_IN_SECONDS], + ); + }); + + describe("#startRebalance", async () => { + let subjectNewComponents: Address[]; + let subjectNewTargetUnits: BigNumber[]; + let subjectOldTargetUnits: BigNumber[]; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + + subjectNewComponents = [sushiswapSetup.uni.address]; + subjectNewTargetUnits = [ether(50)]; + subjectOldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02), ether(50)]; + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).startRebalance( + subjectSetToken.address, + subjectNewComponents, + subjectNewTargetUnits, + subjectOldTargetUnits, + await subjectSetToken.positionMultiplier() + ); + } + + it("should set target units and rebalance info correctly", async () => { + await subject(); + + const currentComponents = await subjectSetToken.getComponents(); + const aggregateComponents = [...currentComponents, ...subjectNewComponents]; + const aggregateTargetUnits = [...subjectOldTargetUnits, ...subjectNewTargetUnits]; + + for (let i = 0; i < aggregateComponents.length; i++) { + const targetUnit = (await indexModule.executionInfo(subjectSetToken.address, aggregateComponents[i])).targetUnit; + const exepectedTargetUnit = aggregateTargetUnits[i]; + expect(targetUnit).to.be.eq(exepectedTargetUnit); + } + + const rebalanceComponents = await indexModule.getRebalanceComponents(subjectSetToken.address); + const expectedRebalanceComponents = aggregateComponents; + for (let i = 0; i < rebalanceComponents.length; i++) { + expect(rebalanceComponents[i]).to.be.eq(expectedRebalanceComponents[i]); + } + + const positionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + const expectedPositionMultiplier = await subjectSetToken.positionMultiplier(); + + expect(positionMultiplier).to.be.eq(expectedPositionMultiplier); + }); + + describe("newComponents and newComponentsTargetUnits are not of same length", async () => { + beforeEach(async () => { + subjectNewTargetUnits = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when missing target units for old comoponents", async () => { + beforeEach(async () => { + subjectOldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("New allocation must have target for all old components"); + }); + }); + + describe("when newComponents contains an old component", async () => { + beforeEach(async () => { + subjectNewComponents = [sushiswapSetup.uni.address, uniswapSetup.uni.address]; + subjectNewTargetUnits = [ether(50), ether(50)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cannot duplicate components"); + }); + }); + }); + + describe("#setCoolOffPeriods", async () => { + let subjectComponents: Address[]; + let subjectCoolOffPeriods: BigNumber[]; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + subjectComponents = [uniswapSetup.uni.address, setup.wbtc.address]; + subjectCoolOffPeriods = [ONE_MINUTE_IN_SECONDS.mul(3), ONE_MINUTE_IN_SECONDS]; + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).setCoolOffPeriods( + subjectSetToken.address, + subjectComponents, + subjectCoolOffPeriods + ); + } + + it("should set values correctly", async () => { + await subject(); + + for (let i = 0; i < subjectComponents.length; i++) { + const coolOffPeriod = (await indexModule.executionInfo(subjectSetToken.address, subjectComponents[i])).coolOffPeriod; + const exepctedCoolOffPeriod = subjectCoolOffPeriods[i]; + expect(coolOffPeriod).to.be.eq(exepctedCoolOffPeriod); + } + }); + + describe("when array lengths are not same", async () => { + beforeEach(async () => { + subjectCoolOffPeriods = [ONE_MINUTE_IN_SECONDS.mul(3), ONE_MINUTE_IN_SECONDS, ONE_MINUTE_IN_SECONDS.mul(2)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when component array has duplilcate values", async () => { + beforeEach(async () => { + subjectComponents = [uniswapSetup.uni.address, setup.wbtc.address, uniswapSetup.uni.address]; + subjectCoolOffPeriods = [ONE_MINUTE_IN_SECONDS.mul(3), ONE_MINUTE_IN_SECONDS, ONE_MINUTE_IN_SECONDS.mul(3)]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cannot duplicate components"); + }); + }); + + describe("when array length is 0", async () => { + beforeEach(async () => { + subjectComponents = []; + subjectCoolOffPeriods = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length must be > 0"); + }); + }); + }); + + describe("#setTradeMaximums", async () => { + let subjectComponents: Address[]; + let subjectTradeMaximums: BigNumber[]; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + subjectComponents = [uniswapSetup.uni.address, setup.wbtc.address]; + subjectTradeMaximums = [ether(800), bitcoin(.1)]; + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).setTradeMaximums( + subjectSetToken.address, + subjectComponents, + subjectTradeMaximums + ); + } + + it("should set values correctly", async () => { + await subject(); + + for (let i = 0; i < subjectComponents.length; i++) { + const maxSize = (await indexModule.executionInfo(subjectSetToken.address, subjectComponents[i])).maxSize; + const exepctedMaxSize = subjectTradeMaximums[i]; + expect(maxSize).to.be.eq(exepctedMaxSize); + } + }); + }); + + describe("#setExchanges", async () => { + let subjectComponents: Address[]; + let subjectExchanges: string[]; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + subjectComponents = [uniswapSetup.uni.address, setup.wbtc.address]; + subjectExchanges = [uniswapAdapterName, sushiswapAdapterName]; + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).setExchanges(subjectSetToken.address, subjectComponents, subjectExchanges); + } + + it("should set values correctly", async () => { + await subject(); + + for (let i = 0; i < subjectComponents.length; i++) { + const exchangeName = (await indexModule.executionInfo(subjectSetToken.address, subjectComponents[i])).exchangeName; + const exepctedExchangeName = subjectExchanges[i]; + expect(exchangeName).to.be.eq(exepctedExchangeName); + } + }); + + describe("when array lengths are not same", async () => { + beforeEach(async () => { + subjectExchanges = [uniswapAdapterName, sushiswapAdapterName, balancerAdapterName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when component array has duplilcate values", async () => { + beforeEach(async () => { + subjectComponents = [uniswapSetup.uni.address, setup.wbtc.address, uniswapSetup.uni.address]; + subjectExchanges = [uniswapAdapterName, sushiswapAdapterName, uniswapAdapterName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cannot duplicate components"); + }); + }); + + describe("when component array has duplilcate values", async () => { + beforeEach(async () => { + subjectComponents = []; + subjectExchanges = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length must be > 0"); + }); + }); + + describe("when exchange name is empty string", async () => { + beforeEach(async () => { + subjectExchanges = [uniswapAdapterName, ""]; + }); + + describe("for component other than weth", async () => { + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Exchange name can not be an empty string"); + }); + }); + + describe("for weth", async () => { + beforeEach(async () => { + subjectComponents = [sushiswapSetup.uni.address, setup.weth.address]; + }); + + it("should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + }); + }); + }); + + describe("#trade", async () => { + let subjectComponent: Address; + let subjectIncreaseTime: BigNumber; + let subjectEthQuantityLimit: BigNumber; + + let expectedOut: BigNumber; + + before(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100), ZERO] + newComponents = []; + oldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02), ether(50)]; + newTargetUnits = []; + issueAmount = ether("20.000000000000000001"); + }); + + const startRebalance = async () => { + await setup.approveAndIssueSetToken(subjectSetToken, issueAmount); + await indexModule.startRebalance( + subjectSetToken.address, + newComponents, + newTargetUnits, + oldTargetUnits, + await index.positionMultiplier() + ); + }; + + const initializeSubjectVariables = () => { + subjectSetToken = index; + subjectCaller = trader; + subjectComponent = setup.dai.address; + subjectIncreaseTime = ONE_MINUTE_IN_SECONDS.mul(5); + subjectEthQuantityLimit = ZERO; + }; + + async function subject(): Promise { + await increaseTimeAsync(subjectIncreaseTime); + return await indexModule.connect(subjectCaller.wallet).trade( + subjectSetToken.address, + subjectComponent, + subjectEthQuantityLimit + ); + } + + describe("with default target units", async () => { + beforeEach(async () => { + initializeSubjectVariables(); + + expectedOut = (await balancerSetup.exchange.viewSplitExactIn( + setup.dai.address, + setup.weth.address, + ether(1000), + THREE + )).totalOutput; + subjectEthQuantityLimit = expectedOut; + }); + cacheBeforeEach(startRebalance); + + it("the position units and lastTradeTimestamp should be set as expected, sell using Balancer", async () => { + const currentDaiAmount = await setup.dai.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut), totalSupply); + const expectedDaiPositionUnits = preciseDiv(currentDaiAmount.sub(ether(1000)), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const daiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.dai.address)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(daiPositionUnits).to.eq(expectedDaiPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + it("emits the correct TradeExecuted event", async () => { + await expect(subject()).to.emit(indexModule, "TradeExecuted").withArgs( + subjectSetToken.address, + setup.dai.address, + setup.weth.address, + balancerExchangeAdapter.address, + trader.address, + ether(1000), + expectedOut, + ZERO + ); + }); + + describe("when there is a protcol fee charged", async () => { + let feePercentage: BigNumber; + + beforeEach(async () => { + feePercentage = ether(0.005); + setup.controller = setup.controller.connect(owner.wallet); + await setup.controller.addFee( + indexModule.address, + ZERO, // Fee type on trade function denoted as 0 + feePercentage // Set fee to 5 bps + ); + }); + + it("the position units and lastTradeTimestamp should be set as expected, sell using Balancer", async () => { + const currentDaiAmount = await setup.dai.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const protocolFee = expectedOut.mul(feePercentage).div(ether(1)); + const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut).sub(protocolFee), totalSupply); + const expectedDaiPositionUnits = preciseDiv(currentDaiAmount.sub(ether(1000)), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const daiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.dai.address)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(daiPositionUnits).to.eq(expectedDaiPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + it("the fees should be received by the fee recipient", async () => { + const feeRecipient = await setup.controller.feeRecipient(); + const beforeWethBalance = await setup.weth.balanceOf(feeRecipient); + + await subject(); + + const wethBalance = await setup.weth.balanceOf(feeRecipient); + + const protocolFee = expectedOut.mul(feePercentage).div(ether(1)); + const expectedWethBalance = beforeWethBalance.add(protocolFee); + + expect(wethBalance).to.eq(expectedWethBalance); + }); + + it("emits the correct TradeExecuted event", async () => { + const protocolFee = expectedOut.mul(feePercentage).div(ether(1)); + await expect(subject()).to.emit(indexModule, "TradeExecuted").withArgs( + subjectSetToken.address, + setup.dai.address, + setup.weth.address, + balancerExchangeAdapter.address, + trader.address, + ether(1000), + expectedOut.sub(protocolFee), + protocolFee + ); + }); + + describe("and the buy component does not meet the max trade size", async () => { + beforeEach(async () => { + await indexModule.startRebalance( + subjectSetToken.address, + [], + [], + [ether("60.869565780223716593"), bitcoin(.016), ether(50)], + await index.positionMultiplier() + ); + + await subject(); + + subjectComponent = setup.wbtc.address; + subjectEthQuantityLimit = MAX_UINT_256; + }); + + it("position units should match the target", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentWbtcUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const expectedWbtcSize = preciseDiv( + preciseMulCeil(bitcoin(.016), totalSupply).sub(preciseMul(currentWbtcUnit, totalSupply)), + PRECISE_UNIT.sub(feePercentage) + ); + + const [expectedIn, expectedOut] = await sushiswapSetup.router.getAmountsIn( + expectedWbtcSize, + [setup.weth.address, setup.wbtc.address] + ); + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + + const wethUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + + await subject(); + + const wbtcExcess = currentWbtcAmount.sub(preciseMul(totalSupply, wbtcUnit)); + const wethExcess = currentWethAmount.sub(preciseMul(totalSupply, wethUnit)); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.sub(expectedIn).sub(wethExcess), totalSupply); + const expectedWbtcPositionUnits = preciseDiv( + currentWbtcAmount.add(preciseMulCeil(expectedOut, PRECISE_UNIT.sub(feePercentage))).sub(wbtcExcess), + totalSupply + ); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + }); + }); + }); + + describe("when the component being sold doesn't meet the max trade size", async () => { + beforeEach(async () => { + subjectComponent = uniswapSetup.uni.address; + subjectEthQuantityLimit = ZERO; + }); + + it("the trade gets rounded down to meet the target", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentUniUnit = await subjectSetToken.getDefaultPositionRealUnit(uniswapSetup.uni.address); + const expectedUniSize = preciseMul(currentUniUnit.sub(ether("60.869565780223716593")), totalSupply); + + const [expectedIn, expectedOut] = await uniswapSetup.router.getAmountsOut( + expectedUniSize, + [uniswapSetup.uni.address, setup.weth.address] + ); + + const currentUniAmount = await uniswapSetup.uni.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut), totalSupply); + const expectedUniPositionUnits = preciseDiv(currentUniAmount.sub(expectedIn), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const uniPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(uniswapSetup.uni.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, uniswapSetup.uni.address)).lastTradeTimestamp; + + expect(uniPositionUnits).to.eq(expectedUniPositionUnits); + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + }); + + describe("when the component is being bought using Sushiswap", async () => { + beforeEach(async () => { + await subject(); + + subjectComponent = setup.wbtc.address; + subjectEthQuantityLimit = MAX_UINT_256; + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => {0; + const [expectedIn, expectedOut] = await sushiswapSetup.router.getAmountsIn( + bitcoin(.1), + [setup.weth.address, setup.wbtc.address] + ); + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + + const wethUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const wbtcExcess = currentWbtcAmount.sub(preciseMul(totalSupply, wbtcUnit)); + const wethExcess = currentWethAmount.sub(preciseMul(totalSupply, wethUnit)); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.sub(expectedIn).sub(wethExcess), totalSupply); + const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.add(expectedOut).sub(wbtcExcess), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.wbtc.address)).lastTradeTimestamp; + + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + it("emits the correct TradeExecuted event", async () => { + const [expectedIn, ] = await sushiswapSetup.router.getAmountsIn( + bitcoin(.1), + [setup.weth.address, setup.wbtc.address] + ); + await expect(subject()).to.emit(indexModule, "TradeExecuted").withArgs( + subjectSetToken.address, + setup.weth.address, + setup.wbtc.address, + sushiswapExchangeAdapter.address, + trader.address, + expectedIn, + bitcoin(.1), + ZERO + ); + }); + }); + + describe("when exchange doesn't return minimum receive eth amount, while selling component", async () => { + beforeEach(async () => { + expectedOut = (await balancerSetup.exchange.viewSplitExactIn( + setup.dai.address, + setup.weth.address, + ether(1000), + THREE + )).totalOutput; + subjectEthQuantityLimit = expectedOut.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.reverted; + }); + }); + + describe("when exchange takes more than maximum input eth amount, while buying component", async () => { + beforeEach(async () => { + subjectComponent = setup.wbtc.address; + const [expectedIn, ] = await sushiswapSetup.router.getAmountsOut( + bitcoin(.1), + [setup.wbtc.address, setup.weth.address] + ); + subjectEthQuantityLimit = expectedIn.div(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.reverted; + }); + }); + + describe("when anyoneTrade is true and a random address calls", async () => { + beforeEach(async () => { + await indexModule.updateAnyoneTrade(subjectSetToken.address, true); + subjectCaller = await getRandomAccount(); + }); + + it("the trade should not revert", async () => { + await expect(subject()).to.not.be.reverted; + }); + }); + + describe("when not enough time has elapsed between trades", async () => { + beforeEach(async () => { + await subject(); + subjectIncreaseTime = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cool off period has not elapsed."); + }); + }); + + describe("when correct exchange has not been set", async () => { + beforeEach(async () => { + await indexModule.setExchanges(subjectSetToken.address, [subjectComponent], ["BadExchangeName"]); + }); + + afterEach(async () => { + await indexModule.setExchanges(subjectSetToken.address, [subjectComponent], [balancerAdapterName]); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Must be valid adapter"); + }); + }); + + describe("when the passed component is not included in the rebalance", async () => { + beforeEach(async () => { + subjectComponent = sushiswapSetup.uni.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Passed component not included in rebalance"); + }); + }); + + describe("when the calling address is not a permissioned address", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Address not permitted to trade"); + }); + }); + + describe("when the component is weth", async() => { + beforeEach(async () => { + subjectComponent = setup.weth.address; + }); + + it("should revert", async () => { + expect(subject()).to.be.revertedWith("Can not explicitly trade WETH"); + }); + }); + + describe("when caller is a contract", async () => { + let subjectTarget: Address; + let subjectCallData: string; + let subjectValue: BigNumber; + + let contractCaller: ContractCallerMock; + + beforeEach(async () => { + contractCaller = await deployer.mocks.deployContractCallerMock(); + await indexModule.connect(owner.wallet).updateTraderStatus(subjectSetToken.address, [contractCaller.address], [true]); + + subjectTarget = indexModule.address; + subjectCallData = indexModule.interface.encodeFunctionData("trade", [subjectSetToken.address, subjectComponent, ZERO]); + subjectValue = ZERO; + }); + + async function subjectContractCaller(): Promise { + return await contractCaller.invoke( + subjectTarget, + subjectValue, + subjectCallData + ); + } + + it("should not revert", async () => { + await expect(subjectContractCaller()).to.not.be.reverted; + }); + + describe("when anyone trade is true", async () => { + beforeEach(async () => { + await indexModule.connect(owner.wallet).updateAnyoneTrade(subjectSetToken.address, true); + }); + + it("the trader reverts", async () => { + await expect(subjectContractCaller()).to.be.revertedWith("Caller must be EOA Address"); + }); + }); + }); + }); + + describe("with alternative target units", async () => { + before(async () => { + oldTargetUnits = [ether(100), ZERO, ether(185)]; + }); + + after(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + }); + + beforeEach(async () => { + initializeSubjectVariables(); + + expectedOut = (await balancerSetup.exchange.viewSplitExactIn( + setup.dai.address, + setup.weth.address, + ether(1000), + THREE + )).totalOutput; + subjectEthQuantityLimit = expectedOut; + }); + cacheBeforeEach(startRebalance); + + describe("when the sell happens on Sushiswap", async () => { + beforeEach(async () => { + subjectComponent = setup.wbtc.address; + subjectEthQuantityLimit = ZERO; + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const [expectedIn, expectedOut] = await sushiswapSetup.router.getAmountsOut( + bitcoin(.1), + [setup.wbtc.address, setup.weth.address] + ); + + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + + const wethUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const wbtcExcess = currentWbtcAmount.sub(preciseMul(totalSupply, wbtcUnit)); + const wethExcess = currentWethAmount.sub(preciseMul(totalSupply, wethUnit)); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut).sub(wethExcess), totalSupply); + const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.sub(expectedIn).sub(wbtcExcess), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.wbtc.address)).lastTradeTimestamp; + + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + describe("sell trade zeroes out the asset", async () => { + before(async () => { + oldTargetUnits = [ether(100), ZERO, ether(185)]; + }); + + beforeEach(async () => { + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, ZERO); + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, ZERO); + }); + + after(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + }); + + it("should remove the asset from the index", async () => { + await subject(); + + const components = await subjectSetToken.getComponents(); + const positionUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + + expect(components).to.not.contain(setup.wbtc.address); + expect(positionUnit).to.eq(ZERO); + }); + }); + }); + + describe("when the buy happens on Balancer", async () => { + beforeEach(async () => { + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, ZERO); + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, ZERO); + + subjectComponent = setup.dai.address; + subjectEthQuantityLimit = MAX_UINT_256; + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const expectedIn = (await balancerSetup.exchange.viewSplitExactOut( + setup.weth.address, + setup.dai.address, + ether(1000), + THREE + )).totalOutput; + const currentDaiAmount = await setup.dai.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + + const wethUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const daiUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const daiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.dai.address)).lastTradeTimestamp; + + const daiExcess = currentDaiAmount.sub(preciseMul(totalSupply, daiUnit)); + const wethExcess = currentWethAmount.sub(preciseMul(totalSupply, wethUnit)); + const expectedWethPositionUnits = preciseDiv( + currentWethAmount.sub(expectedIn).sub(wethExcess), + totalSupply + ); + const expectedDaiPositionUnits = preciseDiv( + currentDaiAmount.add(ether(1000)).sub(daiExcess), + totalSupply + ); + + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(daiPositionUnits).to.eq(expectedDaiPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + }); + }); + + describe("when alternative issue amount", async () => { + before(async () => { + issueAmount = ether(20); + }); + + after(async () => { + issueAmount = ether("20.000000000000000001"); + }); + + beforeEach(async () => { + initializeSubjectVariables(); + + expectedOut = (await balancerSetup.exchange.viewSplitExactIn( + setup.dai.address, + setup.weth.address, + ether(1000), + THREE + )).totalOutput; + subjectEthQuantityLimit = expectedOut; + }); + cacheBeforeEach(startRebalance); + + describe("when fees are accrued and target is met", async () => { + beforeEach(async () => { + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.dai.address, ZERO); + + await setup.streamingFeeModule.accrueFee(subjectSetToken.address); + }); + + it("the trade reverts", async () => { + const targetUnit = (await indexModule.executionInfo(subjectSetToken.address, setup.dai.address)).targetUnit; + const currentUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + + expect(targetUnit).to.not.eq(currentUnit); + await expect(subject()).to.be.revertedWith("Target already met"); + }); + }); + + describe("when the target has been met", async () => { + + beforeEach(async () => { + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.dai.address, ZERO); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Target already met"); + }); + }); + }); + + describe("when set has weth as component", async () => { + beforeEach(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100), ether(0.434782609), ZERO] + oldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02), ether(50), ether(0.434782609)]; + issueAmount = ether(20); + + expectedOut = (await balancerSetup.exchange.viewSplitExactIn( + setup.dai.address, + setup.weth.address, + ether(1000), + THREE + )).totalOutput; + subjectEthQuantityLimit = expectedOut; + + initializeSubjectVariables(); + subjectSetToken = indexWithWeth; + + await startRebalance(); + }); + + after(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100), ZERO] + oldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02), ether(50)]; + issueAmount = ether("20.000000000000000001"); + }); + + it("the position units and lastTradeTimestamp should be set as expected, sell using Balancer", async () => { + const currentDaiAmount = await setup.dai.balanceOf(subjectSetToken.address); + const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address); + const totalSupply = await subjectSetToken.totalSupply(); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut), totalSupply); + const expectedDaiPositionUnits = preciseDiv(currentDaiAmount.sub(ether(1000)), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const daiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, setup.dai.address)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(expectedWethPositionUnits); + expect(daiPositionUnits).to.eq(expectedDaiPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + }); + + describe("when adding a new asset", async () => { + before(async () => { + oldTargetUnits = [ether(100), ZERO, ether(185)]; + newComponents = [sushiswapSetup.uni.address]; + newTargetUnits = [ether(50)]; + }); + + beforeEach(async () => { + await setup.weth.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(1000)); + await sushiswapSetup.uni.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(200000)); + await sushiswapSetup.router.connect(owner.wallet).addLiquidity( + setup.weth.address, + sushiswapSetup.uni.address, + ether(1000), + ether(200000), + ether(800), + ether(100000), + owner.address, + MAX_UINT_256 + ); + + initializeSubjectVariables(); + subjectComponent = sushiswapSetup.uni.address; + subjectEthQuantityLimit = MAX_UINT_256; + + await startRebalance(); + + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, ZERO); + }); + + after(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + newComponents = []; + newTargetUnits = []; + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + const totalSupply = await subjectSetToken.totalSupply(); + const components = await subjectSetToken.getComponents(); + const expectedSushiPositionUnits = preciseDiv(ether(500), totalSupply); + + const sushiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(sushiswapSetup.uni.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, sushiswapSetup.uni.address)).lastTradeTimestamp; + + expect(components).to.contain(sushiswapSetup.uni.address); + expect(sushiPositionUnits).to.eq(expectedSushiPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + }); + }); + + describe("#tradeRemainingWETH", async () => { + let subjectComponent: Address; + let subjectIncreaseTime: BigNumber; + let subjectComponentQuantityLimit: BigNumber; + + before(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100)] + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + }); + + const startRebalanceAndTrade = async () => { + // oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + await setup.approveAndIssueSetToken(subjectSetToken, ether(20)); + await indexModule.startRebalance(subjectSetToken.address, [], [], oldTargetUnits, await subjectSetToken.positionMultiplier()); + + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.dai.address, ZERO); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, uniswapSetup.uni.address, ZERO); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, MAX_UINT_256); + }; + + const getFixedAmountIn = async (setToken: SetToken, component: Address, considerMaxSize: boolean = false) => { + const totalSupply = await setToken.totalSupply(); + const componentMaxSize = considerMaxSize ? (await indexModule.executionInfo(setToken.address, component)).maxSize : MAX_UINT_256; + const currentPositionMultiplier = await setToken.positionMultiplier(); + const positionMultiplier = (await indexModule.rebalanceInfo(setToken.address)).positionMultiplier; + + const currentUnit = await setToken.getDefaultPositionRealUnit(component); + const targetUnit = (await indexModule.executionInfo(setToken.address, component)).targetUnit; + const normalizedTargetUnit = targetUnit.mul(currentPositionMultiplier).div(positionMultiplier); + + const currentNotional = preciseMul(totalSupply, currentUnit); + const targetNotional = preciseMulCeil(totalSupply, normalizedTargetUnit); + + if (targetNotional.lt(currentNotional)) { + return componentMaxSize.lt(currentNotional.sub(targetNotional)) ? componentMaxSize : currentNotional.sub(targetNotional); + } else { + return componentMaxSize.lt(targetNotional.sub(currentNotional)) ? componentMaxSize : targetNotional.sub(currentNotional); + } + }; + + const initializeSubjectVariables = () => { + subjectCaller = trader; + subjectSetToken = index; + subjectComponent = setup.wbtc.address; + subjectIncreaseTime = ONE_MINUTE_IN_SECONDS.mul(5); + subjectComponentQuantityLimit = ZERO; + }; + + async function subject(): Promise { + await increaseTimeAsync(subjectIncreaseTime); + return await indexModule.connect(subjectCaller.wallet).tradeRemainingWETH( + subjectSetToken.address, + subjectComponent, + subjectComponentQuantityLimit + ); + } + + describe("with default target units", () => { + let wethAmountIn: BigNumber; + let expectedWbtcOut: BigNumber; + + beforeEach(initializeSubjectVariables); + cacheBeforeEach(startRebalanceAndTrade); + + describe("when ETH remaining in contract, trade remaining WETH", async () => { + beforeEach(async () => { + wethAmountIn = await getFixedAmountIn(subjectSetToken, setup.weth.address); + [, expectedWbtcOut] = await sushiswapSetup.router.getAmountsOut( + wethAmountIn, + [setup.weth.address, setup.wbtc.address] + ); + + subjectComponentQuantityLimit = expectedWbtcOut; + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.add(expectedWbtcOut), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, subjectComponent)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(ZERO); + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + it("emits the correct TradeExecuted event", async () => { + await expect(subject()).to.be.emit(indexModule, "TradeExecuted").withArgs( + subjectSetToken.address, + setup.weth.address, + subjectComponent, + sushiswapExchangeAdapter.address, + subjectCaller.wallet.address, + wethAmountIn, + expectedWbtcOut, + ZERO, + ); + }); + + describe("when protocol fees is charged", async () => { + let feePercentage: BigNumber; + + beforeEach(async () => { + feePercentage = ether(0.05); + setup.controller = setup.controller.connect(owner.wallet); + await setup.controller.addFee( + indexModule.address, + ZERO, // Fee type on trade function denoted as 0 + feePercentage // Set fee to 5 bps + ); + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const protocolFee = expectedWbtcOut.mul(feePercentage).div(ether(1)); + const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.add(expectedWbtcOut).sub(protocolFee), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, subjectComponent)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(ZERO); + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + it("the fees should be received by the fee recipient", async () => { + const feeRecipient = await setup.controller.feeRecipient(); + const beforeWbtcBalance = await setup.wbtc.balanceOf(feeRecipient); + + await subject(); + + const wbtcBalance = await setup.wbtc.balanceOf(feeRecipient); + + const protocolFee = expectedWbtcOut.mul(feePercentage).div(ether(1)); + const expectedWbtcBalance = beforeWbtcBalance.add(protocolFee); + + expect(wbtcBalance).to.eq(expectedWbtcBalance); + }); + + it("emits the correct TradeExecuted event", async () => { + const protocolFee = expectedWbtcOut.mul(feePercentage).div(ether(1)); + await expect(subject()).to.be.emit(indexModule, "TradeExecuted").withArgs( + subjectSetToken.address, + setup.weth.address, + subjectComponent, + sushiswapExchangeAdapter.address, + subjectCaller.wallet.address, + wethAmountIn, + expectedWbtcOut.sub(protocolFee), + protocolFee, + ); + }); + }); + + describe("when exchange returns amount less than subjectComponentQuantityLimit", async () => { + beforeEach(async () => { + [, expectedWbtcOut] = await sushiswapSetup.router.getAmountsOut( + wethAmountIn, + [setup.weth.address, setup.wbtc.address] + ); + subjectComponentQuantityLimit = expectedWbtcOut.mul(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.reverted; + }); + }); + + describe("when the target has been met and trading overshoots target unit", async () => { + beforeEach(async () => { + subjectComponent = setup.dai.address; + subjectComponentQuantityLimit = ZERO; + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Can not exceed target unit"); + }); + }); + + describe("when not enough time has elapsed between trades", async () => { + beforeEach(async () => { + subjectIncreaseTime = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cool off period has not elapsed."); + }); + }); + + describe("when the passed component is not included in rebalance components", async () => { + beforeEach(async () => { + subjectComponent = sushiswapSetup.uni.address; + subjectComponentQuantityLimit = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Passed component not included in rebalance"); + }); + }); + + describe("when the calling address is not a permissioned address", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Address not permitted to trade"); + }); + }); + + describe("when caller is a contract", async () => { + let subjectTarget: Address; + let subjectCallData: string; + let subjectValue: BigNumber; + + let contractCaller: ContractCallerMock; + + beforeEach(async () => { + contractCaller = await deployer.mocks.deployContractCallerMock(); + await indexModule.connect(owner.wallet).updateTraderStatus(subjectSetToken.address, [contractCaller.address], [true]); + + subjectTarget = indexModule.address; + subjectIncreaseTime = ONE_MINUTE_IN_SECONDS.mul(5); + subjectCallData = indexModule.interface.encodeFunctionData( + "tradeRemainingWETH", + [subjectSetToken.address, subjectComponent, subjectComponentQuantityLimit] + ); + subjectValue = ZERO; + }); + + async function subjectContractCaller(): Promise { + await increaseTimeAsync(subjectIncreaseTime); + return await contractCaller.invoke( + subjectTarget, + subjectValue, + subjectCallData + ); + } + + it("the trade reverts", async () => { + await expect(subjectContractCaller()).to.not.be.reverted; + }); + }); + }); + }); + + describe("with alternative target units", () => { + describe("when the value of WETH in index exceeds component trade size", async () => { + beforeEach(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.019), ether(50)]; + + initializeSubjectVariables(); + + await startRebalanceAndTrade(); + await indexModule.connect(owner.wallet).setTradeMaximums(subjectSetToken.address, [subjectComponent], [bitcoin(.01)]); + }); + + after(async () => { + await indexModule.connect(owner.wallet).setTradeMaximums( + subjectSetToken.address, + [subjectComponent], + [bitcoin(.1)] + ); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Trade amount exceeds max allowed trade size"); + }); + }); + + describe("when sellable components still remain", async () => { + beforeEach(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.019), ether(48)]; + initializeSubjectVariables(); + + await startRebalanceAndTrade(); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Sell other set components first"); + }); + }); + }); + + describe("when set has weth as component", async () => { + before(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50), ether(.434782609)]; + }); + + beforeEach(async () => { + initializeSubjectVariables(); + subjectSetToken = indexWithWeth; + + await startRebalanceAndTrade(); + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const wethAmountIn = await getFixedAmountIn(subjectSetToken, setup.weth.address); + const [, expectedWbtcOut] = await sushiswapSetup.router.getAmountsOut( + wethAmountIn, + [setup.weth.address, setup.wbtc.address] + ); + const totalSupply = await subjectSetToken.totalSupply(); + const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address); + + await subject(); + + const lastBlockTimestamp = await getLastBlockTimestamp(); + + const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.add(expectedWbtcOut), totalSupply); + + const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address); + const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const lastTrade = (await indexModule.executionInfo(subjectSetToken.address, subjectComponent)).lastTradeTimestamp; + + expect(wethPositionUnits).to.eq(ether(.434782609)); + expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits); + expect(lastTrade).to.eq(lastBlockTimestamp); + }); + + describe("when weth is below target unit", async () => { + before(async () => { + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50), ether(.8)]; + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("WETH is below target unit and can not be traded"); + }); + }); + }); + }); + + describe("#getComponentTradeQuantityAndDirection", async () => { + let subjectComponent: Address; + + let feePercentage: BigNumber; + + before(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100), ZERO] + newComponents = []; + oldTargetUnits = [ether("60.869565780223716593"), bitcoin(.02), ether(55)]; + newTargetUnits = []; + issueAmount = ether("20.000000000000000001"); + }); + + const startRebalance = async () => { + await setup.approveAndIssueSetToken(subjectSetToken, issueAmount); + await indexModule.startRebalance( + subjectSetToken.address, + newComponents, + newTargetUnits, + oldTargetUnits, + await index.positionMultiplier() + ); + }; + + const initializeSubjectVariables = () => { + subjectSetToken = index; + subjectComponent = setup.dai.address; + }; + + beforeEach(async () => { + initializeSubjectVariables(); + + await startRebalance(); + + feePercentage = ether(0.005); + setup.controller = setup.controller.connect(owner.wallet); + await setup.controller.addFee( + indexModule.address, + ZERO, // Fee type on trade function denoted as 0 + feePercentage // Set fee to 5 bps + ); + }); + + async function subject(): Promise { + return await indexModule.getComponentTradeQuantityAndDirection( + subjectSetToken.address, + subjectComponent + ); + } + + it("the position units and lastTradeTimestamp should be set as expected, sell using Balancer", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentDaiUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + const expectedDaiSize = preciseMul(currentDaiUnit, totalSupply).sub(preciseMul(ether(55), totalSupply)); + + const [ + isSell, + componentQuantity, + ] = await subject(); + + expect(componentQuantity).to.eq(expectedDaiSize); + expect(isSell).to.be.true; + }); + + describe("and the buy component does not meet the max trade size", async () => { + beforeEach(async () => { + await indexModule.startRebalance( + subjectSetToken.address, + [], + [], + [ether("60.869565780223716593"), bitcoin(.016), ether(50)], + await index.positionMultiplier() + ); + + subjectComponent = setup.wbtc.address; + }); + + it("the correct trade direction and size should be returned", async () => { + const totalSupply = await subjectSetToken.totalSupply(); + const currentWbtcUnit = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address); + const expectedWbtcSize = preciseDiv( + preciseMulCeil(bitcoin(.016), totalSupply).sub(preciseMul(currentWbtcUnit, totalSupply)), + PRECISE_UNIT.sub(feePercentage) + ); + + const [ + isSell, + componentQuantity, + ] = await subject(); + + expect(componentQuantity).to.eq(expectedWbtcSize); + expect(isSell).to.be.false; + }); + }); + }); + + describe("#updateRaiseTargetPercentage", async () => { + let subjectRaiseTargetPercentage: BigNumber; + + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + subjectRaiseTargetPercentage = ether("0.02"); + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).updateRaiseTargetPercentage( + subjectSetToken.address, + subjectRaiseTargetPercentage + ); + } + + it("updates raiseTargetPercentage", async () => { + await subject(); + const newRaiseTargetPercentage = (await indexModule.rebalanceInfo(subjectSetToken.address)).raiseTargetPercentage; + + expect(newRaiseTargetPercentage).to.eq(subjectRaiseTargetPercentage); + }); + + it("emits correct RaiseTargetPercentageUpdated event", async () => { + await expect(subject()).to.emit(indexModule, "RaiseTargetPercentageUpdated").withArgs( + subjectSetToken.address, + subjectRaiseTargetPercentage + ); + }); + + describe("when target percentage is 0", async () => { + beforeEach(async () => { + subjectRaiseTargetPercentage = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target percentage must be > 0"); + }); + }); + }); + + describe("#raiseAssetTargets", async () => { + before(async () => { + // current units [ether(86.9565217), bitcoin(.01111111), ether(100)] + oldTargetUnits = [ether(60.869565), bitcoin(.015), ether(50)]; + }); + + const startRebalance = async (trade: boolean = true, accrueFee: boolean = false) => { + await setup.approveAndIssueSetToken(subjectSetToken, ether(20)); + + if (accrueFee) { + await increaseTimeAsync(ONE_DAY_IN_SECONDS); + await setup.streamingFeeModule.accrueFee(subjectSetToken.address); + } + + await indexModule.startRebalance(subjectSetToken.address, [], [], oldTargetUnits, await subjectSetToken.positionMultiplier()); + + if (trade) { + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.dai.address, ZERO); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, uniswapSetup.uni.address, ZERO); + await indexModule.connect(trader.wallet).trade(subjectSetToken.address, setup.wbtc.address, MAX_UINT_256); + } + + await indexModule.updateRaiseTargetPercentage(subjectSetToken.address, ether(.0025)); + }; + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).raiseAssetTargets(subjectSetToken.address); + } + + const initialializeSubjectVariables = () => { + subjectSetToken = index; + subjectCaller = trader; + }; + + describe("with default target units", () => { + beforeEach(async () => { + initialializeSubjectVariables(); + }); + cacheBeforeEach(startRebalance); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const prePositionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + await subject(); + + const expectedPositionMultiplier = preciseDiv( + prePositionMultiplier, + PRECISE_UNIT.add(ether(.0025)) + ); + + const positionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + expect(positionMultiplier).to.eq(expectedPositionMultiplier); + }); + + describe("when the calling address is not a permissioned address", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Address not permitted to trade"); + }); + }); + }); + + describe("when the position multiplier is less than 1", () => { + beforeEach(async () => { + initialializeSubjectVariables(); + + await startRebalance(true, true); + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const prePositionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + await subject(); + + const expectedPositionMultiplier = preciseDiv( + prePositionMultiplier, + PRECISE_UNIT.add(ether(.0025)) + ); + + const positionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + expect(positionMultiplier).to.eq(expectedPositionMultiplier); + }); + }); + + describe("when protocol fees are charged", () => { + beforeEach(async () => { + const feePercentage = ether(0.005); + setup.controller = setup.controller.connect(owner.wallet); + await setup.controller.addFee( + indexModule.address, + ZERO, // Fee type on trade function denoted as 0 + feePercentage // Set fee to 5 bps + ); + + initialializeSubjectVariables(); + await startRebalance(true, true); + }); + + it("the position units and lastTradeTimestamp should be set as expected", async () => { + const prePositionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + await subject(); + + const expectedPositionMultiplier = preciseDiv( + prePositionMultiplier, + PRECISE_UNIT.add(ether(.0025)) + ); + + const positionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + expect(positionMultiplier).to.eq(expectedPositionMultiplier); + }); + }); + + describe("when a component is being removed", async () => { + beforeEach(async () => { + // current Units [ether(86.9565217), bitcoin(.01111111), ether(100)] + oldTargetUnits = [ether(60.869565), bitcoin(.015), ZERO]; + + initialializeSubjectVariables(); + + await indexModule.setTradeMaximums(subjectSetToken.address, [setup.dai.address], [ether(2000)]); + await startRebalance(); + }); + + it("the position units and lastTradeTimestamp should be set as expected and the unit should be zeroed out", async () => { + const prePositionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + + await subject(); + + const expectedPositionMultiplier = preciseDiv( + prePositionMultiplier, + PRECISE_UNIT.add(ether(.0025)) + ); + + const positionMultiplier = (await indexModule.rebalanceInfo(subjectSetToken.address)).positionMultiplier; + const daiUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address); + + expect(positionMultiplier).to.eq(expectedPositionMultiplier); + expect(daiUnits).to.eq(ZERO); + }); + }); + + describe("with alternative target units", async () => { + describe("when the target has been met and no ETH remains", async () => { + beforeEach(async () => { + // current Units [ether(86.9565217), bitcoin(.01111111), ether(100)] + oldTargetUnits = [ether(60.869565), bitcoin(.02), ether(50)]; + + initialializeSubjectVariables(); + await startRebalance(); + + await increaseTimeAsync(ONE_MINUTE_IN_SECONDS.mul(5)); + await indexModule.connect(trader.wallet).tradeRemainingWETH(subjectSetToken.address, setup.wbtc.address, ZERO); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Targets must be met and ETH remaining in order to raise target"); + }); + }); + + describe("when set has weth as a component", async () => { + describe("when the target has been met and ETH is below target unit", async () => { + beforeEach(async () => { + // current Units [ether(86.9565217), bitcoin(.01111111), ether(100), ether(0.434782609)] + oldTargetUnits = [ether(86.9565217), bitcoin(.01111111), ether(100), ether(0.5)]; + + subjectSetToken = indexWithWeth; + subjectCaller = trader; + + await startRebalance(false); + }); + + it("the trade reverts", async () => { + await expect(subject()).to.be.revertedWith("Targets must be met and ETH remaining in order to raise target"); + }); + }); + }); + }); + }); + + describe("#updateTraderStatus", async () => { + let subjectTraders: Address[]; + let subjectStatuses: boolean[]; + + beforeEach(async () => { + subjectCaller = owner; + subjectSetToken = index; + subjectTraders = [trader.address, await getRandomAddress(), await getRandomAddress()]; + subjectStatuses = [true, true, true]; + }); + + async function subject(): Promise { + return await indexModule.connect(subjectCaller.wallet).updateTraderStatus( + subjectSetToken.address, + subjectTraders, + subjectStatuses + ); + } + + it("the trader status should be flipped to true", async () => { + await subject(); + + const isTraderOne = await indexModule.getIsAllowedTrader(subjectSetToken.address, subjectTraders[0]); + const isTraderTwo = await indexModule.getIsAllowedTrader(subjectSetToken.address, subjectTraders[1]); + const isTraderThree = await indexModule.getIsAllowedTrader(subjectSetToken.address, subjectTraders[2]); + + expect(isTraderOne).to.be.true; + expect(isTraderTwo).to.be.true; + expect(isTraderThree).to.be.true; + }); + + it("should emit TraderStatusUpdated event", async () => { + await expect(subject()).to.emit(indexModule, "TraderStatusUpdated").withArgs( + subjectSetToken.address, + subjectTraders[0], + true + ); + }); + + describe("when array lengths don't match", async () => { + beforeEach(async () => { + subjectTraders = [trader.address, await getRandomAddress()]; + subjectStatuses = [false]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when traders are duplicated", async () => { + beforeEach(async () => { + subjectTraders = [trader.address, trader.address, await getRandomAddress()]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Cannot duplicate traders"); + }); + }); + + describe("when arrays are empty", async () => { + beforeEach(async () => { + subjectTraders = []; + subjectStatuses = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length must be > 0"); + }); + }); + + describe("when the caller is not the manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken has not initialized the module", async () => { + beforeEach(async () => { + await setup.controller.removeSet(index.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#removeModule", async () => { + beforeEach(async () => { + subjectSetToken = index; + subjectCaller = owner; + }); + async function subject(): Promise { + return subjectSetToken.connect(subjectCaller.wallet).removeModule(indexModule.address); + } + + it("should remove the module", async () => { + await subject(); + const isModuleEnabled = await subjectSetToken.isInitializedModule(indexModule.address); + expect(isModuleEnabled).to.eq(false); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/protocol/modules/tradeModule.spec.ts b/test/protocol/modules/tradeModule.spec.ts index fc6efbbaa..c25d31d57 100644 --- a/test/protocol/modules/tradeModule.spec.ts +++ b/test/protocol/modules/tradeModule.spec.ts @@ -872,8 +872,8 @@ describe("TradeModule", () => { subjectMinDestinationQuantity = destinationTokenQuantity.sub(ether(1)); // Receive a min of 32 WETH for 1 WBTC subjectAdapterName = uniswapAdapterV2Name; const tradePath = [subjectSourceToken, subjectDestinationToken]; - const shouldSwapForExactToken = false; - subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapForExactToken); + const shouldSwapExactTokenForToken = true; + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapExactTokenForToken); subjectCaller = manager; }); @@ -932,8 +932,8 @@ describe("TradeModule", () => { subjectMinDestinationQuantity = ether(1); subjectAdapterName = uniswapAdapterV2Name; const tradePath = [subjectSourceToken, subjectDestinationToken]; - const shouldSwapForExactToken = true; - subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapForExactToken); + const shouldSwapExactTokenForToken = false; + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapExactTokenForToken); subjectCaller = manager; }); @@ -960,8 +960,8 @@ describe("TradeModule", () => { subjectSetToken = setToken.address; subjectAdapterName = uniswapAdapterV2Name; const tradePath = [subjectSourceToken, setup.weth.address, subjectDestinationToken]; - const shouldSwapForExactToken = false; - subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapForExactToken); + const shouldSwapExactTokenForToken = true; + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapExactTokenForToken); subjectCaller = manager; }); @@ -989,8 +989,8 @@ describe("TradeModule", () => { subjectSetToken = setToken.address; subjectAdapterName = uniswapAdapterV2Name; const tradePath = [subjectSourceToken, setup.weth.address, subjectDestinationToken]; - const shouldSwapForExactToken = true; - subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapForExactToken); + const shouldSwapExactTokenForToken = false; + subjectData = await uniswapExchangeAdapterV2.getUniswapExchangeData(tradePath, shouldSwapExactTokenForToken); subjectCaller = manager; }); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index dc42b7feb..b8cc38858 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -9,6 +9,7 @@ export { AirdropModule } from "../../typechain/AirdropModule"; export { AmmAdapterMock } from "../../typechain/AmmAdapterMock"; export { AmmModule } from "../../typechain/AmmModule"; export { AssetLimitHook } from "../../typechain/AssetLimitHook"; +export { BalancerV1ExchangeAdapter } from "../../typechain/BalancerV1ExchangeAdapter"; export { BasicIssuanceModule } from "../../typechain/BasicIssuanceModule"; export { ClaimAdapterMock } from "../../typechain/ClaimAdapterMock"; export { ClaimModule } from "../../typechain/ClaimModule"; @@ -29,6 +30,7 @@ export { DebtModuleMock } from "../../typechain/DebtModuleMock"; export { DelegateRegistry } from "../../typechain/DelegateRegistry"; export { ExplicitERC20Mock } from "../../typechain/ExplicitERC20Mock"; export { GaugeControllerMock } from "../../typechain/GaugeControllerMock"; +export { GeneralIndexModule } from "../../typechain/GeneralIndexModule"; export { GodModeMock } from "../../typechain/GodModeMock"; export { GovernanceAdapterMock } from "../../typechain/GovernanceAdapterMock"; export { GovernanceModule } from "../../typechain/GovernanceModule"; diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index 24aa990ab..46a50b841 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -4,11 +4,13 @@ import { convertLibraryNameToLinkId } from "../common"; import { AirdropModule, AmmModule, + BalancerV1ExchangeAdapter, BasicIssuanceModule, ClaimModule, CompoundLeverageModule, CustomOracleNavIssuanceModule, DebtIssuanceModule, + GeneralIndexModule, GovernanceModule, IssuanceModule, NavIssuanceModule, @@ -17,17 +19,20 @@ import { StreamingFeeModule, TradeModule, UniswapYieldStrategy, - WrapModule + UniswapV2ExchangeAdapterV2, + WrapModule } from "../contracts"; import { Address } from "../types"; import { AirdropModule__factory } from "../../typechain/factories/AirdropModule__factory"; import { AmmModule__factory } from "../../typechain/factories/AmmModule__factory"; +import { BalancerV1ExchangeAdapter__factory } from "../../typechain/factories/BalancerV1ExchangeAdapter__factory"; import { BasicIssuanceModule__factory } from "../../typechain/factories/BasicIssuanceModule__factory"; import { ClaimModule__factory } from "../../typechain/factories/ClaimModule__factory"; import { CompoundLeverageModule__factory } from "../../typechain/factories/CompoundLeverageModule__factory"; import { CustomOracleNavIssuanceModule__factory } from "../../typechain/factories/CustomOracleNavIssuanceModule__factory"; import { DebtIssuanceModule__factory } from "../../typechain/factories/DebtIssuanceModule__factory"; +import { GeneralIndexModule__factory } from "../../typechain/factories/GeneralIndexModule__factory"; import { GovernanceModule__factory } from "../../typechain/factories/GovernanceModule__factory"; import { IssuanceModule__factory } from "../../typechain/factories/IssuanceModule__factory"; import { NavIssuanceModule__factory } from "../../typechain/factories/NavIssuanceModule__factory"; @@ -36,6 +41,7 @@ import { StakingModule__factory } from "../../typechain/factories/StakingModule_ import { StreamingFeeModule__factory } from "../../typechain/factories/StreamingFeeModule__factory"; import { TradeModule__factory } from "../../typechain/factories/TradeModule__factory"; import { UniswapYieldStrategy__factory } from "../../typechain/factories/UniswapYieldStrategy__factory"; +import { UniswapV2ExchangeAdapterV2__factory } from "../../typechain/factories/UniswapV2ExchangeAdapterV2__factory"; import { WrapModule__factory } from "../../typechain/factories/WrapModule__factory"; export default class DeployModules { @@ -147,6 +153,28 @@ export default class DeployModules { ); } + public async deployGeneralIndexModule( + controller: Address, + weth: Address + ): Promise { + return await new GeneralIndexModule__factory(this._deployerSigner).deploy( + controller, + weth + ); + } + + public async deployUniswapV2ExchangeAdapterV2(router: Address): Promise { + return await new UniswapV2ExchangeAdapterV2__factory(this._deployerSigner).deploy( + router + ); + } + + public async deployBalancerV1ExchangeAdapter(balancerProxy: Address): Promise { + return await new BalancerV1ExchangeAdapter__factory(this._deployerSigner).deploy( + balancerProxy + ) + } + public async deployGovernanceModule(controller: Address): Promise { return await new GovernanceModule__factory(this._deployerSigner).deploy(controller); }