From 7202ed7d335c107d51efa5c081fcd68517c310d5 Mon Sep 17 00:00:00 2001 From: christn Date: Mon, 7 Oct 2024 13:18:15 +0200 Subject: [PATCH] feat: Morpho leverage extension (#185) * First version of MorphoLeverageStrategyExtension * Adjust calculation of liquidation thresholds etc * Set up tests for Morpho Leverage Extension * add engage tests * Fix some engage tests * rebalance tests * Simulate price change in rebalance * All rebalance tests passing * Add iterateRebalance tests * Add ripcord tests * Add disengage tests * fix failing tests * tests for setter methods * test shouldRebalance * test shouldRebalanceWithBounds * test #getChunkRebalanceNotional * fix open TODOs * Minor code cleanup in smart contract * Adjust docstring on _calculateMaxBorrowCollateral method * Adjust more comments * Adjust collateralValue to be correctly denominated in borrow asset units * Remove unnecessary sync call from test * Cosmetic changes to smart contract code --- .../MorphoLeverageStrategyExtension.sol | 1291 ++++ .../interfaces/IMorphoLeverageModule.sol | 38 + contracts/interfaces/IMorphoOracle.sol | 14 + .../external/IChainlinkEACAggregatorProxy.sol | 84 + external/abi/set/Morpho.json | 325 + external/abi/set/MorphoLeverageModule.json | 766 +++ .../aaveV3LeverageStrategyExtension.spec.ts | 1 - .../morphoLeverageStrategyExtension.spec.ts | 5237 +++++++++++++++++ utils/config.ts | 2 +- utils/deploys/deployExtensions.ts | 49 +- utils/deploys/deploySetV2.ts | 58 +- utils/test/index.ts | 1 + utils/test/testingUtils.ts | 10 + 13 files changed, 7850 insertions(+), 26 deletions(-) create mode 100644 contracts/adapters/MorphoLeverageStrategyExtension.sol create mode 100644 contracts/interfaces/IMorphoLeverageModule.sol create mode 100644 contracts/interfaces/IMorphoOracle.sol create mode 100644 contracts/interfaces/external/IChainlinkEACAggregatorProxy.sol create mode 100644 external/abi/set/Morpho.json create mode 100644 external/abi/set/MorphoLeverageModule.json create mode 100644 test/integration/ethereum/morphoLeverageStrategyExtension.spec.ts diff --git a/contracts/adapters/MorphoLeverageStrategyExtension.sol b/contracts/adapters/MorphoLeverageStrategyExtension.sol new file mode 100644 index 000000000..32bbf35c0 --- /dev/null +++ b/contracts/adapters/MorphoLeverageStrategyExtension.sol @@ -0,0 +1,1291 @@ +/* + Copyright 2024 Index Coop + + 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 { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Math } from "@openzeppelin/contracts/math/Math.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { BaseExtension } from "../lib/BaseExtension.sol"; +import { IBaseManager } from "../interfaces/IBaseManager.sol"; +import { IMorphoLeverageModule } from "../interfaces/IMorphoLeverageModule.sol"; +import { IMorphoOracle } from "../interfaces/IMorphoOracle.sol"; +import { IMorpho } from "../interfaces/IMorpho.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; +import { StringArrayUtils } from "../lib/StringArrayUtils.sol"; + + + +/** + * @title MorphoLeverageStrategyExtension + * @author Index Coop + * + * Smart contract that enables trustless leverage tokens. This extension is paired with the MorphoLeverageModule where module + * interactions are invoked via the IBaseManager contract. Any leveraged token can be constructed as long as there is + * a Morpho Market available with the desired collateral and borrow asset. + * This extension contract also allows the operator to set an ETH reward to incentivize keepers calling the rebalance + * function at different leverage thresholds. + * + */ +contract MorphoLeverageStrategyExtension is BaseExtension { + using Address for address; + using PreciseUnitMath for uint256; + using SafeMath for uint256; + using SafeCast for int256; + using StringArrayUtils for string[]; + + uint256 constant public MORPHO_ORACLE_PRICE_SCALE = 1e36; + + /* ============ Enums ============ */ + + enum ShouldRebalance { + NONE, // Indicates no rebalance action can be taken + REBALANCE, // Indicates rebalance() function can be successfully called + ITERATE_REBALANCE, // Indicates iterateRebalance() function can be successfully called + RIPCORD // Indicates ripcord() function can be successfully called + } + + /* ============ Structs ============ */ + + struct ActionInfo { + uint256 collateralBalance; // Balance of underlying held in Morpho in base units (e.g. USDC 10e6) + uint256 borrowBalance; // Balance of underlying borrowed from Morpho in base units + uint256 collateralValue; // Valuation of collateral in borrow asset base units + uint256 collateralPrice; // Price of collateral relative to borrow asset as returned by morpho oracle + uint256 setTotalSupply; // Total supply of SetToken + uint256 lltv; // Liquidation loan to value ratio of the morpho market + } + + struct LeverageInfo { + ActionInfo action; + uint256 currentLeverageRatio; // Current leverage ratio of Set + uint256 slippageTolerance; // Allowable percent trade slippage in preciseUnits (1% = 10^16) + uint256 twapMaxTradeSize; // Max trade size in collateral units allowed for rebalance action + string exchangeName; // Exchange to use for trade + } + + struct ContractSettings { + ISetToken setToken; // Instance of leverage token + IMorphoLeverageModule leverageModule; // Instance of Morpho leverage module + address collateralAsset; // Address of underlying collateral + address borrowAsset; // Address of underlying borrow asset + } + + struct MethodologySettings { + uint256 targetLeverageRatio; // Long term target ratio in precise units (10e18) + uint256 minLeverageRatio; // In precise units (10e18). If current leverage is below, rebalance target is this ratio + uint256 maxLeverageRatio; // In precise units (10e18). If current leverage is above, rebalance target is this ratio + uint256 recenteringSpeed; // % at which to rebalance back to target leverage in precise units (10e18) + uint256 rebalanceInterval; // Period of time required since last rebalance timestamp in seconds + } + + struct ExecutionSettings { + uint256 unutilizedLeveragePercentage; // Percent of max borrow left unutilized in precise units (1% = 10e16) + uint256 slippageTolerance; // % in precise units to price min token receive amount from trade quantities + uint256 twapCooldownPeriod; // Cooldown period required since last trade timestamp in seconds + } + + struct ExchangeSettings { + uint256 twapMaxTradeSize; // Max trade size in collateral base units + uint256 exchangeLastTradeTimestamp; // Timestamp of last trade made with this exchange + uint256 incentivizedTwapMaxTradeSize; // Max trade size for incentivized rebalances in collateral base units + bytes leverExchangeData; // Arbitrary exchange data passed into rebalance function for levering up + bytes deleverExchangeData; // Arbitrary exchange data passed into rebalance function for delevering + } + + struct IncentiveSettings { + uint256 etherReward; // ETH reward for incentivized rebalances + uint256 incentivizedLeverageRatio; // Leverage ratio for incentivized rebalances + uint256 incentivizedSlippageTolerance; // Slippage tolerance percentage for incentivized rebalances + uint256 incentivizedTwapCooldownPeriod; // TWAP cooldown in seconds for incentivized rebalances + } + + /* ============ Events ============ */ + + event Engaged( + uint256 _currentLeverageRatio, + uint256 _newLeverageRatio, + uint256 _chunkRebalanceNotional, + uint256 _totalRebalanceNotional + ); + event Rebalanced( + uint256 _currentLeverageRatio, + uint256 _newLeverageRatio, + uint256 _chunkRebalanceNotional, + uint256 _totalRebalanceNotional + ); + event RebalanceIterated( + uint256 _currentLeverageRatio, + uint256 _newLeverageRatio, + uint256 _chunkRebalanceNotional, + uint256 _totalRebalanceNotional + ); + event RipcordCalled( + uint256 _currentLeverageRatio, + uint256 _newLeverageRatio, + uint256 _rebalanceNotional, + uint256 _etherIncentive + ); + event Disengaged( + uint256 _currentLeverageRatio, + uint256 _newLeverageRatio, + uint256 _chunkRebalanceNotional, + uint256 _totalRebalanceNotional + ); + event MethodologySettingsUpdated( + uint256 _targetLeverageRatio, + uint256 _minLeverageRatio, + uint256 _maxLeverageRatio, + uint256 _recenteringSpeed, + uint256 _rebalanceInterval + ); + event ExecutionSettingsUpdated( + uint256 _unutilizedLeveragePercentage, + uint256 _twapCooldownPeriod, + uint256 _slippageTolerance + ); + event IncentiveSettingsUpdated( + uint256 _etherReward, + uint256 _incentivizedLeverageRatio, + uint256 _incentivizedSlippageTolerance, + uint256 _incentivizedTwapCooldownPeriod + ); + event ExchangeUpdated( + string _exchangeName, + uint256 twapMaxTradeSize, + uint256 exchangeLastTradeTimestamp, + uint256 incentivizedTwapMaxTradeSize, + bytes leverExchangeData, + bytes deleverExchangeData + ); + event ExchangeAdded( + string _exchangeName, + uint256 twapMaxTradeSize, + uint256 exchangeLastTradeTimestamp, + uint256 incentivizedTwapMaxTradeSize, + bytes leverExchangeData, + bytes deleverExchangeData + ); + event ExchangeRemoved( + string _exchangeName + ); + + /* ============ Modifiers ============ */ + + /** + * Throws if rebalance is currently in TWAP` can be overriden by the operator + */ + modifier noRebalanceInProgress() { + if(!overrideNoRebalanceInProgress) { + require(twapLeverageRatio == 0, "Rebalance is currently in progress"); + } + _; + } + + /* ============ State Variables ============ */ + + bool public overrideNoRebalanceInProgress; // Manager controlled flag that allows bypassing the noRebalanceInProgress modifier + ContractSettings internal strategy; // Struct of contracts used in the strategy (SetToken, price oracles, leverage module etc) + MethodologySettings internal methodology; // Struct containing methodology parameters + ExecutionSettings internal execution; // Struct containing execution parameters + mapping(string => ExchangeSettings) internal exchangeSettings; // Mapping from exchange name to exchange settings + IncentiveSettings internal incentive; // Struct containing incentive parameters for ripcord + string[] public enabledExchanges; // Array containing enabled exchanges + uint256 public twapLeverageRatio; // Stored leverage ratio to keep track of target between TWAP rebalances + uint256 public globalLastTradeTimestamp; // Last rebalance timestamp. Current timestamp must be greater than this variable + rebalance interval to rebalance + + /* ============ Constructor ============ */ + + /** + * Instantiate addresses, methodology parameters, execution parameters, and incentive parameters. + * + * @param _manager Address of IBaseManager contract + * @param _strategy Struct of contract addresses + * @param _methodology Struct containing methodology parameters + * @param _execution Struct containing execution parameters + * @param _incentive Struct containing incentive parameters for ripcord + * @param _exchangeNames List of initial exchange names + * @param _exchangeSettings List of structs containing exchange parameters for the initial exchanges + */ + constructor( + IBaseManager _manager, + ContractSettings memory _strategy, + MethodologySettings memory _methodology, + ExecutionSettings memory _execution, + IncentiveSettings memory _incentive, + string[] memory _exchangeNames, + ExchangeSettings[] memory _exchangeSettings + ) + public + BaseExtension(_manager) + { + strategy = _strategy; + methodology = _methodology; + execution = _execution; + incentive = _incentive; + + for (uint256 i = 0; i < _exchangeNames.length; i++) { + _validateExchangeSettings(_exchangeSettings[i]); + exchangeSettings[_exchangeNames[i]] = _exchangeSettings[i]; + enabledExchanges.push(_exchangeNames[i]); + } + + _validateNonExchangeSettings(methodology, execution, incentive); + } + + /* ============ External Functions ============ */ + + /** + * OPERATOR ONLY: Enable/Disable override of noRebalanceInProgress modifier + * + * @param _overrideNoRebalanceInProgress Boolean indicating wether to enable / disable override + */ + function setOverrideNoRebalanceInProgress(bool _overrideNoRebalanceInProgress) external onlyOperator { + overrideNoRebalanceInProgress = _overrideNoRebalanceInProgress; + } + + /** + * OPERATOR ONLY: Engage to target leverage ratio for the first time. SetToken will borrow debt position from Morpho and trade for collateral asset. If target + * leverage ratio is above max borrow or max trade size, then TWAP is kicked off. To complete engage if TWAP, any valid caller must call iterateRebalance until target + * is met. + * + * @param _exchangeName the exchange used for trading + */ + function engage(string memory _exchangeName) external onlyOperator { + _enterCollateralPosition(); + + ActionInfo memory engageInfo = _createActionInfo(); + require(engageInfo.setTotalSupply > 0, "SetToken must have > 0 supply"); + require(engageInfo.collateralBalance > 0, "Collateral balance must be > 0"); + require(engageInfo.borrowBalance == 0, "Debt must be 0"); + + LeverageInfo memory leverageInfo = LeverageInfo({ + action: engageInfo, + currentLeverageRatio: PreciseUnitMath.preciseUnit(), // 1x leverage in precise units + slippageTolerance: execution.slippageTolerance, + twapMaxTradeSize: exchangeSettings[_exchangeName].twapMaxTradeSize, + exchangeName: _exchangeName + }); + + // Calculate total rebalance units and kick off TWAP if above max borrow or max trade size + ( + uint256 chunkRebalanceNotional, + uint256 totalRebalanceNotional + ) = _calculateChunkRebalanceNotional(leverageInfo, methodology.targetLeverageRatio, true); + + + _lever(leverageInfo, chunkRebalanceNotional); + + _updateRebalanceState( + chunkRebalanceNotional, + totalRebalanceNotional, + methodology.targetLeverageRatio, + _exchangeName + ); + + emit Engaged( + leverageInfo.currentLeverageRatio, + methodology.targetLeverageRatio, + chunkRebalanceNotional, + totalRebalanceNotional + ); + } + + /** + * ONLY EOA AND ALLOWED CALLER: Rebalance product. If current leverage ratio is between the max and min bounds, then rebalance + * can only be called once the rebalance interval has elapsed since last timestamp. If outside the max and min, rebalance can be called anytime to bring leverage + * ratio back to the max or min bounds. The methodology will determine whether to delever or lever. + * + * Note: If the calculated current leverage ratio is above the incentivized leverage ratio or in TWAP then rebalance cannot be called. Instead, you must call + * ripcord() which is incentivized with a reward in Ether or iterateRebalance(). + * + * @param _exchangeName the exchange used for trading + */ + function rebalance(string memory _exchangeName) external onlyEOA onlyAllowedCaller(msg.sender) { + LeverageInfo memory leverageInfo = _getAndValidateLeveragedInfo( + execution.slippageTolerance, + exchangeSettings[_exchangeName].twapMaxTradeSize, + _exchangeName + ); + + // use globalLastTradeTimestamps to prevent multiple rebalances being called with different exchanges during the epoch rebalance + _validateNormalRebalance(leverageInfo, methodology.rebalanceInterval, globalLastTradeTimestamp); + _validateNonTWAP(); + + uint256 newLeverageRatio = _calculateNewLeverageRatio(leverageInfo.currentLeverageRatio); + + ( + uint256 chunkRebalanceNotional, + uint256 totalRebalanceNotional + ) = _handleRebalance(leverageInfo, newLeverageRatio); + + _updateRebalanceState(chunkRebalanceNotional, totalRebalanceNotional, newLeverageRatio, _exchangeName); + + emit Rebalanced( + leverageInfo.currentLeverageRatio, + newLeverageRatio, + chunkRebalanceNotional, + totalRebalanceNotional + ); + } + + /** + * ONLY EOA AND ALLOWED CALLER: Iterate a rebalance when in TWAP. TWAP cooldown period must have elapsed. If price moves advantageously, then exit without rebalancing + * and clear TWAP state. This function can only be called when below incentivized leverage ratio and in TWAP state. + * + * @param _exchangeName the exchange used for trading + */ + function iterateRebalance(string memory _exchangeName) external onlyEOA onlyAllowedCaller(msg.sender) { + LeverageInfo memory leverageInfo = _getAndValidateLeveragedInfo( + execution.slippageTolerance, + exchangeSettings[_exchangeName].twapMaxTradeSize, + _exchangeName + ); + + // Use the exchangeLastTradeTimestamp since cooldown periods are measured on a per-exchange basis, allowing it to rebalance multiple time in quick + // succession with different exchanges + _validateNormalRebalance(leverageInfo, execution.twapCooldownPeriod, exchangeSettings[_exchangeName].exchangeLastTradeTimestamp); + _validateTWAP(); + + uint256 chunkRebalanceNotional; + uint256 totalRebalanceNotional; + if (!_isAdvantageousTWAP(leverageInfo.currentLeverageRatio)) { + (chunkRebalanceNotional, totalRebalanceNotional) = _handleRebalance(leverageInfo, twapLeverageRatio); + } + + // If not advantageous, then rebalance is skipped and chunk and total rebalance notional are both 0, which means TWAP state is + // cleared + _updateIterateState(chunkRebalanceNotional, totalRebalanceNotional, _exchangeName); + + emit RebalanceIterated( + leverageInfo.currentLeverageRatio, + twapLeverageRatio, + chunkRebalanceNotional, + totalRebalanceNotional + ); + } + + /** + * ONLY EOA: In case the current leverage ratio exceeds the incentivized leverage threshold, the ripcord function can be called by anyone to return leverage ratio + * back to the max leverage ratio. This function typically would only be called during times of high downside volatility and / or normal keeper malfunctions. The caller + * of ripcord() will receive a reward in Ether. The ripcord function uses it's own TWAP cooldown period, slippage tolerance and TWAP max trade size which are typically + * looser than in regular rebalances. + * + * @param _exchangeName the exchange used for trading + */ + function ripcord(string memory _exchangeName) external onlyEOA { + LeverageInfo memory leverageInfo = _getAndValidateLeveragedInfo( + incentive.incentivizedSlippageTolerance, + exchangeSettings[_exchangeName].incentivizedTwapMaxTradeSize, + _exchangeName + ); + + // Use the exchangeLastTradeTimestamp so it can ripcord quickly with multiple exchanges + _validateRipcord(leverageInfo, exchangeSettings[_exchangeName].exchangeLastTradeTimestamp); + + ( uint256 chunkRebalanceNotional, ) = _calculateChunkRebalanceNotional(leverageInfo, methodology.maxLeverageRatio, false); + + _delever(leverageInfo, chunkRebalanceNotional); + + _updateRipcordState(_exchangeName); + + uint256 etherTransferred = _transferEtherRewardToCaller(incentive.etherReward); + + emit RipcordCalled( + leverageInfo.currentLeverageRatio, + methodology.maxLeverageRatio, + chunkRebalanceNotional, + etherTransferred + ); + } + + /** + * OPERATOR ONLY: Return leverage ratio to 1x and delever to repay loan. This can be used for upgrading or shutting down the strategy. SetToken will redeem + * collateral position and trade for debt position to repay Morpho. If the chunk rebalance size is less than the total notional size, then this function will + * delever and repay entire borrow balance on Morpho. If chunk rebalance size is above max borrow or max trade size, then operator must + * continue to call this function to complete repayment of loan. The function iterateRebalance will not work. + * + * Note: Delever to 0 will likely result in additional units of the borrow asset added as equity on the SetToken due to oracle price / market price mismatch + * + * @param _exchangeName the exchange used for trading + */ + function disengage(string memory _exchangeName) external onlyOperator { + LeverageInfo memory leverageInfo = _getAndValidateLeveragedInfo( + execution.slippageTolerance, + exchangeSettings[_exchangeName].twapMaxTradeSize, + _exchangeName + ); + + uint256 newLeverageRatio = PreciseUnitMath.preciseUnit(); + + ( + uint256 chunkRebalanceNotional, + uint256 totalRebalanceNotional + ) = _calculateChunkRebalanceNotional(leverageInfo, newLeverageRatio, false); + + if (totalRebalanceNotional > chunkRebalanceNotional) { + _delever(leverageInfo, chunkRebalanceNotional); + } else { + _deleverToZeroBorrowBalance(leverageInfo, totalRebalanceNotional); + } + + emit Disengaged( + leverageInfo.currentLeverageRatio, + newLeverageRatio, + chunkRebalanceNotional, + totalRebalanceNotional + ); + } + + /** + * OPERATOR ONLY: Set methodology settings and check new settings are valid. Note: Need to pass in existing parameters if only changing a few settings. Must not be + * in a rebalance. + * + * @param _newMethodologySettings Struct containing methodology parameters + */ + function setMethodologySettings(MethodologySettings memory _newMethodologySettings) external onlyOperator noRebalanceInProgress { + methodology = _newMethodologySettings; + + _validateNonExchangeSettings(methodology, execution, incentive); + + emit MethodologySettingsUpdated( + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + methodology.rebalanceInterval + ); + } + + /** + * OPERATOR ONLY: Set execution settings and check new settings are valid. Note: Need to pass in existing parameters if only changing a few settings. Must not be + * in a rebalance. + * + * @param _newExecutionSettings Struct containing execution parameters + */ + function setExecutionSettings(ExecutionSettings memory _newExecutionSettings) external onlyOperator noRebalanceInProgress { + execution = _newExecutionSettings; + + _validateNonExchangeSettings(methodology, execution, incentive); + + emit ExecutionSettingsUpdated( + execution.unutilizedLeveragePercentage, + execution.twapCooldownPeriod, + execution.slippageTolerance + ); + } + + /** + * OPERATOR ONLY: Set incentive settings and check new settings are valid. Note: Need to pass in existing parameters if only changing a few settings. Must not be + * in a rebalance. + * + * @param _newIncentiveSettings Struct containing incentive parameters + */ + function setIncentiveSettings(IncentiveSettings memory _newIncentiveSettings) external onlyOperator noRebalanceInProgress { + incentive = _newIncentiveSettings; + + _validateNonExchangeSettings(methodology, execution, incentive); + + emit IncentiveSettingsUpdated( + incentive.etherReward, + incentive.incentivizedLeverageRatio, + incentive.incentivizedSlippageTolerance, + incentive.incentivizedTwapCooldownPeriod + ); + } + + /** + * OPERATOR ONLY: Add a new enabled exchange for trading during rebalances. New exchanges will have their exchangeLastTradeTimestamp set to 0. Adding + * exchanges during rebalances is allowed, as it is not possible to enter an unexpected state while doing so. + * + * @param _exchangeName Name of the exchange + * @param _exchangeSettings Struct containing exchange parameters + */ + function addEnabledExchange( + string memory _exchangeName, + ExchangeSettings memory _exchangeSettings + ) + external + onlyOperator + { + require(exchangeSettings[_exchangeName].twapMaxTradeSize == 0, "Exchange already enabled"); + _validateExchangeSettings(_exchangeSettings); + + exchangeSettings[_exchangeName].twapMaxTradeSize = _exchangeSettings.twapMaxTradeSize; + exchangeSettings[_exchangeName].incentivizedTwapMaxTradeSize = _exchangeSettings.incentivizedTwapMaxTradeSize; + exchangeSettings[_exchangeName].leverExchangeData = _exchangeSettings.leverExchangeData; + exchangeSettings[_exchangeName].deleverExchangeData = _exchangeSettings.deleverExchangeData; + exchangeSettings[_exchangeName].exchangeLastTradeTimestamp = 0; + + enabledExchanges.push(_exchangeName); + + emit ExchangeAdded( + _exchangeName, + _exchangeSettings.twapMaxTradeSize, + _exchangeSettings.exchangeLastTradeTimestamp, + _exchangeSettings.incentivizedTwapMaxTradeSize, + _exchangeSettings.leverExchangeData, + _exchangeSettings.deleverExchangeData + ); + } + + /** + * OPERATOR ONLY: Removes an exchange. Reverts if the exchange is not already enabled. Removing exchanges during rebalances is allowed, + * as it is not possible to enter an unexpected state while doing so. + * + * @param _exchangeName Name of exchange to remove + */ + function removeEnabledExchange(string memory _exchangeName) external onlyOperator { + require(exchangeSettings[_exchangeName].twapMaxTradeSize != 0, "Exchange not enabled"); + + delete exchangeSettings[_exchangeName]; + enabledExchanges.removeStorage(_exchangeName); + + emit ExchangeRemoved(_exchangeName); + } + + /** + * OPERATOR ONLY: Updates the settings of an exchange. Reverts if exchange is not already added. When updating an exchange, exchangeLastTradeTimestamp + * is preserved. Updating exchanges during rebalances is allowed, as it is not possible to enter an unexpected state while doing so. Note: Need to + * pass in all existing parameters even if only changing a few settings. + * + * @param _exchangeName Name of the exchange + * @param _exchangeSettings Struct containing exchange parameters + */ + function updateEnabledExchange( + string memory _exchangeName, + ExchangeSettings memory _exchangeSettings + ) + external + onlyOperator + { + require(exchangeSettings[_exchangeName].twapMaxTradeSize != 0, "Exchange not enabled"); + _validateExchangeSettings(_exchangeSettings); + + exchangeSettings[_exchangeName].twapMaxTradeSize = _exchangeSettings.twapMaxTradeSize; + exchangeSettings[_exchangeName].incentivizedTwapMaxTradeSize = _exchangeSettings.incentivizedTwapMaxTradeSize; + exchangeSettings[_exchangeName].leverExchangeData = _exchangeSettings.leverExchangeData; + exchangeSettings[_exchangeName].deleverExchangeData = _exchangeSettings.deleverExchangeData; + + emit ExchangeUpdated( + _exchangeName, + _exchangeSettings.twapMaxTradeSize, + _exchangeSettings.exchangeLastTradeTimestamp, + _exchangeSettings.incentivizedTwapMaxTradeSize, + _exchangeSettings.leverExchangeData, + _exchangeSettings.deleverExchangeData + ); + } + + /** + * OPERATOR ONLY: Withdraw entire balance of ETH in this contract to operator. Rebalance must not be in progress + */ + function withdrawEtherBalance() external onlyOperator noRebalanceInProgress { + msg.sender.transfer(address(this).balance); + } + + receive() external payable {} + + /* ============ External Getter Functions ============ */ + + /** + * Get current leverage ratio. Current leverage ratio is defined as the Collateral Value (relative to borrow asset as per morpho oracle) divided by the borrow balance. + * + * return currentLeverageRatio Current leverage ratio in precise units (10e18) + */ + function getCurrentLeverageRatio() public view returns(uint256) { + ActionInfo memory currentLeverageInfo = _createActionInfo(); + + return _calculateCurrentLeverageRatio(currentLeverageInfo.collateralValue, currentLeverageInfo.borrowBalance); + } + + /** + * Calculates the chunk rebalance size. This can be used by external contracts and keeper bots to calculate the optimal exchange to rebalance with. + * Note: this function does not take into account timestamps, so it may return a nonzero value even when shouldRebalance would return ShouldRebalance.NONE for + * all exchanges (since minimum delays have not elapsed) + * + * @param _exchangeNames Array of exchange names to get rebalance sizes for + * + * @return sizes Array of total notional chunk size. Measured in the asset that would be sold + * @return sellAsset Asset that would be sold during a rebalance + * @return buyAsset Asset that would be purchased during a rebalance + */ + function getChunkRebalanceNotional( + string[] calldata _exchangeNames + ) + external + view + returns(uint256[] memory sizes, address sellAsset, address buyAsset) + { + + uint256 newLeverageRatio; + uint256 currentLeverageRatio = getCurrentLeverageRatio(); + bool isRipcord = false; + + // if over incentivized leverage ratio, always ripcord + if (currentLeverageRatio > incentive.incentivizedLeverageRatio) { + newLeverageRatio = methodology.maxLeverageRatio; + isRipcord = true; + // if we are in an ongoing twap, use the cached twapLeverageRatio as our target leverage + } else if (twapLeverageRatio > 0) { + newLeverageRatio = twapLeverageRatio; + // if all else is false, then we would just use the normal rebalance new leverage ratio calculation + } else { + newLeverageRatio = _calculateNewLeverageRatio(currentLeverageRatio); + } + + ActionInfo memory actionInfo = _createActionInfo(); + bool isLever = newLeverageRatio > currentLeverageRatio; + + sizes = new uint256[](_exchangeNames.length); + + for (uint256 i = 0; i < _exchangeNames.length; i++) { + + LeverageInfo memory leverageInfo = LeverageInfo({ + action: actionInfo, + currentLeverageRatio: currentLeverageRatio, + slippageTolerance: isRipcord ? incentive.incentivizedSlippageTolerance : execution.slippageTolerance, + twapMaxTradeSize: isRipcord ? + exchangeSettings[_exchangeNames[i]].incentivizedTwapMaxTradeSize : + exchangeSettings[_exchangeNames[i]].twapMaxTradeSize, + exchangeName: _exchangeNames[i] + }); + + (uint256 collateralNotional, ) = _calculateChunkRebalanceNotional(leverageInfo, newLeverageRatio, isLever); + + // _calculateBorrowUnits can convert both unit and notional values + sizes[i] = isLever ? _calculateBorrowUnits(collateralNotional, leverageInfo.action) : collateralNotional; + } + + sellAsset = isLever ? strategy.borrowAsset : strategy.collateralAsset; + buyAsset = isLever ? strategy.collateralAsset : strategy.borrowAsset; + } + + /** + * Get current Ether incentive for when current leverage ratio exceeds incentivized leverage ratio and ripcord can be called. If ETH balance on the contract is + * below the etherReward, then return the balance of ETH instead. + * + * return etherReward Quantity of ETH reward in base units (10e18) + */ + function getCurrentEtherIncentive() external view returns(uint256) { + uint256 currentLeverageRatio = getCurrentLeverageRatio(); + + if (currentLeverageRatio >= incentive.incentivizedLeverageRatio) { + // If ETH reward is below the balance on this contract, then return ETH balance on contract instead + return incentive.etherReward < address(this).balance ? incentive.etherReward : address(this).balance; + } else { + return 0; + } + } + + /** + * Helper that checks if conditions are met for rebalance or ripcord. Returns an enum with 0 = no rebalance, 1 = call rebalance(), 2 = call iterateRebalance() + * 3 = call ripcord() + * + * @return (string[] memory, ShouldRebalance[] memory) List of exchange names and a list of enums representing whether that exchange should rebalance + */ + function shouldRebalance() external view returns(string[] memory, ShouldRebalance[] memory) { + uint256 currentLeverageRatio = getCurrentLeverageRatio(); + + return _shouldRebalance(currentLeverageRatio, methodology.minLeverageRatio, methodology.maxLeverageRatio); + } + + /** + * Helper that checks if conditions are met for rebalance or ripcord with custom max and min bounds specified by caller. This function simplifies the + * logic for off-chain keeper bots to determine what threshold to call rebalance when leverage exceeds max or drops below min. Returns an enum with + * 0 = no rebalance, 1 = call rebalance(), 2 = call iterateRebalance(), 3 = call ripcord() + * + * @param _customMinLeverageRatio Min leverage ratio passed in by caller + * @param _customMaxLeverageRatio Max leverage ratio passed in by caller + * + * @return (string[] memory, ShouldRebalance[] memory) List of exchange names and a list of enums representing whether that exchange should rebalance + */ + function shouldRebalanceWithBounds( + uint256 _customMinLeverageRatio, + uint256 _customMaxLeverageRatio + ) + external + view + returns(string[] memory, ShouldRebalance[] memory) + { + require ( + _customMinLeverageRatio <= methodology.minLeverageRatio && _customMaxLeverageRatio >= methodology.maxLeverageRatio, + "Custom bounds must be valid" + ); + + uint256 currentLeverageRatio = getCurrentLeverageRatio(); + + return _shouldRebalance(currentLeverageRatio, _customMinLeverageRatio, _customMaxLeverageRatio); + } + + /** + * Gets the list of enabled exchanges + */ + function getEnabledExchanges() external view returns (string[] memory) { + return enabledExchanges; + } + + /** + * Explicit getter functions for parameter structs are defined as workaround to issues fetching structs that have dynamic types. + */ + function getStrategy() external view returns (ContractSettings memory) { return strategy; } + function getMethodology() external view returns (MethodologySettings memory) { return methodology; } + function getExecution() external view returns (ExecutionSettings memory) { return execution; } + function getIncentive() external view returns (IncentiveSettings memory) { return incentive; } + function getExchangeSettings(string memory _exchangeName) external view returns (ExchangeSettings memory) { + return exchangeSettings[_exchangeName]; + } + + /* ============ Internal Functions ============ */ + + + function _enterCollateralPosition() + internal + { + bytes memory enterPositionCallData = abi.encodeWithSignature( + "enterCollateralPosition(address)", + address(strategy.setToken) + ); + + invokeManager(address(strategy.leverageModule), enterPositionCallData); + } + + /** + * Calculate notional rebalance quantity, whether to chunk rebalance based on max trade size and max borrow and invoke lever on MorphoLeverageModule + * + */ + function _lever( + LeverageInfo memory _leverageInfo, + uint256 _chunkRebalanceNotional + ) + internal + { + uint256 collateralRebalanceUnits = _chunkRebalanceNotional.preciseDiv(_leverageInfo.action.setTotalSupply); + + uint256 borrowUnits = _calculateBorrowUnits(collateralRebalanceUnits, _leverageInfo.action); + + uint256 minReceiveCollateralUnits = _calculateMinCollateralReceiveUnits(collateralRebalanceUnits, _leverageInfo.slippageTolerance); + + bytes memory leverCallData = abi.encodeWithSignature( + "lever(address,uint256,uint256,string,bytes)", + address(strategy.setToken), + borrowUnits, + minReceiveCollateralUnits, + _leverageInfo.exchangeName, + exchangeSettings[_leverageInfo.exchangeName].leverExchangeData + ); + + invokeManager(address(strategy.leverageModule), leverCallData); + } + + /** + * Calculate delever units Invoke delever on MorphoLeverageModule. + */ + function _delever( + LeverageInfo memory _leverageInfo, + uint256 _chunkRebalanceNotional + ) + internal + { + uint256 collateralRebalanceUnits = _chunkRebalanceNotional.preciseDiv(_leverageInfo.action.setTotalSupply); + + uint256 minRepayUnits = _calculateMinRepayUnits(collateralRebalanceUnits, _leverageInfo.slippageTolerance, _leverageInfo.action); + + bytes memory deleverCallData = abi.encodeWithSignature( + "delever(address,uint256,uint256,string,bytes)", + address(strategy.setToken), + collateralRebalanceUnits, + minRepayUnits, + _leverageInfo.exchangeName, + exchangeSettings[_leverageInfo.exchangeName].deleverExchangeData + ); + + invokeManager(address(strategy.leverageModule), deleverCallData); + } + + /** + * Invoke deleverToZeroBorrowBalance on MorphoLeverageModule. + */ + function _deleverToZeroBorrowBalance( + LeverageInfo memory _leverageInfo, + uint256 _chunkRebalanceNotional + ) + internal + { + // Account for slippage tolerance in redeem quantity for the deleverToZeroBorrowBalance function + uint256 maxCollateralRebalanceUnits = _chunkRebalanceNotional + .preciseMul(PreciseUnitMath.preciseUnit().add(execution.slippageTolerance)) + .preciseDiv(_leverageInfo.action.setTotalSupply); + + bytes memory deleverToZeroBorrowBalanceCallData = abi.encodeWithSignature( + "deleverToZeroBorrowBalance(address,uint256,string,bytes)", + address(strategy.setToken), + maxCollateralRebalanceUnits, + _leverageInfo.exchangeName, + exchangeSettings[_leverageInfo.exchangeName].deleverExchangeData + ); + + invokeManager(address(strategy.leverageModule), deleverToZeroBorrowBalanceCallData); + } + + /** + * Check whether to delever or lever based on the current vs new leverage ratios. Used in the rebalance() and iterateRebalance() functions + * + * return uint256 Calculated notional to trade + * return uint256 Total notional to rebalance over TWAP + */ + function _handleRebalance(LeverageInfo memory _leverageInfo, uint256 _newLeverageRatio) internal returns(uint256, uint256) { + uint256 chunkRebalanceNotional; + uint256 totalRebalanceNotional; + if (_newLeverageRatio < _leverageInfo.currentLeverageRatio) { + ( + chunkRebalanceNotional, + totalRebalanceNotional + ) = _calculateChunkRebalanceNotional(_leverageInfo, _newLeverageRatio, false); + + _delever(_leverageInfo, chunkRebalanceNotional); + } else { + ( + chunkRebalanceNotional, + totalRebalanceNotional + ) = _calculateChunkRebalanceNotional(_leverageInfo, _newLeverageRatio, true); + + _lever(_leverageInfo, chunkRebalanceNotional); + } + + return (chunkRebalanceNotional, totalRebalanceNotional); + } + + /** + * Create the leverage info struct to be used in internal functions + * + * return LeverageInfo Struct containing ActionInfo and other data + */ + function _getAndValidateLeveragedInfo(uint256 _slippageTolerance, uint256 _maxTradeSize, string memory _exchangeName) internal view returns(LeverageInfo memory) { + // Assume if maxTradeSize is 0, then the exchange is not enabled. This is enforced by addEnabledExchange and updateEnabledExchange + require(_maxTradeSize > 0, "Must be valid exchange"); + + ActionInfo memory actionInfo = _createActionInfo(); + + require(actionInfo.setTotalSupply > 0, "SetToken must have > 0 supply"); + require(actionInfo.collateralBalance > 0, "Collateral balance must be > 0"); + require(actionInfo.borrowBalance > 0, "Borrow balance must exist"); + + // Get current leverage ratio + uint256 currentLeverageRatio = _calculateCurrentLeverageRatio( + actionInfo.collateralValue, + actionInfo.borrowBalance + ); + + return LeverageInfo({ + action: actionInfo, + currentLeverageRatio: currentLeverageRatio, + slippageTolerance: _slippageTolerance, + twapMaxTradeSize: _maxTradeSize, + exchangeName: _exchangeName + }); + } + + /** + * Create the action info struct to be used in internal functions + * + * return ActionInfo Struct containing data used by internal lever and delever functions + */ + function _createActionInfo() internal view virtual returns(ActionInfo memory) { + ActionInfo memory rebalanceInfo; + + IMorpho.MarketParams memory marketParams = strategy.leverageModule.marketParams(strategy.setToken); + // Collateral Price is returned relative to borrow asset with MORPHO_ORACLE_PRICE_SCALE decimal places + rebalanceInfo.collateralPrice = IMorphoOracle(marketParams.oracle).price(); + + (uint256 collateralBalance, uint256 borrowBalance,) = strategy.leverageModule.getCollateralAndBorrowBalances(strategy.setToken); + rebalanceInfo.collateralBalance = collateralBalance; + rebalanceInfo.borrowBalance = borrowBalance; + rebalanceInfo.collateralValue = rebalanceInfo.collateralPrice.mul(rebalanceInfo.collateralBalance).div(MORPHO_ORACLE_PRICE_SCALE); + rebalanceInfo.setTotalSupply = strategy.setToken.totalSupply(); + rebalanceInfo.lltv = marketParams.lltv; + + return rebalanceInfo; + } + + /** + * Validate non-exchange settings in constructor and setters when updating. + */ + function _validateNonExchangeSettings( + MethodologySettings memory _methodology, + ExecutionSettings memory _execution, + IncentiveSettings memory _incentive + ) + internal + virtual + pure + { + require ( + _methodology.minLeverageRatio <= _methodology.targetLeverageRatio && _methodology.minLeverageRatio > 0, + "Must be valid min leverage" + ); + require ( + _methodology.maxLeverageRatio >= _methodology.targetLeverageRatio, + "Must be valid max leverage" + ); + require ( + _methodology.recenteringSpeed <= PreciseUnitMath.preciseUnit() && _methodology.recenteringSpeed > 0, + "Must be valid recentering speed" + ); + require( + _methodology.targetLeverageRatio >= 1 ether, + "Target leverage ratio must be >= 1e18" + ); + require ( + _execution.unutilizedLeveragePercentage <= PreciseUnitMath.preciseUnit(), + "Unutilized leverage must be <100%" + ); + require ( + _execution.slippageTolerance <= PreciseUnitMath.preciseUnit(), + "Slippage tolerance must be <100%" + ); + require ( + _incentive.incentivizedSlippageTolerance <= PreciseUnitMath.preciseUnit(), + "Incentivized slippage tolerance must be <100%" + ); + require ( + _incentive.incentivizedLeverageRatio >= _methodology.maxLeverageRatio, + "Incentivized leverage ratio must be > max leverage ratio" + ); + require ( + _methodology.rebalanceInterval >= _execution.twapCooldownPeriod, + "Rebalance interval must be greater than TWAP cooldown period" + ); + require ( + _execution.twapCooldownPeriod >= _incentive.incentivizedTwapCooldownPeriod, + "TWAP cooldown must be greater than incentivized TWAP cooldown" + ); + } + + /** + * Validate an ExchangeSettings struct when adding or updating an exchange. Does not validate that twapMaxTradeSize < incentivizedMaxTradeSize since + * it may be useful to disable exchanges for ripcord by setting incentivizedMaxTradeSize to 0. + */ + function _validateExchangeSettings(ExchangeSettings memory _settings) internal pure { + require(_settings.twapMaxTradeSize != 0, "Max TWAP trade size must not be 0"); + } + + /** + * Validate that current leverage is below incentivized leverage ratio and cooldown / rebalance period has elapsed or outsize max/min bounds. Used + * in rebalance() and iterateRebalance() functions + */ + function _validateNormalRebalance(LeverageInfo memory _leverageInfo, uint256 _coolDown, uint256 _lastTradeTimestamp) internal view { + require(_leverageInfo.currentLeverageRatio < incentive.incentivizedLeverageRatio, "Must be below incentivized leverage ratio"); + require( + block.timestamp.sub(_lastTradeTimestamp) > _coolDown + || _leverageInfo.currentLeverageRatio > methodology.maxLeverageRatio + || _leverageInfo.currentLeverageRatio < methodology.minLeverageRatio, + "Cooldown not elapsed or not valid leverage ratio" + ); + } + + /** + * Validate that current leverage is above incentivized leverage ratio and incentivized cooldown period has elapsed in ripcord() + */ + function _validateRipcord(LeverageInfo memory _leverageInfo, uint256 _lastTradeTimestamp) internal view { + require(_leverageInfo.currentLeverageRatio >= incentive.incentivizedLeverageRatio, "Must be above incentivized leverage ratio"); + // If currently in the midst of a TWAP rebalance, ensure that the cooldown period has elapsed + require(_lastTradeTimestamp.add(incentive.incentivizedTwapCooldownPeriod) < block.timestamp, "TWAP cooldown must have elapsed"); + } + + /** + * Validate TWAP in the iterateRebalance() function + */ + function _validateTWAP() internal view { + require(twapLeverageRatio > 0, "Not in TWAP state"); + } + + /** + * Validate not TWAP in the rebalance() function + */ + function _validateNonTWAP() internal view { + require(twapLeverageRatio == 0, "Must call iterate"); + } + + /** + * Check if price has moved advantageously while in the midst of the TWAP rebalance. This means the current leverage ratio has moved over/under + * the stored TWAP leverage ratio on lever/delever so there is no need to execute a rebalance. Used in iterateRebalance() + */ + function _isAdvantageousTWAP(uint256 _currentLeverageRatio) internal view returns (bool) { + return ( + (twapLeverageRatio < methodology.targetLeverageRatio && _currentLeverageRatio >= twapLeverageRatio) + || (twapLeverageRatio > methodology.targetLeverageRatio && _currentLeverageRatio <= twapLeverageRatio) + ); + } + + /** + * Calculate the current leverage ratio given a valuation of the collateral in terms of the borrow asset + * and the borrow balance + * + * return uint256 Current leverage ratio + */ + function _calculateCurrentLeverageRatio( + uint256 _collateralValue, + uint256 _borrowBalance + ) + internal + pure + returns(uint256) + { + return _collateralValue.preciseDiv(_collateralValue.sub(_borrowBalance)); + } + + /** + * Calculate the new leverage ratio. The methodology reduces the size of each rebalance by weighting + * the current leverage ratio against the target leverage ratio by the recentering speed percentage. The lower the recentering speed, the slower + * the leverage token will move towards the target leverage each rebalance. + * + * return uint256 New leverage ratio + */ + function _calculateNewLeverageRatio(uint256 _currentLeverageRatio) internal view returns(uint256) { + // CLRt+1 = max(MINLR, min(MAXLR, CLRt * (1 - RS) + TLR * RS)) + // a: TLR * RS + // b: (1- RS) * CLRt + // c: (1- RS) * CLRt + TLR * RS + // d: min(MAXLR, CLRt * (1 - RS) + TLR * RS) + uint256 a = methodology.targetLeverageRatio.preciseMul(methodology.recenteringSpeed); + uint256 b = PreciseUnitMath.preciseUnit().sub(methodology.recenteringSpeed).preciseMul(_currentLeverageRatio); + uint256 c = a.add(b); + uint256 d = Math.min(c, methodology.maxLeverageRatio); + return Math.max(methodology.minLeverageRatio, d); + } + + /** + * Calculate total notional rebalance quantity and chunked rebalance quantity in collateral units. + * + * return uint256 Chunked rebalance notional in collateral units + * return uint256 Total rebalance notional in collateral units + */ + function _calculateChunkRebalanceNotional( + LeverageInfo memory _leverageInfo, + uint256 _newLeverageRatio, + bool _isLever + ) + internal + view + virtual + returns (uint256, uint256) + { + // Calculate absolute value of difference between new and current leverage ratio + uint256 leverageRatioDifference = _isLever ? _newLeverageRatio.sub(_leverageInfo.currentLeverageRatio) : _leverageInfo.currentLeverageRatio.sub(_newLeverageRatio); + + uint256 totalRebalanceNotional = leverageRatioDifference.preciseDiv(_leverageInfo.currentLeverageRatio).preciseMul(_leverageInfo.action.collateralBalance); + + uint256 maxBorrow = _calculateMaxBorrowCollateral(_leverageInfo.action, _isLever); + + uint256 chunkRebalanceNotional = Math.min(Math.min(maxBorrow, totalRebalanceNotional), _leverageInfo.twapMaxTradeSize); + + return (chunkRebalanceNotional, totalRebalanceNotional); + } + + /** + * Calculate the max borrow / repay amount allowed in base units for lever / delever. This is due to overcollateralization requirements on + * assets deposited in lending protocols for borrowing. + * + * For lever, max borrow is calculated as: + * (Net borrow limit - existing borrow balance) / collateral price in borrow asset units + * + * For delever, max repay is calculated as: + * Collateral balance * (net borrow limit - existing borrow balance) / net borrow limit + * + * Net borrow limit is calculated as: + * Collateral Balance * Collateral Price in Borrow units * morpho LLTV * (1 - unutilized leverage %) + * + * return uint256 Max borrow notional denominated in collateral asset + */ + function _calculateMaxBorrowCollateral(ActionInfo memory _actionInfo, bool _isLever) internal virtual view returns(uint256) { + + // Note NetBorrow Limit is already denominated in borrow asset + uint256 netBorrowLimit = _actionInfo.collateralBalance + .mul(_actionInfo.collateralPrice).div(MORPHO_ORACLE_PRICE_SCALE) + .preciseMul(_actionInfo.lltv) + .preciseMul(PreciseUnitMath.preciseUnit().sub(execution.unutilizedLeveragePercentage)); + if (_isLever) { + return netBorrowLimit + .sub(_actionInfo.borrowBalance) + .mul(MORPHO_ORACLE_PRICE_SCALE).div(_actionInfo.collateralPrice); + } else { + return _actionInfo.collateralBalance + .preciseMul(netBorrowLimit.sub(_actionInfo.borrowBalance)) + .preciseDiv(netBorrowLimit); + } + } + + /** + * Derive the borrow units for lever. The units are calculated by the collateral units multiplied by collateral / borrow asset price. + * Output is measured to borrow unit decimals. + * + * return uint256 Position units to borrow + */ + function _calculateBorrowUnits(uint256 _collateralRebalanceUnits, ActionInfo memory _actionInfo) internal pure returns (uint256) { + return _collateralRebalanceUnits.mul(_actionInfo.collateralPrice).div(MORPHO_ORACLE_PRICE_SCALE); + } + + /** + * Calculate the min receive units in collateral units for lever. Units are calculated as target collateral rebalance units multiplied by slippage tolerance + * Output is measured in collateral asset decimals. + * + * return uint256 Min position units to receive after lever trade + */ + function _calculateMinCollateralReceiveUnits(uint256 _collateralRebalanceUnits, uint256 _slippageTolerance) internal pure returns (uint256) { + return _collateralRebalanceUnits.preciseMul(PreciseUnitMath.preciseUnit().sub(_slippageTolerance)); + } + + /** + * Derive the min repay units from collateral units for delever. Units are calculated as target collateral rebalance units multiplied by slippage tolerance + * and collateral price (in borrow units). Output is measured in borrow unit decimals. + * + * return uint256 Min position units to repay in borrow asset + */ + function _calculateMinRepayUnits(uint256 _collateralRebalanceUnits, uint256 _slippageTolerance, ActionInfo memory _actionInfo) internal virtual pure returns (uint256) { + return _collateralRebalanceUnits + .mul(_actionInfo.collateralPrice).div(MORPHO_ORACLE_PRICE_SCALE) + .preciseMul(PreciseUnitMath.preciseUnit().sub(_slippageTolerance)); + } + + /** + * Update last trade timestamp and if chunk rebalance size is less than total rebalance notional, store new leverage ratio to kick off TWAP. Used in + * the engage() and rebalance() functions + */ + function _updateRebalanceState( + uint256 _chunkRebalanceNotional, + uint256 _totalRebalanceNotional, + uint256 _newLeverageRatio, + string memory _exchangeName + ) + internal + { + + _updateLastTradeTimestamp(_exchangeName); + + if (_chunkRebalanceNotional < _totalRebalanceNotional) { + twapLeverageRatio = _newLeverageRatio; + } + } + + /** + * Update last trade timestamp and if chunk rebalance size is equal to the total rebalance notional, end TWAP by clearing state. This function is used + * in iterateRebalance() + */ + function _updateIterateState(uint256 _chunkRebalanceNotional, uint256 _totalRebalanceNotional, string memory _exchangeName) internal { + + _updateLastTradeTimestamp(_exchangeName); + + // If the chunk size is equal to the total notional meaning that rebalances are not chunked, then clear TWAP state. + if (_chunkRebalanceNotional == _totalRebalanceNotional) { + delete twapLeverageRatio; + } + } + + /** + * Update last trade timestamp and if currently in a TWAP, delete the TWAP state. Used in the ripcord() function. + */ + function _updateRipcordState(string memory _exchangeName) internal { + + _updateLastTradeTimestamp(_exchangeName); + + // If TWAP leverage ratio is stored, then clear state. This may happen if we are currently in a TWAP rebalance, and the leverage ratio moves above the + // incentivized threshold for ripcord. + if (twapLeverageRatio > 0) { + delete twapLeverageRatio; + } + } + + /** + * Update globalLastTradeTimestamp and exchangeLastTradeTimestamp values. This function updates both the exchange-specific and global timestamp so that the + * epoch rebalance can use the global timestamp (since the global timestamp is always equal to the most recently used exchange timestamp). This allows for + * multiple rebalances to occur simultaneously since only the exchange-specific timestamp is checked for non-epoch rebalances. + */ + function _updateLastTradeTimestamp(string memory _exchangeName) internal { + globalLastTradeTimestamp = block.timestamp; + exchangeSettings[_exchangeName].exchangeLastTradeTimestamp = block.timestamp; + } + + /** + * Transfer ETH reward to caller of the ripcord function. If the ETH balance on this contract is less than required + * incentive quantity, then transfer contract balance instead to prevent reverts. + * + * return uint256 Amount of ETH transferred to caller + */ + function _transferEtherRewardToCaller(uint256 _etherReward) internal returns(uint256) { + uint256 etherToTransfer = _etherReward < address(this).balance ? _etherReward : address(this).balance; + + msg.sender.transfer(etherToTransfer); + + return etherToTransfer; + } + + /** + * Internal function returning the ShouldRebalance enum used in shouldRebalance and shouldRebalanceWithBounds external getter functions + * + * return ShouldRebalance Enum detailing whether to rebalance, iterateRebalance, ripcord or no action + */ + function _shouldRebalance( + uint256 _currentLeverageRatio, + uint256 _minLeverageRatio, + uint256 _maxLeverageRatio + ) + internal + view + returns(string[] memory, ShouldRebalance[] memory) + { + + ShouldRebalance[] memory shouldRebalanceEnums = new ShouldRebalance[](enabledExchanges.length); + + for (uint256 i = 0; i < enabledExchanges.length; i++) { + // If none of the below conditions are satisfied, then should not rebalance + shouldRebalanceEnums[i] = ShouldRebalance.NONE; + + // If above ripcord threshold, then check if incentivized cooldown period has elapsed + if (_currentLeverageRatio >= incentive.incentivizedLeverageRatio) { + if (exchangeSettings[enabledExchanges[i]].exchangeLastTradeTimestamp.add(incentive.incentivizedTwapCooldownPeriod) < block.timestamp) { + shouldRebalanceEnums[i] = ShouldRebalance.RIPCORD; + } + } else { + // If TWAP, then check if the cooldown period has elapsed + if (twapLeverageRatio > 0) { + if (exchangeSettings[enabledExchanges[i]].exchangeLastTradeTimestamp.add(execution.twapCooldownPeriod) < block.timestamp) { + shouldRebalanceEnums[i] = ShouldRebalance.ITERATE_REBALANCE; + } + } else { + // If not TWAP, then check if the rebalance interval has elapsed OR current leverage is above max leverage OR current leverage is below + // min leverage + if ( + block.timestamp.sub(globalLastTradeTimestamp) > methodology.rebalanceInterval + || _currentLeverageRatio > _maxLeverageRatio + || _currentLeverageRatio < _minLeverageRatio + ) { + shouldRebalanceEnums[i] = ShouldRebalance.REBALANCE; + } + } + } + } + + return (enabledExchanges, shouldRebalanceEnums); + } +} diff --git a/contracts/interfaces/IMorphoLeverageModule.sol b/contracts/interfaces/IMorphoLeverageModule.sol new file mode 100644 index 000000000..9c3a2f302 --- /dev/null +++ b/contracts/interfaces/IMorphoLeverageModule.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache License, Version 2.0 +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { ISetToken } from "./ISetToken.sol"; +import { IMorpho } from "./IMorpho.sol"; + +interface IMorphoLeverageModule { + function sync( + ISetToken _setToken + ) external; + + function lever( + ISetToken _setToken, + uint256 _borrowQuantityUnits, + uint256 _minReceiveQuantityUnits, + string memory _tradeAdapterName, + bytes memory _tradeData + ) external; + + function delever( + ISetToken _setToken, + uint256 _redeemQuantityUnits, + uint256 _minRepayQuantityUnits, + string memory _tradeAdapterName, + bytes memory _tradeData + ) external; + + function marketParams(ISetToken _setToken) external view returns (IMorpho.MarketParams memory); + + function getCollateralAndBorrowBalances( + ISetToken _setToken + ) + external + view + returns(uint256 collateralBalance, uint256 borrowBalance, uint256 borrowSharesU256); + +} diff --git a/contracts/interfaces/IMorphoOracle.sol b/contracts/interfaces/IMorphoOracle.sol new file mode 100644 index 000000000..0f68d00c4 --- /dev/null +++ b/contracts/interfaces/IMorphoOracle.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.6.10; + +/// @title IOracle +/// @author Morpho Labs +/// @notice Interface that oracles used by Morpho must implement. +/// @dev It is the user's responsibility to select markets with safe oracles. +interface IMorphoOracle { + /// @notice Returns the price of 1 asset of collateral token quoted in 1 asset of loan token, scaled by 1e36. + /// @dev It corresponds to the price of 10**(collateral token decimals) assets of collateral token quoted in + /// 10**(loan token decimals) assets of loan token with `36 + loan token decimals - collateral token decimals` + /// decimals of precision. + function price() external view returns (uint256); +} diff --git a/contracts/interfaces/external/IChainlinkEACAggregatorProxy.sol b/contracts/interfaces/external/IChainlinkEACAggregatorProxy.sol new file mode 100644 index 000000000..79abe1fc6 --- /dev/null +++ b/contracts/interfaces/external/IChainlinkEACAggregatorProxy.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; + +interface AggregatorInterface { + function latestAnswer() external view returns (int256); + function latestTimestamp() external view returns (uint256); + function latestRound() external view returns (uint256); + function getAnswer(uint256 roundId) external view returns (int256); + function getTimestamp(uint256 roundId) external view returns (uint256); +} + +interface AggregatorV3Interface { + + function decimals() external view returns (uint8); + function description() external view returns (string memory); + function version() external view returns (uint256); + + // getRoundData and latestRoundData should both raise "No data present" + // if they do not have data to report, instead of returning unset values + // which could be misinterpreted as actual reported values. + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + +} + +interface AggregatorV2V3Interface is AggregatorInterface, AggregatorV3Interface +{ +} + +interface IChainlinkEACAggregatorProxy { + function acceptOwnership() external; + function accessController() external view returns (address); + function aggregator() external view returns (address); + function confirmAggregator(address _aggregator) external; + function decimals() external view returns (uint8); + function description() external view returns (string memory); + function getAnswer(uint256 _roundId) external view returns (int256); + function getRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + function getTimestamp(uint256 _roundId) external view returns (uint256); + function latestAnswer() external view returns (int256); + function latestRound() external view returns (uint256); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + function latestTimestamp() external view returns (uint256); + function owner() external view returns (address payable); + function phaseAggregators(uint16) external view returns (address); + function phaseId() external view returns (uint16); + function proposeAggregator(address _aggregator) external; + function proposedAggregator() external view returns (address); + function proposedGetRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + function proposedLatestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + function setController(address _accessController) external; + function transferOwnership(address _to) external; + function version() external view returns (uint256); +} diff --git a/external/abi/set/Morpho.json b/external/abi/set/Morpho.json new file mode 100644 index 000000000..69b9b2d40 --- /dev/null +++ b/external/abi/set/Morpho.json @@ -0,0 +1,325 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Morpho", + "sourceName": "contracts/protocol/integration/lib/Morpho.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IMorpho", + "name": "_morpho", + "type": "IMorpho" + }, + { + "components": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "internalType": "struct MarketParams", + "name": "_marketParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_assets", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_onBehalfOf", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "getBorrowCalldata", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract IMorpho", + "name": "_morpho", + "type": "IMorpho" + }, + { + "components": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "internalType": "struct MarketParams", + "name": "_marketParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_assets", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_shares", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_onBehalfOf", + "type": "address" + } + ], + "name": "getRepayCalldata", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract IMorpho", + "name": "_morpho", + "type": "IMorpho" + }, + { + "components": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "internalType": "struct MarketParams", + "name": "_marketParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_onBehalfOf", + "type": "address" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "getSupplyCollateralCalldata", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract IMorpho", + "name": "_morpho", + "type": "IMorpho" + }, + { + "components": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "internalType": "struct MarketParams", + "name": "_marketParams", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_assets", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_onBehalfOf", + "type": "address" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "getWithdrawCollateralCalldata", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x610a43610026600b82828239805160001a60731461001957fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600436106100925760003560e01c806342b71c941161006557806342b71c9414610124578063853704c814610144578063a5d2a3cc14610157578063c946b87f1461016a57610092565b80630c18de95146100975780632d379e12146100c2578063354efce3146100e4578063401fe5c414610104575b600080fd5b6100aa6100a53660046106cd565b61017d565b6040516100b993929190610854565b60405180910390f35b8180156100ce57600080fd5b506100e26100dd36600461073e565b6101d8565b005b8180156100f057600080fd5b506100e26100ff36600461078e565b61027d565b81801561011057600080fd5b506100e261011f36600461073e565b610321565b81801561013057600080fd5b506100e261013f36600461073e565b610330565b6100aa610152366004610558565b61034e565b6100aa61016536600461067c565b6103a6565b6100aa6101783660046105c0565b610410565b600080606080888888888860405160240161019c9594939291906108f2565b60408051601f198184030181529190526020810180516001600160e01b03166350d8cd4b60e01b179052999a60009a9950975050505050505050565b60606101e98484846000898a61017d565b6040516347b7819960e11b81529093506001600160a01b0388169250638f6f0332915061021f9087906000908690600401610854565b600060405180830381600087803b15801561023957600080fd5b505af115801561024d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261027591908101906104dd565b505050505050565b606061028c858585858a6103a6565b6040516347b7819960e11b81529093506001600160a01b0389169250638f6f033291506102c29088906000908690600401610854565b600060405180830381600087803b1580156102dc57600080fd5b505af11580156102f0573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261031891908101906104dd565b50505050505050565b60606101e9848484888961034e565b604080516020810190915260008152606061028c8585858986610410565b6000806060808787878760405160240161036b949392919061087b565b60408051601f198184030181529190526020810180516001600160e01b0316638720316d60e01b179052989960009998509650505050505050565b6040805160208101825260008082529151829160609182906103d4908a908a908a908a90879060240161092e565b60408051601f198184030181529190526020810180516001600160e01b03166320b76e8160e01b179052999a60009a9950975050505050505050565b6000806060808787878760405160240161042d94939291906108af565b60408051601f198184030181529190526020810180516001600160e01b031663238d657960e01b179052989960009998509650505050505050565b600060a08284031215610479578081fd5b61048360a061097a565b90508135610490816109f5565b815260208201356104a0816109f5565b602082015260408201356104b3816109f5565b604082015260608201356104c6816109f5565b806060830152506080820135608082015292915050565b6000602082840312156104ee578081fd5b815167ffffffffffffffff811115610504578182fd5b80830184601f820112610515578283fd5b8051915061052a610525836109a1565b61097a565b82815285602084840101111561053e578384fd5b61054f8360208301602085016109c5565b95945050505050565b60008060008060006101208688031215610570578081fd5b853561057b816109f5565b945061058a8760208801610468565b935060c0860135925060e08601356105a1816109f5565b91506101008601356105b2816109f5565b809150509295509295909350565b600080600080600061012086880312156105d8578081fd5b85356105e3816109f5565b94506105f28760208801610468565b935060c0860135925060e0860135610609816109f5565b915061010086013567ffffffffffffffff811115610625578182fd5b80870188601f820112610636578283fd5b80359150610646610525836109a1565b82815289602084840101111561065a578384fd5b8260208301602083013783602084830101528093505050509295509295909350565b60008060008060006101208688031215610694578081fd5b853561069f816109f5565b94506106ae8760208801610468565b935060c0860135925060e086013591506101008601356105b2816109f5565b60008060008060008061014087890312156106e6578081fd5b86356106f1816109f5565b95506107008860208901610468565b945060c0870135935060e0870135925061010087013561071f816109f5565b9150610120870135610730816109f5565b809150509295509295509295565b6000806000806101008587031215610754578384fd5b843561075f816109f5565b9350602085013561076f816109f5565b925061077e8660408701610468565b9396929550929360e00135925050565b600080600080600061012086880312156107a6578081fd5b85356107b1816109f5565b945060208601356107c1816109f5565b93506107d08760408801610468565b9497939650939460e08101359450610100013592915050565b600081518084526108018160208601602086016109c5565b601f01601f19169290920160200192915050565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b600060018060a01b03851682528360208301526060604083015261054f60608301846107e9565b610100810161088a8287610815565b60a08201949094526001600160a01b0392831660c0820152911660e090910152919050565b60006101006108be8388610815565b60a083018690526001600160a01b03851660c084015260e083018190526108e7818401856107e9565b979650505050505050565b61012081016109018288610815565b60a082019590955260c08101939093526001600160a01b0391821660e08401521661010090910152919050565b600061012061093d8389610815565b60a0830187905260c083018690526001600160a01b03851660e0840152610100830181905261096e818401856107e9565b98975050505050505050565b60405181810167ffffffffffffffff8111828210171561099957600080fd5b604052919050565b600067ffffffffffffffff8211156109b7578081fd5b50601f01601f191660200190565b60005b838110156109e05781810151838201526020016109c8565b838111156109ef576000848401525b50505050565b6001600160a01b0381168114610a0a57600080fd5b5056fea2646970667358221220f242801c9e5eb2db4c453065b6af30ec2b4ac53704f7607c2e4d6711303a798b64736f6c634300060a0033", + "deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600436106100925760003560e01c806342b71c941161006557806342b71c9414610124578063853704c814610144578063a5d2a3cc14610157578063c946b87f1461016a57610092565b80630c18de95146100975780632d379e12146100c2578063354efce3146100e4578063401fe5c414610104575b600080fd5b6100aa6100a53660046106cd565b61017d565b6040516100b993929190610854565b60405180910390f35b8180156100ce57600080fd5b506100e26100dd36600461073e565b6101d8565b005b8180156100f057600080fd5b506100e26100ff36600461078e565b61027d565b81801561011057600080fd5b506100e261011f36600461073e565b610321565b81801561013057600080fd5b506100e261013f36600461073e565b610330565b6100aa610152366004610558565b61034e565b6100aa61016536600461067c565b6103a6565b6100aa6101783660046105c0565b610410565b600080606080888888888860405160240161019c9594939291906108f2565b60408051601f198184030181529190526020810180516001600160e01b03166350d8cd4b60e01b179052999a60009a9950975050505050505050565b60606101e98484846000898a61017d565b6040516347b7819960e11b81529093506001600160a01b0388169250638f6f0332915061021f9087906000908690600401610854565b600060405180830381600087803b15801561023957600080fd5b505af115801561024d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261027591908101906104dd565b505050505050565b606061028c858585858a6103a6565b6040516347b7819960e11b81529093506001600160a01b0389169250638f6f033291506102c29088906000908690600401610854565b600060405180830381600087803b1580156102dc57600080fd5b505af11580156102f0573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261031891908101906104dd565b50505050505050565b60606101e9848484888961034e565b604080516020810190915260008152606061028c8585858986610410565b6000806060808787878760405160240161036b949392919061087b565b60408051601f198184030181529190526020810180516001600160e01b0316638720316d60e01b179052989960009998509650505050505050565b6040805160208101825260008082529151829160609182906103d4908a908a908a908a90879060240161092e565b60408051601f198184030181529190526020810180516001600160e01b03166320b76e8160e01b179052999a60009a9950975050505050505050565b6000806060808787878760405160240161042d94939291906108af565b60408051601f198184030181529190526020810180516001600160e01b031663238d657960e01b179052989960009998509650505050505050565b600060a08284031215610479578081fd5b61048360a061097a565b90508135610490816109f5565b815260208201356104a0816109f5565b602082015260408201356104b3816109f5565b604082015260608201356104c6816109f5565b806060830152506080820135608082015292915050565b6000602082840312156104ee578081fd5b815167ffffffffffffffff811115610504578182fd5b80830184601f820112610515578283fd5b8051915061052a610525836109a1565b61097a565b82815285602084840101111561053e578384fd5b61054f8360208301602085016109c5565b95945050505050565b60008060008060006101208688031215610570578081fd5b853561057b816109f5565b945061058a8760208801610468565b935060c0860135925060e08601356105a1816109f5565b91506101008601356105b2816109f5565b809150509295509295909350565b600080600080600061012086880312156105d8578081fd5b85356105e3816109f5565b94506105f28760208801610468565b935060c0860135925060e0860135610609816109f5565b915061010086013567ffffffffffffffff811115610625578182fd5b80870188601f820112610636578283fd5b80359150610646610525836109a1565b82815289602084840101111561065a578384fd5b8260208301602083013783602084830101528093505050509295509295909350565b60008060008060006101208688031215610694578081fd5b853561069f816109f5565b94506106ae8760208801610468565b935060c0860135925060e086013591506101008601356105b2816109f5565b60008060008060008061014087890312156106e6578081fd5b86356106f1816109f5565b95506107008860208901610468565b945060c0870135935060e0870135925061010087013561071f816109f5565b9150610120870135610730816109f5565b809150509295509295509295565b6000806000806101008587031215610754578384fd5b843561075f816109f5565b9350602085013561076f816109f5565b925061077e8660408701610468565b9396929550929360e00135925050565b600080600080600061012086880312156107a6578081fd5b85356107b1816109f5565b945060208601356107c1816109f5565b93506107d08760408801610468565b9497939650939460e08101359450610100013592915050565b600081518084526108018160208601602086016109c5565b601f01601f19169290920160200192915050565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b600060018060a01b03851682528360208301526060604083015261054f60608301846107e9565b610100810161088a8287610815565b60a08201949094526001600160a01b0392831660c0820152911660e090910152919050565b60006101006108be8388610815565b60a083018690526001600160a01b03851660c084015260e083018190526108e7818401856107e9565b979650505050505050565b61012081016109018288610815565b60a082019590955260c08101939093526001600160a01b0391821660e08401521661010090910152919050565b600061012061093d8389610815565b60a0830187905260c083018690526001600160a01b03851660e0840152610100830181905261096e818401856107e9565b98975050505050505050565b60405181810167ffffffffffffffff8111828210171561099957600080fd5b604052919050565b600067ffffffffffffffff8211156109b7578081fd5b50601f01601f191660200190565b60005b838110156109e05781810151838201526020016109c8565b838111156109ef576000848401525b50505050565b6001600160a01b0381168114610a0a57600080fd5b5056fea2646970667358221220f242801c9e5eb2db4c453065b6af30ec2b4ac53704f7607c2e4d6711303a798b64736f6c634300060a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} + diff --git a/external/abi/set/MorphoLeverageModule.json b/external/abi/set/MorphoLeverageModule.json new file mode 100644 index 000000000..39c84549e --- /dev/null +++ b/external/abi/set/MorphoLeverageModule.json @@ -0,0 +1,766 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "MorphoLeverageModule", + "sourceName": "contracts/protocol/modules/v1/MorphoLeverageModule.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IController", + "name": "_controller", + "type": "address" + }, + { + "internalType": "contract IMorpho", + "name": "_morpho", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bool", + "name": "_anySetAllowed", + "type": "bool" + } + ], + "name": "AnySetAllowedUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "_collateralAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "_repayAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IExchangeAdapter", + "name": "_exchangeAdapter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalRedeemAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalRepayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_protocolFee", + "type": "uint256" + } + ], + "name": "LeverageDecreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "_borrowAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "_collateralAsset", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IExchangeAdapter", + "name": "_exchangeAdapter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalBorrowAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalReceiveAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_protocolFee", + "type": "uint256" + } + ], + "name": "LeverageIncreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "_marketId", + "type": "bytes32" + } + ], + "name": "MorphoMarketUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "bool", + "name": "_added", + "type": "bool" + } + ], + "name": "SetTokenStatusUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + } + ], + "name": "allowedSetTokens", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "anySetAllowed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_setTokenQuantity", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "_component", + "type": "address" + }, + { + "internalType": "bool", + "name": "_isEquity", + "type": "bool" + } + ], + "name": "componentIssueHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_setTokenQuantity", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "_component", + "type": "address" + }, + { + "internalType": "bool", + "name": "_isEquity", + "type": "bool" + } + ], + "name": "componentRedeemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "contract IController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_redeemQuantityUnits", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minRepayQuantityUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_tradeAdapterName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_tradeData", + "type": "bytes" + } + ], + "name": "delever", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_redeemQuantityUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_tradeAdapterName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_tradeData", + "type": "bytes" + } + ], + "name": "deleverToZeroBorrowBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "enterCollateralPosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "getCollateralAndBorrowBalances", + "outputs": [ + { + "internalType": "uint256", + "name": "collateralBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowBalance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowSharesU256", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "getMarketId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "internalType": "struct MarketParams", + "name": "_marketParams", + "type": "tuple" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_borrowQuantityUnits", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minReceiveQuantityUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_tradeAdapterName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_tradeData", + "type": "bytes" + } + ], + "name": "lever", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + } + ], + "name": "marketParams", + "outputs": [ + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle", + "type": "address" + }, + { + "internalType": "address", + "name": "irm", + "type": "address" + }, + { + "internalType": "uint256", + "name": "lltv", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "moduleIssueHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "moduleRedeemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "morpho", + "outputs": [ + { + "internalType": "contract IMorpho", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "contract IDebtIssuanceModule", + "name": "_debtIssuanceModule", + "type": "address" + } + ], + "name": "registerToModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "removeModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "sync", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "bool", + "name": "_status", + "type": "bool" + } + ], + "name": "updateAllowedSetToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_anySetAllowed", + "type": "bool" + } + ], + "name": "updateAnySetAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x60a06040523480156200001157600080fd5b50604051620055f8380380620055f88339810160408190526200003491620000c6565b600080546001600160a01b0319166001600160a01b038416178155600180556200005d620000c2565b600280546001600160a01b0319166001600160a01b038316908117909155604051919250906000907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a35060601b6001600160601b031916608052506200011d565b3390565b60008060408385031215620000d9578182fd5b8251620000e68162000104565b6020840151909250620000f98162000104565b809150509250929050565b6001600160a01b03811681146200011a57600080fd5b50565b60805160601c61548662000172600039806115dd5280611a975280611afc5280611bf95280611d62528061209a52806120ff528061217652806121db528061255952806125f252806134dc52506154866000f3fe608060405234801561001057600080fd5b50600436106101585760003560e01c8063a5841194116100c3578063da35e2831161007c578063da35e283146102aa578063e93353a3146102ce578063ee78244f146102e1578063f25fcc9f146102f4578063f2fde38b14610307578063f77c47911461031a57610158565b8063a584119414610254578063b1dd4d9214610267578063c137f4d71461027c578063c153dd0714610185578063c690a74c1461028f578063d8fbc833146102a257610158565b80635b136512116101155780635b136512146101e95780635c990306146101fc578063715018a61461020f5780637bb3526514610217578063847ef08d146102375780638da5cb5b1461023f57610158565b80630fb96b211461015d57806311976c04146101725780633fe6106b1461018557806348a2f01b146101985780635199e418146101c357806356b27e1a146101d6575b600080fd5b61017061016b3660046146da565b610322565b005b6101706101803660046147a9565b6105a4565b6101706101933660046146af565b610771565b6101ab6101a636600461440c565b610789565b6040516101ba9392919061538b565b60405180910390f35b6101706101d1366004614547565b610827565b6101706101e43660046147a9565b610899565b6101706101f73660046145ea565b610a3d565b61017061020a366004614617565b610b42565b610170610dbd565b61022a61022536600461472c565b610e3c565b6040516101ba9190614aff565b61017061107d565b6102476111d9565b6040516101ba91906149ac565b61017061026236600461440c565b6111e8565b61026f61122b565b6040516101ba9190614af4565b61017061028a3660046146da565b611234565b61017061029d3660046145b2565b611493565b6102476115db565b6102bd6102b836600461440c565b6115ff565b6040516101ba9594939291906149da565b6101706102dc36600461440c565b611641565b61026f6102ef36600461440c565b611775565b61022a61030236600461440c565b61178a565b61017061031536600461440c565b61181d565b6102476118d4565b8361032c816118e3565b610334614275565b506001600160a01b03808616600090815260036020818152604092839020835160a081018552815486168152600182015486169281019290925260028101548516938201939093529082015490921660608301526004015460808201528280156103b35750836001600160a01b031681602001516001600160a01b0316145b1561048f576040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906103e990889030906004016149c0565b60206040518083038186803b15801561040157600080fd5b505afa158015610415573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610439919061484f565b9050600081136104645760405162461bcd60e51b815260040161045b90614c59565b60405180910390fd5b600061047f8761047384611a28565b9063ffffffff611a4e16565b905061048c888483611a81565b50505b8261059c57836001600160a01b031681600001516001600160a01b0316146104c95760405162461bcd60e51b815260040161045b90614bf3565b6040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906104fa90889030906004016149c0565b60206040518083038186803b15801561051257600080fd5b505afa158015610526573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061054a919061484f565b90506000811261056c5760405162461bcd60e51b815260040161045b90614c59565b600061058c876104736105878560001963ffffffff611b5416565b611a28565b9050610599888483611bbf565b50505b505050505050565b600260015414156105c75760405162461bcd60e51b815260040161045b90615248565b6002600155846105d681611c25565b6105de614275565b506001600160a01b03808716600090815260036020818152604092839020835160a081018552815486168152600182015486169281018390526002820154861694810194909452918201549093166060830152600401546080820152906106575760405162461bcd60e51b815260040161045b9061521c565b61065f6142a3565b61067788836020015184600001518a8a8a6000611c70565b905061068c8160000151838360600151611d28565b60006106a2828460200151856000015188611d8e565b905060006106b58a85600001518461201c565b905060006106c9838363ffffffff61203f16565b90506106dc846000015186836000612081565b6106ea848660000151612236565b83516106f5906122ec565b84600001516001600160a01b031685602001516001600160a01b03168c6001600160a01b03167f7cda30123ddfc96659344700585861a8670352b9cc86d1b1054d10083b1dcdd48760200151886060015186886040516107589493929190614b1f565b60405180910390a4505060018055505050505050505050565b8161077b816118e3565b610784836111e8565b505050565b6000806000610796614275565b506001600160a01b03808516600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061080f5760405162461bcd60e51b815260040161045b9061521c565b6108198582612529565b935093509350509193909250565b61082f612672565b6002546001600160a01b0390811691161461085c5760405162461bcd60e51b815260040161045b90614ffe565b6005805460ff19168215159081179091556040517f563e1633136cdd43b8793897cb53ba2a9e31c18b3ae0b6827fbbb03b9902e6c690600090a250565b600260015414156108bc5760405162461bcd60e51b815260040161045b90615248565b6002600155846108cb81611c25565b6108d3614275565b506001600160a01b03808716600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061094c5760405162461bcd60e51b815260040161045b9061521c565b6109546142a3565b61096c88836000015184602001518a8a8a6001611c70565b90506109818160000151838360600151611bbf565b6000610997828460000151856020015188611d8e565b905060006109aa8a85602001518461201c565b905060006109be838363ffffffff61203f16565b90506109cf84600001518683611a81565b83516109da906122ec565b84602001516001600160a01b031685600001516001600160a01b03168c6001600160a01b03167f359f8b62a966cfd521a3815681266407201b20a7c334925faa49e7d9d5dd57ab8760200151886060015186886040516107589493929190614b1f565b81610a4781611c25565b6040516335fc6c9f60e21b81526001600160a01b0384169063d7f1b27c90610a739085906004016149ac565b60206040518083038186803b158015610a8b57600080fd5b505afa158015610a9f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ac39190614563565b610adf5760405162461bcd60e51b815260040161045b90614ed5565b6040516306cd8db760e51b81526001600160a01b0383169063d9b1b6e090610b0b9086906004016149ac565b600060405180830381600087803b158015610b2557600080fd5b505af1158015610b39573d6000803e3d6000fd5b50505050505050565b8133610b4e8282612676565b83610b58816126a0565b60055460ff16610b9a576001600160a01b03851660009081526004602052604090205460ff16610b9a5760405162461bcd60e51b815260040161045b90615033565b846001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b158015610bd557600080fd5b505af1158015610be9573d6000803e3d6000fd5b50505050846001600160a01b031663d7f1b27c610c326040518060400160405280601581526020017444656661756c7449737375616e63654d6f64756c6560581b815250612761565b6040518263ffffffff1660e01b8152600401610c4e91906149ac565b60206040518083038186803b158015610c6657600080fd5b505afa158015610c7a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c9e9190614563565b610cba5760405162461bcd60e51b815260040161045b90614ed5565b6060856001600160a01b031663b2494df36040518163ffffffff1660e01b815260040160006040518083038186803b158015610cf557600080fd5b505afa158015610d09573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610d31919081019061449c565b905060005b8151811015610db257818181518110610d4b57fe5b60200260200101516001600160a01b031663d9b1b6e0886040518263ffffffff1660e01b8152600401610d7e91906149ac565b600060405180830381600087803b158015610d9857600080fd5b505af1925050508015610da9575060015b50600101610d36565b5061059c8686612778565b610dc5612672565b6002546001600160a01b03908116911614610df25760405162461bcd60e51b815260040161045b90614ffe565b6002546040516000916001600160a01b0316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a3600280546001600160a01b0319169055565b600060026001541415610e615760405162461bcd60e51b815260040161045b90615248565b600260015584610e7081611c25565b610e78614275565b506001600160a01b03808716600090815260036020818152604092839020835160a08101855281548616815260018201548616928101839052600282015486169481019490945291820154909316606083015260040154608082015290610ef15760405162461bcd60e51b815260040161045b9061521c565b6000876001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b158015610f2c57600080fd5b505afa158015610f40573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f64919061484f565b90506000610f78888363ffffffff611a4e16565b9050600080610f878b86612529565b9250925050610f946142a3565b610fad8c8760200151886000015187878f60008c612848565b9050610fc28160000151878360600151611d28565b610fd681876020015188600001518c611d8e565b508051610fe590878585612081565b610ff3818760000151612236565b8051610ffe906122ec565b85600001516001600160a01b031686602001516001600160a01b03168d6001600160a01b03167f7cda30123ddfc96659344700585861a8670352b9cc86d1b1054d10083b1dcdd4846020015185606001518860006040516110629493929190614b1f565b60405180910390a45050600180559998505050505050505050565b3361108781611c4b565b33611091816111e8565b6001600160a01b038116600081815260036020819052604080832080546001600160a01b03199081168255600182018054821690556002820180548216905592810180549093169092556004918201839055805163b2494df360e01b815290516060949363b2494df39383810193919291829003018186803b15801561111657600080fd5b505afa15801561112a573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611152919081019061449c565b905060005b81518110156111d35781818151811061116c57fe5b60200260200101516001600160a01b031663e0799620846040518263ffffffff1660e01b815260040161119f91906149ac565b600060405180830381600087803b1580156111b957600080fd5b505af19250505080156111ca575060015b50600101611157565b50505050565b6002546001600160a01b031690565b6002600154141561120b5760405162461bcd60e51b815260040161045b90615248565b60026001558061121a81611c4b565b611223826122ec565b505060018055565b60055460ff1681565b8361123e816118e3565b611246614275565b506001600160a01b03808616600090815260036020818152604092839020835160a081018552815486168152600182015486169281019290925260028101548516938201939093529082015490921660608301526004015460808201528280156112c55750836001600160a01b031681602001516001600160a01b0316145b1561138c576040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906112fb90889030906004016149c0565b60206040518083038186803b15801561131357600080fd5b505afa158015611327573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061134b919061484f565b90506000811361136d5760405162461bcd60e51b815260040161045b90614c59565b600061137c8761047384611a28565b9050611389888483611d28565b50505b8261059c57836001600160a01b031681600001516001600160a01b0316146113c65760405162461bcd60e51b815260040161045b90614bf3565b6040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906113f790889030906004016149c0565b60206040518083038186803b15801561140f57600080fd5b505afa158015611423573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611447919061484f565b9050600081126114695760405162461bcd60e51b815260040161045b90614c59565b6000611484876104736105878560001963ffffffff611b5416565b90506105998884836000612081565b61149b612672565b6002546001600160a01b039081169116146114c85760405162461bcd60e51b815260040161045b90614ffe565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906114f89085906004016149ac565b60206040518083038186803b15801561151057600080fd5b505afa158015611524573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115489190614563565b8061156b57506001600160a01b03821660009081526004602052604090205460ff165b6115875760405162461bcd60e51b815260040161045b90614fd4565b6001600160a01b038216600081815260046020526040808220805460ff191685151590811790915590519092917f2035981b48691b10f6ac65174e570b4d0a8a889ae01bef3e5e7759ff9444f0c491a35050565b7f000000000000000000000000000000000000000000000000000000000000000081565b6003602081905260009182526040909120805460018201546002830154938301546004909301546001600160a01b039283169491831693918316929091169085565b8061164b81611c25565b611653614275565b506001600160a01b038083166000908152600360208181526040808420815160a0810183528154871681526001820154871693810184905260028201548716818401529381015490951660608401526004948501546080840152516370a0823160e01b8152919390916370a08231916116ce918891016149ac565b60206040518083038186803b1580156116e657600080fd5b505afa1580156116fa573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061171e919061484f565b9050600081116117405760405162461bcd60e51b815260040161045b90614f9d565b61174b848383611a81565b602082015161176c906001600160a01b03861690600063ffffffff61295b16565b6111d3846111e8565b60046020526000908152604090205460ff1681565b6000611794614275565b506001600160a01b03808316600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061180d5760405162461bcd60e51b815260040161045b9061521c565b61181681612acf565b9392505050565b611825612672565b6002546001600160a01b039081169116146118525760405162461bcd60e51b815260040161045b90614ffe565b6001600160a01b0381166118785760405162461bcd60e51b815260040161045b90614cd8565b6002546040516001600160a01b038084169216907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3600280546001600160a01b0319166001600160a01b0392909216919091179055565b6000546001600160a01b031681565b6002604051631ade272960e11b81526001600160a01b038316906335bc4e52906119119033906004016149ac565b60206040518083038186803b15801561192957600080fd5b505afa15801561193d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119619190614830565b600281111561196c57fe5b146119895760405162461bcd60e51b815260040161045b90614e9e565b6000546040516342f6e38960e01b81526001600160a01b03909116906342f6e389906119b99033906004016149ac565b60206040518083038186803b1580156119d157600080fd5b505afa1580156119e5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a099190614563565b611a255760405162461bcd60e51b815260040161045b9061515e565b50565b600080821215611a4a5760405162461bcd60e51b815260040161045b90614e69565b5090565b6000611a78670de0b6b3a7640000611a6c858563ffffffff612ad616565b9063ffffffff612b1016565b90505b92915050565b6020820151611ac2906001600160a01b038516907f00000000000000000000000000000000000000000000000000000000000000008463ffffffff612b5216565b6040516310adc72560e21b815273__$68b4132a7897cba73622ed001dedc8ba85$__906342b71c9490611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b60006040518083038186803b158015611b4057600080fd5b505af4158015610b39573d6000803e3d6000fd5b600082611b6357506000611a7b565b82600019148015611b775750600160ff1b82145b15611b945760405162461bcd60e51b815260040161045b90615061565b82820282848281611ba157fe5b0514611a785760405162461bcd60e51b815260040161045b90615061565b60405163169bcf0960e11b815273__$68b4132a7897cba73622ed001dedc8ba85$__90632d379e1290611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b611c2f8133612c19565b611c4b5760405162461bcd60e51b815260040161045b906151e5565b611c5481612ca7565b611a255760405162461bcd60e51b815260040161045b90614c90565b611c786142a3565b6000886001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b158015611cb357600080fd5b505afa158015611cc7573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ceb919061484f565b9050611d1b898989611d038a8663ffffffff611a4e16565b611d138a8763ffffffff611a4e16565b898988612848565b9998505050505050505050565b604051631007f97160e21b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063401fe5c490611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b60008085600001519050600086606001519050611e328688602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b158015611de257600080fd5b505afa158015611df6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e1a9190614428565b6001600160a01b03851691908463ffffffff612b5216565b600080606089602001516001600160a01b031663e171fcab8a8a88888f608001518d6040518763ffffffff1660e01b8152600401611e7596959493929190614a0d565b60006040518083038186803b158015611e8d57600080fd5b505afa158015611ea1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611ec99190810190614444565b925092509250846001600160a01b0316638f6f03328484846040518463ffffffff1660e01b8152600401611eff93929190614acd565b600060405180830381600087803b158015611f1957600080fd5b505af1158015611f2d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611f55919081019061457f565b506000611fe88b60e001518a6001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401611f8c91906149ac565b60206040518083038186803b158015611fa457600080fd5b505afa158015611fb8573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611fdc919061484f565b9063ffffffff61203f16565b90508a6080015181101561200e5760405162461bcd60e51b815260040161045b90614de2565b9a9950505050505050505050565b60008061202a600084612dab565b9050612037858583612e40565b949350505050565b6000611a7883836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250612ee7565b80156121635782516120c5906001600160a01b038616907f00000000000000000000000000000000000000000000000000000000000000008563ffffffff612b5216565b60405163354efce360e01b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063354efce39061212e906001600160a01b038816907f00000000000000000000000000000000000000000000000000000000000000009088906000908890600401614b45565b60006040518083038186803b15801561214657600080fd5b505af415801561215a573d6000803e3d6000fd5b505050506111d3565b82516121a1906001600160a01b038616907f00000000000000000000000000000000000000000000000000000000000000008563ffffffff612b5216565b60405163354efce360e01b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063354efce39061220a906001600160a01b038816907f00000000000000000000000000000000000000000000000000000000000000009088908890600090600401614b45565b60006040518083038186803b15801561222257600080fd5b505af4158015610599573d6000803e3d6000fd5b81516040516370a0823160e01b81526000916001600160a01b038416916370a0823191612265916004016149ac565b60206040518083038186803b15801561227d57600080fd5b505afa158015612291573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906122b5919061484f565b90508260e00151811461078457604083015160e0840151845161059c926001600160a01b039091169185919063ffffffff612f1316565b6122f4614275565b506001600160a01b03808216600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061236d5760405162461bcd60e51b815260040161045b9061521c565b6000826001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156123a857600080fd5b505afa1580156123bc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123e0919061484f565b90506000806123f0858585613054565b915091506000856001600160a01b03166322ebeba48660200151306040518363ffffffff1660e01b81526004016124289291906149c0565b60206040518083038186803b15801561244057600080fd5b505afa158015612454573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612478919061484f565b905082811461249057612490868660200151856130c6565b84516040516308bafae960e21b81526000916001600160a01b038916916322ebeba4916124c19130906004016149c0565b60206040518083038186803b1580156124d957600080fd5b505afa1580156124ed573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612511919061484f565b9050808314610b3957610b39878760000151856130c6565b60008060008061253885612acf565b905061254261430c565b6040516349e2903160e11b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906393c52062906125909085908b90600401614b08565b60606040518083038186803b1580156125a857600080fd5b505afa1580156125bc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906125e091906148f5565b905060008061261e6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168963ffffffff6130f516565b6020870151919550935061264592506001600160801b03169050838363ffffffff61338216565b955082604001516001600160801b0316965082602001516001600160801b03169450505050509250925092565b3390565b6126808282612c19565b61269c5760405162461bcd60e51b815260040161045b906151e5565b5050565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906126d09084906004016149ac565b60206040518083038186803b1580156126e857600080fd5b505afa1580156126fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127209190614563565b61273c5760405162461bcd60e51b815260040161045b906151a2565b612745816133b9565b611a255760405162461bcd60e51b815260040161045b90614d55565b60008061276d836133e8565b9050611816816133f3565b6000612783826134b0565b6001600160a01b03848116600081815260036020818152604092839020885181546001600160a01b0319908116918816919091178255918901516001820180548416918816919091179055888401516002820180548416918816919091179055606089015192810180549092169290951691909117905560808601516004909301929092559051919250907ffc8bae3ed1ee6eb61577be9bbfed36601a07b31902c2e2ff54e924d8ecb3f6c99061283b908490614aff565b60405180910390a2505050565b6128506142a3565b6128586142a3565b6040518061010001604052808b6001600160a01b0316815260200161287c87612761565b6001600160a01b03168152602001848152602001888152602001878152602001856128a7578a6128a9565b895b6001600160a01b03168152602001856128c257896128c4565b8a5b6001600160a01b03168152602001896001600160a01b03166370a082318d6040518263ffffffff1660e01b81526004016128fe91906149ac565b60206040518083038186803b15801561291657600080fd5b505afa15801561292a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061294e919061484f565b90529050611d1b81613597565b6000612967848461366a565b9050801580156129775750600082115b156129ee5761298684846136f1565b6129e9576040516304e3532760e41b81526001600160a01b03851690634e353270906129b69086906004016149ac565b600060405180830381600087803b1580156129d057600080fd5b505af11580156129e4573d6000803e3d6000fd5b505050505b612a6b565b8080156129f9575081155b15612a6b57612a0884846136f1565b612a6b57604051636f86c89760e01b81526001600160a01b03851690636f86c89790612a389086906004016149ac565b600060405180830381600087803b158015612a5257600080fd5b505af1158015612a66573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d1784612a848561377d565b6040518363ffffffff1660e01b8152600401612aa1929190614ab4565b600060405180830381600087803b158015612abb57600080fd5b505af1158015610599573d6000803e3d6000fd5b60a0902090565b600082612ae557506000611a7b565b82820282848281612af257fe5b0414611a785760405162461bcd60e51b815260040161045b90614f5c565b6000611a7883836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f0000000000008152506137a2565b60608282604051602401612b67929190614ab4565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f033290612bc39087906000908690600401614acd565b600060405180830381600087803b158015612bdd57600080fd5b505af1158015612bf1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261059c919081019061457f565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b158015612c5e57600080fd5b505afa158015612c72573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612c969190614428565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec90612cd89085906004016149ac565b60206040518083038186803b158015612cf057600080fd5b505afa158015612d04573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612d289190614563565b8015611a7b57506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90612d5b9030906004016149ac565b60206040518083038186803b158015612d7357600080fd5b505afa158015612d87573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a7b9190614563565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90612dde9030908890600401614ab4565b60206040518083038186803b158015612df657600080fd5b505afa158015612e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612e2e919061484f565b9050612037838263ffffffff611a4e16565b801561078457610784826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015612e9757600080fd5b505afa158015612eab573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612ecf9190614428565b6001600160a01b03861691908463ffffffff6137d916565b60008184841115612f0b5760405162461bcd60e51b815260040161045b9190614bb4565b505050900390565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401612f4591906149ac565b60206040518083038186803b158015612f5d57600080fd5b505afa158015612f71573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612f95919061484f565b90506000613018896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401612fc891906149ac565b60206040518083038186803b158015612fe057600080fd5b505afa158015612ff4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610587919061484f565b9050600082156130355761302e8888858561391b565b9050613039565b5060005b6130448a8a8361295b565b9199909850909650945050505050565b60008082613067575060009050806130be565b6000806130748787612529565b50909250905061309261308d838763ffffffff61396a16565b61377d565b93506130b96000196130ad61308d848963ffffffff61398816565b9063ffffffff611b5416565b925050505b935093915050565b604080516020810190915260008152610784906001600160a01b0385169084903090859063ffffffff6139e716565b600080600080600061310686612acf565b905061311061432c565b604051632e3071cd60e11b81526001600160a01b03891690635c60e39a9061313c908590600401614aff565b60c06040518083038186803b15801561315457600080fd5b505afa158015613168573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061318c9190614867565b60808101519091506001600160801b0316420380158015906131ba575060408201516001600160801b031615155b80156131d2575060608801516001600160a01b031615155b1561334e576060880151604051638c00bf6b60e01b81526000916001600160a01b031690638c00bf6b9061320c908c908790600401615324565b60206040518083038186803b15801561322457600080fd5b505afa158015613238573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061325c919061484f565b9050600061328d613273838563ffffffff613fa016565b60408601516001600160801b03169063ffffffff61401216565b905061329881614027565b604085018051919091016001600160801b031690526132b681614027565b84516001600160801b0391018116855260a0850151161561334b5760006132f38560a001516001600160801b03168361401290919063ffffffff16565b905060006133288287600001516001600160801b03160387602001516001600160801b0316846140509092919063ffffffff16565b905061333381614027565b602087018051919091016001600160801b0316905250505b50505b508051602082015160408301516060909301516001600160801b039283169b9183169a509282169850911695509350505050565b600061203761339884600163ffffffff61408716565b6133ab84620f424063ffffffff61408716565b86919063ffffffff6140ac16565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790612d5b9030906004016149ac565b805160209091012090565b60008054819061340b906001600160a01b03166140d6565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401613438929190614ab4565b60206040518083038186803b15801561345057600080fd5b505afa158015613464573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906134889190614428565b90506001600160a01b038116611a7b5760405162461bcd60e51b815260040161045b90614db3565b60006134bb82612acf565b90506134c561432c565b604051632e3071cd60e11b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690635c60e39a90613511908590600401614aff565b60c06040518083038186803b15801561352957600080fd5b505afa15801561353d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906135619190614867565b905080608001516001600160801b0316600014156135915760405162461bcd60e51b815260040161045b90614bc7565b50919050565b80516001600160a01b03908116600090815260036020526040902060010154166135d35760405162461bcd60e51b815260040161045b90614e0d565b80516001600160a01b039081166000908152600360205260409020541661360c5760405162461bcd60e51b815260040161045b90614e3d565b8060c001516001600160a01b03168160a001516001600160a01b031614156136465760405162461bcd60e51b815260040161045b906152ad565b6000816060015111611a255760405162461bcd60e51b815260040161045b90614d8c565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b815260040161369991906149ac565b60206040518083038186803b1580156136b157600080fd5b505afa1580156136c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906136e9919061484f565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b815260040161372091906149ac565b60006040518083038186803b15801561373857600080fd5b505afa15801561374c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052613774919081019061449c565b51119392505050565b6000600160ff1b8210611a4a5760405162461bcd60e51b815260040161045b90615116565b600081836137c35760405162461bcd60e51b815260040161045b9190614bb4565b5060008385816137cf57fe5b0495945050505050565b80156111d3576040516370a0823160e01b81526000906001600160a01b038516906370a082319061380e9088906004016149ac565b60206040518083038186803b15801561382657600080fd5b505afa15801561383a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061385e919061484f565b905061386c85858585614155565b6040516370a0823160e01b81526000906001600160a01b038616906370a082319061389b9089906004016149ac565b60206040518083038186803b1580156138b357600080fd5b505afa1580156138c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906138eb919061484f565b90506138fd828463ffffffff61203f16565b811461059c5760405162461bcd60e51b815260040161045b906150a8565b60008061393e613931848863ffffffff611a4e16565b869063ffffffff61203f16565b905061396086613954868463ffffffff61203f16565b9063ffffffff61396a16565b9695505050505050565b6000611a7882611a6c85670de0b6b3a764000063ffffffff612ad616565b6000816139a75760405162461bcd60e51b815260040161045b906152fa565b600083116139b6576000611a78565b611a7860016139db84611a6c83611fdc89670de0b6b3a764000063ffffffff612ad616565b9063ffffffff61408716565b8115613cdc5760405163df5e9b2960e01b81526001600160a01b0386169063df5e9b2990613a199087906004016149ac565b60206040518083038186803b158015613a3157600080fd5b505afa158015613a45573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613a699190614563565b613b30576040516304e3532760e41b81526001600160a01b03861690634e35327090613a999087906004016149ac565b600060405180830381600087803b158015613ab357600080fd5b505af1158015613ac7573d6000803e3d6000fd5b505060405163ea0ee55960e01b81526001600160a01b038816925063ea0ee5599150613af990879087906004016149c0565b600060405180830381600087803b158015613b1357600080fd5b505af1158015613b27573d6000803e3d6000fd5b50505050613c13565b604051637d96659360e01b81526001600160a01b03861690637d96659390613b5e90879087906004016149c0565b60206040518083038186803b158015613b7657600080fd5b505afa158015613b8a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613bae9190614563565b613c135760405163ea0ee55960e01b81526001600160a01b0386169063ea0ee55990613be090879087906004016149c0565b600060405180830381600087803b158015613bfa57600080fd5b505af1158015613c0e573d6000803e3d6000fd5b505050505b6040516363a90fc160e01b81526001600160a01b038616906363a90fc190613c4390879087908790600401614a90565b600060405180830381600087803b158015613c5d57600080fd5b505af1158015613c71573d6000803e3d6000fd5b50506040516326898fe160e01b81526001600160a01b03881692506326898fe19150613ca590879087908690600401614a5b565b600060405180830381600087803b158015613cbf57600080fd5b505af1158015613cd3573d6000803e3d6000fd5b50505050613f99565b805115613cfb5760405162461bcd60e51b815260040161045b906150df565b6040516308bafae960e21b81526001600160a01b038616906322ebeba490613d2990879087906004016149c0565b60206040518083038186803b158015613d4157600080fd5b505afa158015613d55573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613d79919061484f565b15613f995760405163a7bdad0360e01b81526060906001600160a01b0387169063a7bdad0390613dad9088906004016149ac565b60006040518083038186803b158015613dc557600080fd5b505afa158015613dd9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052613e01919081019061449c565b6040516366cb8d2f60e01b81529091506001600160a01b038716906366cb8d2f90613e309088906004016149ac565b60206040518083038186803b158015613e4857600080fd5b505afa158015613e5c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613e80919061484f565b158015613e8e575080516001145b15613f3757836001600160a01b031681600081518110613eaa57fe5b60200260200101516001600160a01b031614613ed85760405162461bcd60e51b815260040161045b90614f0c565b604051636f86c89760e01b81526001600160a01b03871690636f86c89790613f049088906004016149ac565b600060405180830381600087803b158015613f1e57600080fd5b505af1158015613f32573d6000803e3d6000fd5b505050505b60405163acf3f07760e01b81526001600160a01b0387169063acf3f07790613f6590889088906004016149c0565b600060405180830381600087803b158015613f7f57600080fd5b505af1158015613f93573d6000803e3d6000fd5b50505050505b5050505050565b600080613fb3848463ffffffff612ad616565b90506000613fda8280613fd5670de0b6b3a7640000600263ffffffff612ad616565b61425f565b90506000613ffc8284613fd5670de0b6b3a7640000600363ffffffff612ad616565b9050613960816139db858563ffffffff61408716565b6000611a788383670de0b6b3a764000061425f565b60006001600160801b03821115611a4a5760405162461bcd60e51b815260040161045b9061527f565b600061203761406883620f424063ffffffff61408716565b61407985600163ffffffff61408716565b86919063ffffffff61425f16565b600082820183811015611a785760405162461bcd60e51b815260040161045b90614d1e565b600061203782611a6c6140c682600163ffffffff61203f16565b6139db888863ffffffff612ad616565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced690614105908490600401614aff565b60206040518083038186803b15801561411d57600080fd5b505afa158015614131573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a7b9190614428565b80156111d35760608282604051602401614170929190614ab4565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506060906001600160a01b03871690638f6f0332906141cf9088906000908790600401614acd565b600060405180830381600087803b1580156141e957600080fd5b505af11580156141fd573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052614225919081019061457f565b80519091501561059c57808060200190518101906142439190614563565b61059c5760405162461bcd60e51b815260040161045b90614c2a565b600061203782611a6c868663ffffffff612ad616565b6040805160a08101825260008082526020820181905291810182905260608101829052608081019190915290565b60405180610100016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160006001600160a01b0316815260200160006001600160a01b03168152602001600081525090565b604080516060810182526000808252602082018190529181019190915290565b6040805160c081018252600080825260208201819052918101829052606081018290526080810182905260a081019190915290565b8051611a7b81615418565b600082601f83011261437c578081fd5b813561438f61438a826153c8565b6153a1565b91508082528360208285010111156143a657600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126143cf578081fd5b81516143dd61438a826153c8565b91508082528360208285010111156143f457600080fd5b6144058160208401602086016153ec565b5092915050565b60006020828403121561441d578081fd5b8135611a7881615418565b600060208284031215614439578081fd5b8151611a7881615418565b600080600060608486031215614458578182fd5b835161446381615418565b60208501516040860151919450925067ffffffffffffffff811115614486578182fd5b614492868287016143bf565b9150509250925092565b600060208083850312156144ae578182fd5b825167ffffffffffffffff808211156144c5578384fd5b81850186601f8201126144d6578485fd5b80519250818311156144e6578485fd5b83830291506144f68483016153a1565b8381528481019082860184840187018a1015614510578788fd5b8794505b8585101561453a576145268a82614361565b835260019490940193918601918601614514565b5098975050505050505050565b600060208284031215614558578081fd5b8135611a788161542d565b600060208284031215614574578081fd5b8151611a788161542d565b600060208284031215614590578081fd5b815167ffffffffffffffff8111156145a6578182fd5b612037848285016143bf565b600080604083850312156145c4578182fd5b82356145cf81615418565b915060208301356145df8161542d565b809150509250929050565b600080604083850312156145fc578182fd5b823561460781615418565b915060208301356145df81615418565b60008082840360c081121561462a578283fd5b833561463581615418565b925060a0601f1982011215614648578182fd5b5061465360a06153a1565b602084013561466181615418565b8152604084013561467181615418565b6020820152606084013561468481615418565b6040820152608084013561469781615418565b606082015260a0939093013560808401525092909150565b600080604083850312156146c1578182fd5b82356146cc81615418565b946020939093013593505050565b600080600080608085870312156146ef578182fd5b84356146fa81615418565b935060208501359250604085013561471181615418565b915060608501356147218161542d565b939692955090935050565b60008060008060808587031215614741578182fd5b843561474c81615418565b935060208501359250604085013567ffffffffffffffff8082111561476f578384fd5b61477b8883890161436c565b93506060870135915080821115614790578283fd5b5061479d8782880161436c565b91505092959194509250565b600080600080600060a086880312156147c0578283fd5b85356147cb81615418565b94506020860135935060408601359250606086013567ffffffffffffffff808211156147f5578283fd5b61480189838a0161436c565b93506080880135915080821115614816578283fd5b506148238882890161436c565b9150509295509295909350565b600060208284031215614841578081fd5b815160038110611a78578182fd5b600060208284031215614860578081fd5b5051919050565b600060c08284031215614878578081fd5b61488260c06153a1565b825161488d8161543b565b8152602083015161489d8161543b565b602082015260408301516148b08161543b565b604082015260608301516148c38161543b565b606082015260808301516148d68161543b565b608082015260a08301516148e98161543b565b60a08201529392505050565b600060608284031215614906578081fd5b61491060606153a1565b8251815260208301516149228161543b565b602082015260408301516149358161543b565b60408201529392505050565b600081518084526149598160208601602086016153ec565b601f01601f19169290920160200192915050565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b6001600160a01b0391909116815260200190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039586168152938516602085015291841660408401529092166060820152608081019190915260a00190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a08201819052600090614a4f90830184614941565b98975050505050505050565b6001600160a01b03848116825283166020820152606060408201819052600090614a8790830184614941565b95945050505050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152614a876060830184614941565b901515815260200190565b90815260200190565b9182526001600160a01b0316602082015260400190565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b6001600160a01b038681168252851660208201526101208101614b6b604083018661496d565b60e082019390935261010001529392505050565b6001600160a01b038581168252841660208201526101008101614ba5604083018561496d565b8260e083015295945050505050565b600060208252611a786020830184614941565b60208082526012908201527113585c9ad95d081b9bdd0818dc99585d195960721b604082015260600190565b60208082526017908201527f4465627420636f6d706f6e656e74206d69736d61746368000000000000000000604082015260600190565b602080825260159082015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b604082015260600190565b6020808252601a908201527f436f6d706f6e656e74206d757374206265206e65676174697665000000000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b60208082526026908201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160408201526564647265737360d01b606082015260800190565b6020808252601b908201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604082015260600190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b6020808252600d908201526c05175616e74697479206973203609c1b604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252601190820152700a6d8d2e0e0c2ceca40e8dede40d0d2ced607b1b604082015260600190565b60208082526016908201527510dbdb1b185d195c985b081b9bdd08195b98589b195960521b604082015260600190565b602080825260129082015271109bdc9c9bddc81b9bdd08195b98589b195960721b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526018908201527f4f6e6c7920746865206d6f64756c652063616e2063616c6c0000000000000000604082015260600190565b60208082526018908201527f49737375616e6365206e6f7420696e697469616c697a65640000000000000000604082015260600190565b60208082526030908201527f45787465726e616c20706f736974696f6e73206d757374206265203020746f2060408201526f1c995b5bdd994818dbdb5c1bdb995b9d60821b606082015260800190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b60208082526017908201527f436f6c6c61746572616c2062616c616e63652069732030000000000000000000604082015260600190565b60208082526010908201526f24b73b30b634b21029b2ba2a37b5b2b760811b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b6020808252601490820152732737ba1030b63637bbb2b21029b2ba2a37b5b2b760611b604082015260600190565b60208082526027908201527f5369676e6564536166654d6174683a206d756c7469706c69636174696f6e206f604082015266766572666c6f7760c81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526018908201527f5061737365642064617461206d757374206265206e756c6c0000000000000000604082015260600190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526024908201527f4d6f64756c65206d75737420626520656e61626c6564206f6e20636f6e74726f604082015263363632b960e11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b60208082526012908201527110dbdb1b185d195c985b081b9bdd081cd95d60721b604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b60208082526014908201527313505617d55253950c4c8e17d15610d15151115160621b604082015260600190565b6020808252602d908201527f436f6c6c61746572616c20616e6420626f72726f77206173736574206d75737460408201526c08189948191a5999995c995b9d609a1b606082015260800190565b60208082526010908201526f043616e742064697669646520627920360841b604082015260600190565b6101608101615333828561496d565b6001600160801b038084511660a08401528060208501511660c08401528060408501511660e084015280606085015116610100840152806080850151166101208401528060a085015116610140840152509392505050565b9283526020830191909152604082015260600190565b60405181810167ffffffffffffffff811182821017156153c057600080fd5b604052919050565b600067ffffffffffffffff8211156153de578081fd5b50601f01601f191660200190565b60005b838110156154075781810151838201526020016153ef565b838111156111d35750506000910152565b6001600160a01b0381168114611a2557600080fd5b8015158114611a2557600080fd5b6001600160801b0381168114611a2557600080fdfea2646970667358221220e6473242f805740368c97ae48e597ddf564bac546affb0d5435a79111ad75ae564736f6c634300060a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106101585760003560e01c8063a5841194116100c3578063da35e2831161007c578063da35e283146102aa578063e93353a3146102ce578063ee78244f146102e1578063f25fcc9f146102f4578063f2fde38b14610307578063f77c47911461031a57610158565b8063a584119414610254578063b1dd4d9214610267578063c137f4d71461027c578063c153dd0714610185578063c690a74c1461028f578063d8fbc833146102a257610158565b80635b136512116101155780635b136512146101e95780635c990306146101fc578063715018a61461020f5780637bb3526514610217578063847ef08d146102375780638da5cb5b1461023f57610158565b80630fb96b211461015d57806311976c04146101725780633fe6106b1461018557806348a2f01b146101985780635199e418146101c357806356b27e1a146101d6575b600080fd5b61017061016b3660046146da565b610322565b005b6101706101803660046147a9565b6105a4565b6101706101933660046146af565b610771565b6101ab6101a636600461440c565b610789565b6040516101ba9392919061538b565b60405180910390f35b6101706101d1366004614547565b610827565b6101706101e43660046147a9565b610899565b6101706101f73660046145ea565b610a3d565b61017061020a366004614617565b610b42565b610170610dbd565b61022a61022536600461472c565b610e3c565b6040516101ba9190614aff565b61017061107d565b6102476111d9565b6040516101ba91906149ac565b61017061026236600461440c565b6111e8565b61026f61122b565b6040516101ba9190614af4565b61017061028a3660046146da565b611234565b61017061029d3660046145b2565b611493565b6102476115db565b6102bd6102b836600461440c565b6115ff565b6040516101ba9594939291906149da565b6101706102dc36600461440c565b611641565b61026f6102ef36600461440c565b611775565b61022a61030236600461440c565b61178a565b61017061031536600461440c565b61181d565b6102476118d4565b8361032c816118e3565b610334614275565b506001600160a01b03808616600090815260036020818152604092839020835160a081018552815486168152600182015486169281019290925260028101548516938201939093529082015490921660608301526004015460808201528280156103b35750836001600160a01b031681602001516001600160a01b0316145b1561048f576040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906103e990889030906004016149c0565b60206040518083038186803b15801561040157600080fd5b505afa158015610415573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610439919061484f565b9050600081136104645760405162461bcd60e51b815260040161045b90614c59565b60405180910390fd5b600061047f8761047384611a28565b9063ffffffff611a4e16565b905061048c888483611a81565b50505b8261059c57836001600160a01b031681600001516001600160a01b0316146104c95760405162461bcd60e51b815260040161045b90614bf3565b6040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906104fa90889030906004016149c0565b60206040518083038186803b15801561051257600080fd5b505afa158015610526573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061054a919061484f565b90506000811261056c5760405162461bcd60e51b815260040161045b90614c59565b600061058c876104736105878560001963ffffffff611b5416565b611a28565b9050610599888483611bbf565b50505b505050505050565b600260015414156105c75760405162461bcd60e51b815260040161045b90615248565b6002600155846105d681611c25565b6105de614275565b506001600160a01b03808716600090815260036020818152604092839020835160a081018552815486168152600182015486169281018390526002820154861694810194909452918201549093166060830152600401546080820152906106575760405162461bcd60e51b815260040161045b9061521c565b61065f6142a3565b61067788836020015184600001518a8a8a6000611c70565b905061068c8160000151838360600151611d28565b60006106a2828460200151856000015188611d8e565b905060006106b58a85600001518461201c565b905060006106c9838363ffffffff61203f16565b90506106dc846000015186836000612081565b6106ea848660000151612236565b83516106f5906122ec565b84600001516001600160a01b031685602001516001600160a01b03168c6001600160a01b03167f7cda30123ddfc96659344700585861a8670352b9cc86d1b1054d10083b1dcdd48760200151886060015186886040516107589493929190614b1f565b60405180910390a4505060018055505050505050505050565b8161077b816118e3565b610784836111e8565b505050565b6000806000610796614275565b506001600160a01b03808516600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061080f5760405162461bcd60e51b815260040161045b9061521c565b6108198582612529565b935093509350509193909250565b61082f612672565b6002546001600160a01b0390811691161461085c5760405162461bcd60e51b815260040161045b90614ffe565b6005805460ff19168215159081179091556040517f563e1633136cdd43b8793897cb53ba2a9e31c18b3ae0b6827fbbb03b9902e6c690600090a250565b600260015414156108bc5760405162461bcd60e51b815260040161045b90615248565b6002600155846108cb81611c25565b6108d3614275565b506001600160a01b03808716600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061094c5760405162461bcd60e51b815260040161045b9061521c565b6109546142a3565b61096c88836000015184602001518a8a8a6001611c70565b90506109818160000151838360600151611bbf565b6000610997828460000151856020015188611d8e565b905060006109aa8a85602001518461201c565b905060006109be838363ffffffff61203f16565b90506109cf84600001518683611a81565b83516109da906122ec565b84602001516001600160a01b031685600001516001600160a01b03168c6001600160a01b03167f359f8b62a966cfd521a3815681266407201b20a7c334925faa49e7d9d5dd57ab8760200151886060015186886040516107589493929190614b1f565b81610a4781611c25565b6040516335fc6c9f60e21b81526001600160a01b0384169063d7f1b27c90610a739085906004016149ac565b60206040518083038186803b158015610a8b57600080fd5b505afa158015610a9f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ac39190614563565b610adf5760405162461bcd60e51b815260040161045b90614ed5565b6040516306cd8db760e51b81526001600160a01b0383169063d9b1b6e090610b0b9086906004016149ac565b600060405180830381600087803b158015610b2557600080fd5b505af1158015610b39573d6000803e3d6000fd5b50505050505050565b8133610b4e8282612676565b83610b58816126a0565b60055460ff16610b9a576001600160a01b03851660009081526004602052604090205460ff16610b9a5760405162461bcd60e51b815260040161045b90615033565b846001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b158015610bd557600080fd5b505af1158015610be9573d6000803e3d6000fd5b50505050846001600160a01b031663d7f1b27c610c326040518060400160405280601581526020017444656661756c7449737375616e63654d6f64756c6560581b815250612761565b6040518263ffffffff1660e01b8152600401610c4e91906149ac565b60206040518083038186803b158015610c6657600080fd5b505afa158015610c7a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c9e9190614563565b610cba5760405162461bcd60e51b815260040161045b90614ed5565b6060856001600160a01b031663b2494df36040518163ffffffff1660e01b815260040160006040518083038186803b158015610cf557600080fd5b505afa158015610d09573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610d31919081019061449c565b905060005b8151811015610db257818181518110610d4b57fe5b60200260200101516001600160a01b031663d9b1b6e0886040518263ffffffff1660e01b8152600401610d7e91906149ac565b600060405180830381600087803b158015610d9857600080fd5b505af1925050508015610da9575060015b50600101610d36565b5061059c8686612778565b610dc5612672565b6002546001600160a01b03908116911614610df25760405162461bcd60e51b815260040161045b90614ffe565b6002546040516000916001600160a01b0316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a3600280546001600160a01b0319169055565b600060026001541415610e615760405162461bcd60e51b815260040161045b90615248565b600260015584610e7081611c25565b610e78614275565b506001600160a01b03808716600090815260036020818152604092839020835160a08101855281548616815260018201548616928101839052600282015486169481019490945291820154909316606083015260040154608082015290610ef15760405162461bcd60e51b815260040161045b9061521c565b6000876001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b158015610f2c57600080fd5b505afa158015610f40573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f64919061484f565b90506000610f78888363ffffffff611a4e16565b9050600080610f878b86612529565b9250925050610f946142a3565b610fad8c8760200151886000015187878f60008c612848565b9050610fc28160000151878360600151611d28565b610fd681876020015188600001518c611d8e565b508051610fe590878585612081565b610ff3818760000151612236565b8051610ffe906122ec565b85600001516001600160a01b031686602001516001600160a01b03168d6001600160a01b03167f7cda30123ddfc96659344700585861a8670352b9cc86d1b1054d10083b1dcdd4846020015185606001518860006040516110629493929190614b1f565b60405180910390a45050600180559998505050505050505050565b3361108781611c4b565b33611091816111e8565b6001600160a01b038116600081815260036020819052604080832080546001600160a01b03199081168255600182018054821690556002820180548216905592810180549093169092556004918201839055805163b2494df360e01b815290516060949363b2494df39383810193919291829003018186803b15801561111657600080fd5b505afa15801561112a573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611152919081019061449c565b905060005b81518110156111d35781818151811061116c57fe5b60200260200101516001600160a01b031663e0799620846040518263ffffffff1660e01b815260040161119f91906149ac565b600060405180830381600087803b1580156111b957600080fd5b505af19250505080156111ca575060015b50600101611157565b50505050565b6002546001600160a01b031690565b6002600154141561120b5760405162461bcd60e51b815260040161045b90615248565b60026001558061121a81611c4b565b611223826122ec565b505060018055565b60055460ff1681565b8361123e816118e3565b611246614275565b506001600160a01b03808616600090815260036020818152604092839020835160a081018552815486168152600182015486169281019290925260028101548516938201939093529082015490921660608301526004015460808201528280156112c55750836001600160a01b031681602001516001600160a01b0316145b1561138c576040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906112fb90889030906004016149c0565b60206040518083038186803b15801561131357600080fd5b505afa158015611327573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061134b919061484f565b90506000811361136d5760405162461bcd60e51b815260040161045b90614c59565b600061137c8761047384611a28565b9050611389888483611d28565b50505b8261059c57836001600160a01b031681600001516001600160a01b0316146113c65760405162461bcd60e51b815260040161045b90614bf3565b6040516308bafae960e21b81526000906001600160a01b038816906322ebeba4906113f790889030906004016149c0565b60206040518083038186803b15801561140f57600080fd5b505afa158015611423573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611447919061484f565b9050600081126114695760405162461bcd60e51b815260040161045b90614c59565b6000611484876104736105878560001963ffffffff611b5416565b90506105998884836000612081565b61149b612672565b6002546001600160a01b039081169116146114c85760405162461bcd60e51b815260040161045b90614ffe565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906114f89085906004016149ac565b60206040518083038186803b15801561151057600080fd5b505afa158015611524573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115489190614563565b8061156b57506001600160a01b03821660009081526004602052604090205460ff165b6115875760405162461bcd60e51b815260040161045b90614fd4565b6001600160a01b038216600081815260046020526040808220805460ff191685151590811790915590519092917f2035981b48691b10f6ac65174e570b4d0a8a889ae01bef3e5e7759ff9444f0c491a35050565b7f000000000000000000000000000000000000000000000000000000000000000081565b6003602081905260009182526040909120805460018201546002830154938301546004909301546001600160a01b039283169491831693918316929091169085565b8061164b81611c25565b611653614275565b506001600160a01b038083166000908152600360208181526040808420815160a0810183528154871681526001820154871693810184905260028201548716818401529381015490951660608401526004948501546080840152516370a0823160e01b8152919390916370a08231916116ce918891016149ac565b60206040518083038186803b1580156116e657600080fd5b505afa1580156116fa573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061171e919061484f565b9050600081116117405760405162461bcd60e51b815260040161045b90614f9d565b61174b848383611a81565b602082015161176c906001600160a01b03861690600063ffffffff61295b16565b6111d3846111e8565b60046020526000908152604090205460ff1681565b6000611794614275565b506001600160a01b03808316600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061180d5760405162461bcd60e51b815260040161045b9061521c565b61181681612acf565b9392505050565b611825612672565b6002546001600160a01b039081169116146118525760405162461bcd60e51b815260040161045b90614ffe565b6001600160a01b0381166118785760405162461bcd60e51b815260040161045b90614cd8565b6002546040516001600160a01b038084169216907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3600280546001600160a01b0319166001600160a01b0392909216919091179055565b6000546001600160a01b031681565b6002604051631ade272960e11b81526001600160a01b038316906335bc4e52906119119033906004016149ac565b60206040518083038186803b15801561192957600080fd5b505afa15801561193d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906119619190614830565b600281111561196c57fe5b146119895760405162461bcd60e51b815260040161045b90614e9e565b6000546040516342f6e38960e01b81526001600160a01b03909116906342f6e389906119b99033906004016149ac565b60206040518083038186803b1580156119d157600080fd5b505afa1580156119e5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a099190614563565b611a255760405162461bcd60e51b815260040161045b9061515e565b50565b600080821215611a4a5760405162461bcd60e51b815260040161045b90614e69565b5090565b6000611a78670de0b6b3a7640000611a6c858563ffffffff612ad616565b9063ffffffff612b1016565b90505b92915050565b6020820151611ac2906001600160a01b038516907f00000000000000000000000000000000000000000000000000000000000000008463ffffffff612b5216565b6040516310adc72560e21b815273__$68b4132a7897cba73622ed001dedc8ba85$__906342b71c9490611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b60006040518083038186803b158015611b4057600080fd5b505af4158015610b39573d6000803e3d6000fd5b600082611b6357506000611a7b565b82600019148015611b775750600160ff1b82145b15611b945760405162461bcd60e51b815260040161045b90615061565b82820282848281611ba157fe5b0514611a785760405162461bcd60e51b815260040161045b90615061565b60405163169bcf0960e11b815273__$68b4132a7897cba73622ed001dedc8ba85$__90632d379e1290611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b611c2f8133612c19565b611c4b5760405162461bcd60e51b815260040161045b906151e5565b611c5481612ca7565b611a255760405162461bcd60e51b815260040161045b90614c90565b611c786142a3565b6000886001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b158015611cb357600080fd5b505afa158015611cc7573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ceb919061484f565b9050611d1b898989611d038a8663ffffffff611a4e16565b611d138a8763ffffffff611a4e16565b898988612848565b9998505050505050505050565b604051631007f97160e21b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063401fe5c490611b28906001600160a01b038716907f00000000000000000000000000000000000000000000000000000000000000009087908790600401614b7f565b60008085600001519050600086606001519050611e328688602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b158015611de257600080fd5b505afa158015611df6573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e1a9190614428565b6001600160a01b03851691908463ffffffff612b5216565b600080606089602001516001600160a01b031663e171fcab8a8a88888f608001518d6040518763ffffffff1660e01b8152600401611e7596959493929190614a0d565b60006040518083038186803b158015611e8d57600080fd5b505afa158015611ea1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611ec99190810190614444565b925092509250846001600160a01b0316638f6f03328484846040518463ffffffff1660e01b8152600401611eff93929190614acd565b600060405180830381600087803b158015611f1957600080fd5b505af1158015611f2d573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052611f55919081019061457f565b506000611fe88b60e001518a6001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401611f8c91906149ac565b60206040518083038186803b158015611fa457600080fd5b505afa158015611fb8573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611fdc919061484f565b9063ffffffff61203f16565b90508a6080015181101561200e5760405162461bcd60e51b815260040161045b90614de2565b9a9950505050505050505050565b60008061202a600084612dab565b9050612037858583612e40565b949350505050565b6000611a7883836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250612ee7565b80156121635782516120c5906001600160a01b038616907f00000000000000000000000000000000000000000000000000000000000000008563ffffffff612b5216565b60405163354efce360e01b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063354efce39061212e906001600160a01b038816907f00000000000000000000000000000000000000000000000000000000000000009088906000908890600401614b45565b60006040518083038186803b15801561214657600080fd5b505af415801561215a573d6000803e3d6000fd5b505050506111d3565b82516121a1906001600160a01b038616907f00000000000000000000000000000000000000000000000000000000000000008563ffffffff612b5216565b60405163354efce360e01b815273__$68b4132a7897cba73622ed001dedc8ba85$__9063354efce39061220a906001600160a01b038816907f00000000000000000000000000000000000000000000000000000000000000009088908890600090600401614b45565b60006040518083038186803b15801561222257600080fd5b505af4158015610599573d6000803e3d6000fd5b81516040516370a0823160e01b81526000916001600160a01b038416916370a0823191612265916004016149ac565b60206040518083038186803b15801561227d57600080fd5b505afa158015612291573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906122b5919061484f565b90508260e00151811461078457604083015160e0840151845161059c926001600160a01b039091169185919063ffffffff612f1316565b6122f4614275565b506001600160a01b03808216600090815260036020818152604092839020835160a0810185528154861681526001820154861692810183905260028201548616948101949094529182015490931660608301526004015460808201529061236d5760405162461bcd60e51b815260040161045b9061521c565b6000826001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156123a857600080fd5b505afa1580156123bc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123e0919061484f565b90506000806123f0858585613054565b915091506000856001600160a01b03166322ebeba48660200151306040518363ffffffff1660e01b81526004016124289291906149c0565b60206040518083038186803b15801561244057600080fd5b505afa158015612454573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612478919061484f565b905082811461249057612490868660200151856130c6565b84516040516308bafae960e21b81526000916001600160a01b038916916322ebeba4916124c19130906004016149c0565b60206040518083038186803b1580156124d957600080fd5b505afa1580156124ed573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612511919061484f565b9050808314610b3957610b39878760000151856130c6565b60008060008061253885612acf565b905061254261430c565b6040516349e2903160e11b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906393c52062906125909085908b90600401614b08565b60606040518083038186803b1580156125a857600080fd5b505afa1580156125bc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906125e091906148f5565b905060008061261e6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168963ffffffff6130f516565b6020870151919550935061264592506001600160801b03169050838363ffffffff61338216565b955082604001516001600160801b0316965082602001516001600160801b03169450505050509250925092565b3390565b6126808282612c19565b61269c5760405162461bcd60e51b815260040161045b906151e5565b5050565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906126d09084906004016149ac565b60206040518083038186803b1580156126e857600080fd5b505afa1580156126fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127209190614563565b61273c5760405162461bcd60e51b815260040161045b906151a2565b612745816133b9565b611a255760405162461bcd60e51b815260040161045b90614d55565b60008061276d836133e8565b9050611816816133f3565b6000612783826134b0565b6001600160a01b03848116600081815260036020818152604092839020885181546001600160a01b0319908116918816919091178255918901516001820180548416918816919091179055888401516002820180548416918816919091179055606089015192810180549092169290951691909117905560808601516004909301929092559051919250907ffc8bae3ed1ee6eb61577be9bbfed36601a07b31902c2e2ff54e924d8ecb3f6c99061283b908490614aff565b60405180910390a2505050565b6128506142a3565b6128586142a3565b6040518061010001604052808b6001600160a01b0316815260200161287c87612761565b6001600160a01b03168152602001848152602001888152602001878152602001856128a7578a6128a9565b895b6001600160a01b03168152602001856128c257896128c4565b8a5b6001600160a01b03168152602001896001600160a01b03166370a082318d6040518263ffffffff1660e01b81526004016128fe91906149ac565b60206040518083038186803b15801561291657600080fd5b505afa15801561292a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061294e919061484f565b90529050611d1b81613597565b6000612967848461366a565b9050801580156129775750600082115b156129ee5761298684846136f1565b6129e9576040516304e3532760e41b81526001600160a01b03851690634e353270906129b69086906004016149ac565b600060405180830381600087803b1580156129d057600080fd5b505af11580156129e4573d6000803e3d6000fd5b505050505b612a6b565b8080156129f9575081155b15612a6b57612a0884846136f1565b612a6b57604051636f86c89760e01b81526001600160a01b03851690636f86c89790612a389086906004016149ac565b600060405180830381600087803b158015612a5257600080fd5b505af1158015612a66573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d1784612a848561377d565b6040518363ffffffff1660e01b8152600401612aa1929190614ab4565b600060405180830381600087803b158015612abb57600080fd5b505af1158015610599573d6000803e3d6000fd5b60a0902090565b600082612ae557506000611a7b565b82820282848281612af257fe5b0414611a785760405162461bcd60e51b815260040161045b90614f5c565b6000611a7883836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f0000000000008152506137a2565b60608282604051602401612b67929190614ab4565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f033290612bc39087906000908690600401614acd565b600060405180830381600087803b158015612bdd57600080fd5b505af1158015612bf1573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261059c919081019061457f565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b158015612c5e57600080fd5b505afa158015612c72573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612c969190614428565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec90612cd89085906004016149ac565b60206040518083038186803b158015612cf057600080fd5b505afa158015612d04573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612d289190614563565b8015611a7b57506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90612d5b9030906004016149ac565b60206040518083038186803b158015612d7357600080fd5b505afa158015612d87573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a7b9190614563565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90612dde9030908890600401614ab4565b60206040518083038186803b158015612df657600080fd5b505afa158015612e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612e2e919061484f565b9050612037838263ffffffff611a4e16565b801561078457610784826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015612e9757600080fd5b505afa158015612eab573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612ecf9190614428565b6001600160a01b03861691908463ffffffff6137d916565b60008184841115612f0b5760405162461bcd60e51b815260040161045b9190614bb4565b505050900390565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401612f4591906149ac565b60206040518083038186803b158015612f5d57600080fd5b505afa158015612f71573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612f95919061484f565b90506000613018896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401612fc891906149ac565b60206040518083038186803b158015612fe057600080fd5b505afa158015612ff4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610587919061484f565b9050600082156130355761302e8888858561391b565b9050613039565b5060005b6130448a8a8361295b565b9199909850909650945050505050565b60008082613067575060009050806130be565b6000806130748787612529565b50909250905061309261308d838763ffffffff61396a16565b61377d565b93506130b96000196130ad61308d848963ffffffff61398816565b9063ffffffff611b5416565b925050505b935093915050565b604080516020810190915260008152610784906001600160a01b0385169084903090859063ffffffff6139e716565b600080600080600061310686612acf565b905061311061432c565b604051632e3071cd60e11b81526001600160a01b03891690635c60e39a9061313c908590600401614aff565b60c06040518083038186803b15801561315457600080fd5b505afa158015613168573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061318c9190614867565b60808101519091506001600160801b0316420380158015906131ba575060408201516001600160801b031615155b80156131d2575060608801516001600160a01b031615155b1561334e576060880151604051638c00bf6b60e01b81526000916001600160a01b031690638c00bf6b9061320c908c908790600401615324565b60206040518083038186803b15801561322457600080fd5b505afa158015613238573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061325c919061484f565b9050600061328d613273838563ffffffff613fa016565b60408601516001600160801b03169063ffffffff61401216565b905061329881614027565b604085018051919091016001600160801b031690526132b681614027565b84516001600160801b0391018116855260a0850151161561334b5760006132f38560a001516001600160801b03168361401290919063ffffffff16565b905060006133288287600001516001600160801b03160387602001516001600160801b0316846140509092919063ffffffff16565b905061333381614027565b602087018051919091016001600160801b0316905250505b50505b508051602082015160408301516060909301516001600160801b039283169b9183169a509282169850911695509350505050565b600061203761339884600163ffffffff61408716565b6133ab84620f424063ffffffff61408716565b86919063ffffffff6140ac16565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790612d5b9030906004016149ac565b805160209091012090565b60008054819061340b906001600160a01b03166140d6565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401613438929190614ab4565b60206040518083038186803b15801561345057600080fd5b505afa158015613464573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906134889190614428565b90506001600160a01b038116611a7b5760405162461bcd60e51b815260040161045b90614db3565b60006134bb82612acf565b90506134c561432c565b604051632e3071cd60e11b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690635c60e39a90613511908590600401614aff565b60c06040518083038186803b15801561352957600080fd5b505afa15801561353d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906135619190614867565b905080608001516001600160801b0316600014156135915760405162461bcd60e51b815260040161045b90614bc7565b50919050565b80516001600160a01b03908116600090815260036020526040902060010154166135d35760405162461bcd60e51b815260040161045b90614e0d565b80516001600160a01b039081166000908152600360205260409020541661360c5760405162461bcd60e51b815260040161045b90614e3d565b8060c001516001600160a01b03168160a001516001600160a01b031614156136465760405162461bcd60e51b815260040161045b906152ad565b6000816060015111611a255760405162461bcd60e51b815260040161045b90614d8c565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b815260040161369991906149ac565b60206040518083038186803b1580156136b157600080fd5b505afa1580156136c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906136e9919061484f565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b815260040161372091906149ac565b60006040518083038186803b15801561373857600080fd5b505afa15801561374c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052613774919081019061449c565b51119392505050565b6000600160ff1b8210611a4a5760405162461bcd60e51b815260040161045b90615116565b600081836137c35760405162461bcd60e51b815260040161045b9190614bb4565b5060008385816137cf57fe5b0495945050505050565b80156111d3576040516370a0823160e01b81526000906001600160a01b038516906370a082319061380e9088906004016149ac565b60206040518083038186803b15801561382657600080fd5b505afa15801561383a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061385e919061484f565b905061386c85858585614155565b6040516370a0823160e01b81526000906001600160a01b038616906370a082319061389b9089906004016149ac565b60206040518083038186803b1580156138b357600080fd5b505afa1580156138c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906138eb919061484f565b90506138fd828463ffffffff61203f16565b811461059c5760405162461bcd60e51b815260040161045b906150a8565b60008061393e613931848863ffffffff611a4e16565b869063ffffffff61203f16565b905061396086613954868463ffffffff61203f16565b9063ffffffff61396a16565b9695505050505050565b6000611a7882611a6c85670de0b6b3a764000063ffffffff612ad616565b6000816139a75760405162461bcd60e51b815260040161045b906152fa565b600083116139b6576000611a78565b611a7860016139db84611a6c83611fdc89670de0b6b3a764000063ffffffff612ad616565b9063ffffffff61408716565b8115613cdc5760405163df5e9b2960e01b81526001600160a01b0386169063df5e9b2990613a199087906004016149ac565b60206040518083038186803b158015613a3157600080fd5b505afa158015613a45573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613a699190614563565b613b30576040516304e3532760e41b81526001600160a01b03861690634e35327090613a999087906004016149ac565b600060405180830381600087803b158015613ab357600080fd5b505af1158015613ac7573d6000803e3d6000fd5b505060405163ea0ee55960e01b81526001600160a01b038816925063ea0ee5599150613af990879087906004016149c0565b600060405180830381600087803b158015613b1357600080fd5b505af1158015613b27573d6000803e3d6000fd5b50505050613c13565b604051637d96659360e01b81526001600160a01b03861690637d96659390613b5e90879087906004016149c0565b60206040518083038186803b158015613b7657600080fd5b505afa158015613b8a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613bae9190614563565b613c135760405163ea0ee55960e01b81526001600160a01b0386169063ea0ee55990613be090879087906004016149c0565b600060405180830381600087803b158015613bfa57600080fd5b505af1158015613c0e573d6000803e3d6000fd5b505050505b6040516363a90fc160e01b81526001600160a01b038616906363a90fc190613c4390879087908790600401614a90565b600060405180830381600087803b158015613c5d57600080fd5b505af1158015613c71573d6000803e3d6000fd5b50506040516326898fe160e01b81526001600160a01b03881692506326898fe19150613ca590879087908690600401614a5b565b600060405180830381600087803b158015613cbf57600080fd5b505af1158015613cd3573d6000803e3d6000fd5b50505050613f99565b805115613cfb5760405162461bcd60e51b815260040161045b906150df565b6040516308bafae960e21b81526001600160a01b038616906322ebeba490613d2990879087906004016149c0565b60206040518083038186803b158015613d4157600080fd5b505afa158015613d55573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613d79919061484f565b15613f995760405163a7bdad0360e01b81526060906001600160a01b0387169063a7bdad0390613dad9088906004016149ac565b60006040518083038186803b158015613dc557600080fd5b505afa158015613dd9573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052613e01919081019061449c565b6040516366cb8d2f60e01b81529091506001600160a01b038716906366cb8d2f90613e309088906004016149ac565b60206040518083038186803b158015613e4857600080fd5b505afa158015613e5c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613e80919061484f565b158015613e8e575080516001145b15613f3757836001600160a01b031681600081518110613eaa57fe5b60200260200101516001600160a01b031614613ed85760405162461bcd60e51b815260040161045b90614f0c565b604051636f86c89760e01b81526001600160a01b03871690636f86c89790613f049088906004016149ac565b600060405180830381600087803b158015613f1e57600080fd5b505af1158015613f32573d6000803e3d6000fd5b505050505b60405163acf3f07760e01b81526001600160a01b0387169063acf3f07790613f6590889088906004016149c0565b600060405180830381600087803b158015613f7f57600080fd5b505af1158015613f93573d6000803e3d6000fd5b50505050505b5050505050565b600080613fb3848463ffffffff612ad616565b90506000613fda8280613fd5670de0b6b3a7640000600263ffffffff612ad616565b61425f565b90506000613ffc8284613fd5670de0b6b3a7640000600363ffffffff612ad616565b9050613960816139db858563ffffffff61408716565b6000611a788383670de0b6b3a764000061425f565b60006001600160801b03821115611a4a5760405162461bcd60e51b815260040161045b9061527f565b600061203761406883620f424063ffffffff61408716565b61407985600163ffffffff61408716565b86919063ffffffff61425f16565b600082820183811015611a785760405162461bcd60e51b815260040161045b90614d1e565b600061203782611a6c6140c682600163ffffffff61203f16565b6139db888863ffffffff612ad616565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced690614105908490600401614aff565b60206040518083038186803b15801561411d57600080fd5b505afa158015614131573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a7b9190614428565b80156111d35760608282604051602401614170929190614ab4565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506060906001600160a01b03871690638f6f0332906141cf9088906000908790600401614acd565b600060405180830381600087803b1580156141e957600080fd5b505af11580156141fd573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052614225919081019061457f565b80519091501561059c57808060200190518101906142439190614563565b61059c5760405162461bcd60e51b815260040161045b90614c2a565b600061203782611a6c868663ffffffff612ad616565b6040805160a08101825260008082526020820181905291810182905260608101829052608081019190915290565b60405180610100016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160006001600160a01b0316815260200160006001600160a01b03168152602001600081525090565b604080516060810182526000808252602082018190529181019190915290565b6040805160c081018252600080825260208201819052918101829052606081018290526080810182905260a081019190915290565b8051611a7b81615418565b600082601f83011261437c578081fd5b813561438f61438a826153c8565b6153a1565b91508082528360208285010111156143a657600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126143cf578081fd5b81516143dd61438a826153c8565b91508082528360208285010111156143f457600080fd5b6144058160208401602086016153ec565b5092915050565b60006020828403121561441d578081fd5b8135611a7881615418565b600060208284031215614439578081fd5b8151611a7881615418565b600080600060608486031215614458578182fd5b835161446381615418565b60208501516040860151919450925067ffffffffffffffff811115614486578182fd5b614492868287016143bf565b9150509250925092565b600060208083850312156144ae578182fd5b825167ffffffffffffffff808211156144c5578384fd5b81850186601f8201126144d6578485fd5b80519250818311156144e6578485fd5b83830291506144f68483016153a1565b8381528481019082860184840187018a1015614510578788fd5b8794505b8585101561453a576145268a82614361565b835260019490940193918601918601614514565b5098975050505050505050565b600060208284031215614558578081fd5b8135611a788161542d565b600060208284031215614574578081fd5b8151611a788161542d565b600060208284031215614590578081fd5b815167ffffffffffffffff8111156145a6578182fd5b612037848285016143bf565b600080604083850312156145c4578182fd5b82356145cf81615418565b915060208301356145df8161542d565b809150509250929050565b600080604083850312156145fc578182fd5b823561460781615418565b915060208301356145df81615418565b60008082840360c081121561462a578283fd5b833561463581615418565b925060a0601f1982011215614648578182fd5b5061465360a06153a1565b602084013561466181615418565b8152604084013561467181615418565b6020820152606084013561468481615418565b6040820152608084013561469781615418565b606082015260a0939093013560808401525092909150565b600080604083850312156146c1578182fd5b82356146cc81615418565b946020939093013593505050565b600080600080608085870312156146ef578182fd5b84356146fa81615418565b935060208501359250604085013561471181615418565b915060608501356147218161542d565b939692955090935050565b60008060008060808587031215614741578182fd5b843561474c81615418565b935060208501359250604085013567ffffffffffffffff8082111561476f578384fd5b61477b8883890161436c565b93506060870135915080821115614790578283fd5b5061479d8782880161436c565b91505092959194509250565b600080600080600060a086880312156147c0578283fd5b85356147cb81615418565b94506020860135935060408601359250606086013567ffffffffffffffff808211156147f5578283fd5b61480189838a0161436c565b93506080880135915080821115614816578283fd5b506148238882890161436c565b9150509295509295909350565b600060208284031215614841578081fd5b815160038110611a78578182fd5b600060208284031215614860578081fd5b5051919050565b600060c08284031215614878578081fd5b61488260c06153a1565b825161488d8161543b565b8152602083015161489d8161543b565b602082015260408301516148b08161543b565b604082015260608301516148c38161543b565b606082015260808301516148d68161543b565b608082015260a08301516148e98161543b565b60a08201529392505050565b600060608284031215614906578081fd5b61491060606153a1565b8251815260208301516149228161543b565b602082015260408301516149358161543b565b60408201529392505050565b600081518084526149598160208601602086016153ec565b601f01601f19169290920160200192915050565b80516001600160a01b03908116835260208083015182169084015260408083015182169084015260608083015190911690830152608090810151910152565b6001600160a01b0391909116815260200190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039586168152938516602085015291841660408401529092166060820152608081019190915260a00190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a08201819052600090614a4f90830184614941565b98975050505050505050565b6001600160a01b03848116825283166020820152606060408201819052600090614a8790830184614941565b95945050505050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152614a876060830184614941565b901515815260200190565b90815260200190565b9182526001600160a01b0316602082015260400190565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b6001600160a01b038681168252851660208201526101208101614b6b604083018661496d565b60e082019390935261010001529392505050565b6001600160a01b038581168252841660208201526101008101614ba5604083018561496d565b8260e083015295945050505050565b600060208252611a786020830184614941565b60208082526012908201527113585c9ad95d081b9bdd0818dc99585d195960721b604082015260600190565b60208082526017908201527f4465627420636f6d706f6e656e74206d69736d61746368000000000000000000604082015260600190565b602080825260159082015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b604082015260600190565b6020808252601a908201527f436f6d706f6e656e74206d757374206265206e65676174697665000000000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b60208082526026908201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160408201526564647265737360d01b606082015260800190565b6020808252601b908201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604082015260600190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b6020808252600d908201526c05175616e74697479206973203609c1b604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252601190820152700a6d8d2e0e0c2ceca40e8dede40d0d2ced607b1b604082015260600190565b60208082526016908201527510dbdb1b185d195c985b081b9bdd08195b98589b195960521b604082015260600190565b602080825260129082015271109bdc9c9bddc81b9bdd08195b98589b195960721b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526018908201527f4f6e6c7920746865206d6f64756c652063616e2063616c6c0000000000000000604082015260600190565b60208082526018908201527f49737375616e6365206e6f7420696e697469616c697a65640000000000000000604082015260600190565b60208082526030908201527f45787465726e616c20706f736974696f6e73206d757374206265203020746f2060408201526f1c995b5bdd994818dbdb5c1bdb995b9d60821b606082015260800190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b60208082526017908201527f436f6c6c61746572616c2062616c616e63652069732030000000000000000000604082015260600190565b60208082526010908201526f24b73b30b634b21029b2ba2a37b5b2b760811b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b6020808252601490820152732737ba1030b63637bbb2b21029b2ba2a37b5b2b760611b604082015260600190565b60208082526027908201527f5369676e6564536166654d6174683a206d756c7469706c69636174696f6e206f604082015266766572666c6f7760c81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526018908201527f5061737365642064617461206d757374206265206e756c6c0000000000000000604082015260600190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526024908201527f4d6f64756c65206d75737420626520656e61626c6564206f6e20636f6e74726f604082015263363632b960e11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b60208082526012908201527110dbdb1b185d195c985b081b9bdd081cd95d60721b604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b60208082526014908201527313505617d55253950c4c8e17d15610d15151115160621b604082015260600190565b6020808252602d908201527f436f6c6c61746572616c20616e6420626f72726f77206173736574206d75737460408201526c08189948191a5999995c995b9d609a1b606082015260800190565b60208082526010908201526f043616e742064697669646520627920360841b604082015260600190565b6101608101615333828561496d565b6001600160801b038084511660a08401528060208501511660c08401528060408501511660e084015280606085015116610100840152806080850151166101208401528060a085015116610140840152509392505050565b9283526020830191909152604082015260600190565b60405181810167ffffffffffffffff811182821017156153c057600080fd5b604052919050565b600067ffffffffffffffff8211156153de578081fd5b50601f01601f191660200190565b60005b838110156154075781810151838201526020016153ef565b838111156111d35750506000910152565b6001600160a01b0381168114611a2557600080fd5b8015158114611a2557600080fd5b6001600160801b0381168114611a2557600080fdfea2646970667358221220e6473242f805740368c97ae48e597ddf564bac546affb0d5435a79111ad75ae564736f6c634300060a0033", + "linkReferences": { + "contracts/protocol/integration/lib/Morpho.sol": { + "Morpho": [ + { + "length": 20, + "start": 7235 + }, + { + "length": 20, + "start": 7488 + }, + { + "length": 20, + "start": 7849 + }, + { + "length": 20, + "start": 8774 + }, + { + "length": 20, + "start": 8994 + } + ] + } + }, + "deployedLinkReferences": { + "contracts/protocol/integration/lib/Morpho.sol": { + "Morpho": [ + { + "length": 20, + "start": 6865 + }, + { + "length": 20, + "start": 7118 + }, + { + "length": 20, + "start": 7479 + }, + { + "length": 20, + "start": 8404 + }, + { + "length": 20, + "start": 8624 + } + ] + } + } +} diff --git a/test/integration/ethereum/aaveV3LeverageStrategyExtension.spec.ts b/test/integration/ethereum/aaveV3LeverageStrategyExtension.spec.ts index 9f6f7f5fc..5e2f382d2 100644 --- a/test/integration/ethereum/aaveV3LeverageStrategyExtension.spec.ts +++ b/test/integration/ethereum/aaveV3LeverageStrategyExtension.spec.ts @@ -147,7 +147,6 @@ if (process.env.INTEGRATIONTEST) { let exchangeSettings: ExchangeSettings; let customTargetLeverageRatio: any; let customMinLeverageRatio: any; - let customATokenCollateralAddress: any; let leverageStrategyExtension: AaveV3LeverageStrategyExtension; let baseManagerV2: BaseManager; diff --git a/test/integration/ethereum/morphoLeverageStrategyExtension.spec.ts b/test/integration/ethereum/morphoLeverageStrategyExtension.spec.ts new file mode 100644 index 000000000..94b7767e4 --- /dev/null +++ b/test/integration/ethereum/morphoLeverageStrategyExtension.spec.ts @@ -0,0 +1,5237 @@ +import "module-alias/register"; +import { BigNumber } from "@ethersproject/bignumber"; +import { ethers, network } from "hardhat"; + +import { + Address, + Account, + MethodologySettings, + ExecutionSettings, + IncentiveSettings, + ExchangeSettings, +} from "@utils/types"; +import { impersonateAccount, setBalance } from "../../../utils/test/testingUtils"; +import { ADDRESS_ZERO, EMPTY_BYTES, ZERO, THREE, TWO, ONE } from "@utils/constants"; +import { BaseManager } from "@utils/contracts/index"; +import { + ChainlinkAggregatorV3Mock, + ContractCallerMock, + MorphoLeverageModule, + MorphoLeverageStrategyExtension, + Controller, + Controller__factory, + IMorphoOracle, + IMorphoOracle__factory, + IMorpho, + IMorpho__factory, + DebtIssuanceModuleV2, + DebtIssuanceModuleV2__factory, + IntegrationRegistry, + IntegrationRegistry__factory, + SetTokenCreator, + SetTokenCreator__factory, + SetToken, + SetToken__factory, + IERC20, + IERC20__factory, + TradeAdapterMock, + IChainlinkEACAggregatorProxy, + IChainlinkEACAggregatorProxy__factory, +} from "../../../typechain"; +import DeployHelper from "@utils/deploys"; +import { + cacheBeforeEach, + ether, + getAccounts, + getLastBlockTimestamp, + getWaffleExpect, + getRandomAccount, + preciseDivCeil, + increaseTimeAsync, + calculateCollateralRebalanceUnits, + calculateNewLeverageRatio, + getEthBalance, + preciseMul, + preciseDiv, + calculateMaxRedeemForDeleverToZero, +} from "@utils/index"; +import { convertPositionToNotional } from "@utils/test"; + +const expect = getWaffleExpect(); + +const MORPHO_ORACLE_PRICE_SCALE = BigNumber.from(10).pow(36); + +const contractAddresses = { + controller: "0xD2463675a099101E36D85278494268261a66603A", + debtIssuanceModule: "0x04b59F9F09750C044D7CfbC177561E409085f0f3", + setTokenCreator: "0x2758BF6Af0EC63f1710d3d7890e1C263a247B75E", + integrationRegistry: "0xb9083dee5e8273E54B9DB4c31bA9d4aB7C6B28d3", + uniswapV3ExchangeAdapterV2: "0xe6382D2D44402Bad8a03F11170032aBCF1Df1102", + uniswapV3Router: "0xe6382D2D44402Bad8a03F11170032aBCF1Df1102", + wethDaiPool: "0x60594a405d53811d3bc4766596efd80fd545a270", + morpho: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", + // Note: This is the ultimate source for the current eth price for the morpho oracle + chainlinkUsdcEthOracleProxy: "0x986b5E1e1755e3C2440e960477f25201B0a8bbD4", +}; + +const tokenAddresses = { + weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + wbtc: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + stEth: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + wsteth: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", +}; + +const whales = { + dai: "0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8", + wsteth: "0x3c22ec75ea5D745c78fc84762F7F1E6D82a2c5BF", + weth: "0x8EB8a3b98659Cce290402893d0123abb75E3ab28", + usdc: "0xCFFAd3200574698b78f32232aa9D63eABD290703", +}; + +const wstethUsdcMarketParams = { + loanToken: tokenAddresses.usdc, + collateralToken: tokenAddresses.wsteth, + oracle: "0x48F7E36EB6B826B2dF4B2E630B62Cd25e89E40e2", + irm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC", + lltv: ether(0.86), +}; + +const marketId = "0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc"; +if (process.env.INTEGRATIONTEST) { + describe("MorphoLeverageStrategyExtension", () => { + let owner: Account; + let nonOwner: Account; + let methodologist: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let morphoLeverageModule: MorphoLeverageModule; + let debtIssuanceModule: DebtIssuanceModuleV2; + let morphoOracle: IMorphoOracle; + let integrationRegistry: IntegrationRegistry; + let setTokenCreator: SetTokenCreator; + let tradeAdapterMock: TradeAdapterMock; + let tradeAdapterMock2: TradeAdapterMock; + let wsteth: IERC20; + let usdc: IERC20; + let customTargetLeverageRatio: any; + let customMinLeverageRatio: any; + let morpho: IMorpho; + let usdcEthOracleProxy: IChainlinkEACAggregatorProxy; + let controller: Controller; + + let usdcEthOrackeMock: ChainlinkAggregatorV3Mock; + + let strategy: any; + let methodology: MethodologySettings; + let execution: ExecutionSettings; + let incentive: IncentiveSettings; + const exchangeName = "MockTradeAdapter"; + const exchangeName2 = "MockTradeAdapter2"; + let exchangeSettings: ExchangeSettings; + let initialCollateralPriceInverted: BigNumber; + + let leverageStrategyExtension: MorphoLeverageStrategyExtension; + let baseManagerV2: BaseManager; + let manager: Address; + + cacheBeforeEach(async () => { + [owner, methodologist, nonOwner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + usdcEthOrackeMock = await deployer.mocks.deployChainlinkAggregatorMock(); + usdcEthOracleProxy = IChainlinkEACAggregatorProxy__factory.connect( + contractAddresses.chainlinkUsdcEthOracleProxy, + owner.wallet, + ); + initialCollateralPriceInverted = await usdcEthOracleProxy.latestAnswer(); + usdcEthOrackeMock.setPrice(initialCollateralPriceInverted); + + const oracleOwner = await usdcEthOracleProxy.owner(); + await setBalance(oracleOwner, ether(10000)); + usdcEthOracleProxy = usdcEthOracleProxy.connect(await impersonateAccount(oracleOwner)); + await usdcEthOracleProxy.proposeAggregator(usdcEthOrackeMock.address); + await usdcEthOracleProxy.confirmAggregator(usdcEthOrackeMock.address); + + morphoLeverageModule = await deployer.setV2.deployMorphoLeverageModule( + contractAddresses.controller, + contractAddresses.morpho, + ); + morpho = IMorpho__factory.connect(contractAddresses.morpho, owner.wallet); + + controller = Controller__factory.connect(contractAddresses.controller, owner.wallet); + const controllerOwner = await controller.owner(); + // setBalance of controller Owner to 100 eth + await setBalance(controllerOwner, ether(100)); + controller = controller.connect(await impersonateAccount(controllerOwner)); + + manager = owner.address; + usdc = IERC20__factory.connect(tokenAddresses.usdc, owner.wallet); + const usdcWhaleBalance = await usdc.balanceOf(whales.usdc); + await usdc + .connect(await impersonateAccount(whales.usdc)) + .transfer(owner.address, usdcWhaleBalance); + wsteth = IERC20__factory.connect(tokenAddresses.wsteth, owner.wallet); + // whale needs eth for the transfer. + await network.provider.send("hardhat_setBalance", [whales.wsteth, ether(10).toHexString()]); + const wstethWhaleBalance = await wsteth.balanceOf(whales.wsteth); + await wsteth + .connect(await impersonateAccount(whales.wsteth)) + .transfer(owner.address, wstethWhaleBalance); + + morphoOracle = IMorphoOracle__factory.connect(wstethUsdcMarketParams.oracle, owner.wallet); + integrationRegistry = IntegrationRegistry__factory.connect( + contractAddresses.integrationRegistry, + owner.wallet, + ); + const integrationRegistryOwner = await integrationRegistry.owner(); + integrationRegistry = integrationRegistry.connect( + await impersonateAccount(integrationRegistryOwner), + ); + + setTokenCreator = SetTokenCreator__factory.connect( + contractAddresses.setTokenCreator, + owner.wallet, + ); + + debtIssuanceModule = DebtIssuanceModuleV2__factory.connect( + contractAddresses.debtIssuanceModule, + owner.wallet, + ); + }); + + const replaceRegistry = async ( + integrationModuleAddress: string, + name: string, + adapterAddress: string, + ) => { + const currentAdapterAddress = await integrationRegistry.getIntegrationAdapter( + integrationModuleAddress, + name, + ); + if (!ethers.utils.isAddress(adapterAddress)) { + throw new Error("Invalid address: " + adapterAddress + " for " + name + " adapter"); + } + if (ethers.utils.isAddress(currentAdapterAddress) && currentAdapterAddress != ADDRESS_ZERO) { + await integrationRegistry.editIntegration(integrationModuleAddress, name, adapterAddress); + } else { + await integrationRegistry.addIntegration(integrationModuleAddress, name, adapterAddress); + } + }; + const sharesToAssetsUp = ( + shares: BigNumber, + totalAssets: BigNumber, + totalShares: BigNumber, + ) => { + const VIRTUAL_SHARES = 1e6; + const VIRTUAL_ASSETS = 1; + const totalAssetsAdjusted = totalAssets.add(VIRTUAL_ASSETS); + const totalSharesAdjusted = totalShares.add(VIRTUAL_SHARES); + return shares + .mul(totalAssetsAdjusted) + .add(totalSharesAdjusted) + .sub(1) + .div(totalSharesAdjusted); + }; + + async function calculateTotalRebalanceNotional( + currentLeverageRatio: BigNumber, + newLeverageRatio: BigNumber, + ): Promise { + const { collateralTotalBalance } = await getBorrowAndCollateralBalances(); + const a = currentLeverageRatio.gt(newLeverageRatio) + ? currentLeverageRatio.sub(newLeverageRatio) + : newLeverageRatio.sub(currentLeverageRatio); + const b = preciseMul(a, collateralTotalBalance); + return preciseDiv(b, currentLeverageRatio); + } + + function calculateMaxBorrowForDeleverV3( + collateralBalance: BigNumber, + collateralPrice: BigNumber, + borrowBalance: BigNumber, + ) { + const netBorrowLimit = preciseMul( + preciseMul( + collateralBalance.mul(collateralPrice).div(MORPHO_ORACLE_PRICE_SCALE), + wstethUsdcMarketParams.lltv, + ), + ether(1).sub(execution.unutilizedLeveragePercentage), + ); + return preciseDiv( + preciseMul(collateralBalance, netBorrowLimit.sub(borrowBalance)), + netBorrowLimit, + ); + } + + async function getBorrowAndCollateralBalances() { + const [, borrowShares, collateral] = await morpho.position(marketId, setToken.address); + const collateralTokenBalance = await wsteth.balanceOf(setToken.address); + const collateralTotalBalance = collateralTokenBalance.add(collateral); + const [, , totalBorrowAssets, totalBorrowShares, , ] = await morpho.market(marketId); + const borrowAssets = sharesToAssetsUp(borrowShares, totalBorrowAssets, totalBorrowShares); + return { collateralTotalBalance, borrowAssets }; + } + + async function checkSetComponentsAgainstMorphoPosition() { + await morpho.accrueInterest(wstethUsdcMarketParams); + const currentPositions = await setToken.getPositions(); + const initialSetTokenSupply = await setToken.totalSupply(); + const collateralNotional = await convertPositionToNotional( + currentPositions[0].unit, + setToken, + ); + + const { collateralTotalBalance, borrowAssets } = await getBorrowAndCollateralBalances(); + + expect(collateralNotional).to.lte(collateralTotalBalance); + // Maximum rounding error when converting position to notional + expect(collateralNotional).to.gt( + collateralTotalBalance.sub(initialSetTokenSupply.div(ether(1))), + ); + if (borrowAssets.gt(0)) { + const borrowNotional = await convertPositionToNotional(currentPositions[1].unit, setToken); + // TODO: Review that this error margin is correct / expected + expect(borrowNotional.mul(-1)).to.gte( + borrowAssets.sub(preciseDivCeil(initialSetTokenSupply, ether(1))), + ); + expect(borrowNotional.mul(-1)).to.lte( + borrowAssets.add(preciseDivCeil(initialSetTokenSupply, ether(1))), + ); + } + } + + async function createSetToken( + components: Address[], + positions: BigNumber[], + modules: Address[], + ): Promise { + const setTokenAddress = await setTokenCreator.callStatic.create( + components, + positions, + modules, + manager, + "TestSetToken", + "TEST", + ); + + await setTokenCreator.create(components, positions, modules, manager, "TestSetToken", "TEST"); + return SetToken__factory.connect(setTokenAddress, owner.wallet); + } + + const initializeRootScopeContracts = async () => { + if (!(await controller.isModule(morphoLeverageModule.address))) { + await controller.addModule(morphoLeverageModule.address); + tradeAdapterMock = await deployer.mocks.deployTradeAdapterMock(); + replaceRegistry(morphoLeverageModule.address, exchangeName, tradeAdapterMock.address); + // Deploy mock trade adapter 2 + tradeAdapterMock2 = await deployer.mocks.deployTradeAdapterMock(); + replaceRegistry(morphoLeverageModule.address, exchangeName2, tradeAdapterMock2.address); + replaceRegistry( + morphoLeverageModule.address, + "DefaultIssuanceModule", + debtIssuanceModule.address, + ); + replaceRegistry( + debtIssuanceModule.address, + "MorphoLeverageModuleV3", + morphoLeverageModule.address, + ); + } + + setToken = await createSetToken( + [wsteth.address], + [ether(1)], + [debtIssuanceModule.address, morphoLeverageModule.address], + ); + const ownerofLeveverageModule = await morphoLeverageModule.owner(); + if (ownerofLeveverageModule != owner.address) { + await morphoLeverageModule + .connect(await impersonateAccount(ownerofLeveverageModule)) + .updateAnySetAllowed(true); + } else { + await morphoLeverageModule.updateAnySetAllowed(true); + } + // Initialize modules + await debtIssuanceModule.initialize( + setToken.address, + ether(1), + ZERO, + ZERO, + owner.address, + ADDRESS_ZERO, + ); + + await morphoLeverageModule.initialize(setToken.address, wstethUsdcMarketParams); + + baseManagerV2 = await deployer.manager.deployBaseManager( + setToken.address, + owner.address, + methodologist.address, + ); + + // Transfer ownership to ic manager + if ((await setToken.manager()) == owner.address) { + await setToken.connect(owner.wallet).setManager(baseManagerV2.address); + } + + // Deploy adapter + const targetLeverageRatio = customTargetLeverageRatio || ether(2); + const minLeverageRatio = customMinLeverageRatio || ether(1.7); + const maxLeverageRatio = ether(2.3); + const recenteringSpeed = ether(0.05); + const rebalanceInterval = BigNumber.from(86400); + + const unutilizedLeveragePercentage = ether(0.01); + const twapMaxTradeSize = ether(0.5); + const twapCooldownPeriod = BigNumber.from(3000); + const slippageTolerance = ether(0.01); + + const incentivizedTwapMaxTradeSize = ether(2); + const incentivizedTwapCooldownPeriod = BigNumber.from(60); + const incentivizedSlippageTolerance = ether(0.05); + const etherReward = ether(1); + const incentivizedLeverageRatio = ether(2.6); + + strategy = { + setToken: setToken.address, + leverageModule: morphoLeverageModule.address, + collateralAsset: wsteth.address, + borrowAsset: usdc.address, + collateralDecimalAdjustment: BigNumber.from(10), + borrowDecimalAdjustment: BigNumber.from(10), + }; + methodology = { + targetLeverageRatio: targetLeverageRatio, + minLeverageRatio: minLeverageRatio, + maxLeverageRatio: maxLeverageRatio, + recenteringSpeed: recenteringSpeed, + rebalanceInterval: rebalanceInterval, + }; + execution = { + unutilizedLeveragePercentage: unutilizedLeveragePercentage, + twapCooldownPeriod: twapCooldownPeriod, + slippageTolerance: slippageTolerance, + }; + incentive = { + incentivizedTwapCooldownPeriod: incentivizedTwapCooldownPeriod, + incentivizedSlippageTolerance: incentivizedSlippageTolerance, + etherReward: etherReward, + incentivizedLeverageRatio: incentivizedLeverageRatio, + }; + const leverExchangeData = EMPTY_BYTES; + const deleverExchangeData = EMPTY_BYTES; + exchangeSettings = { + twapMaxTradeSize: twapMaxTradeSize, + incentivizedTwapMaxTradeSize: incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: BigNumber.from(0), + leverExchangeData, + deleverExchangeData, + }; + + leverageStrategyExtension = await deployer.extensions.deployMorphoLeverageStrategyExtension( + baseManagerV2.address, + strategy, + methodology, + execution, + incentive, + [exchangeName], + [exchangeSettings], + ); + + // Add adapter + await baseManagerV2.connect(owner.wallet).addAdapter(leverageStrategyExtension.address); + }; + + describe("#constructor", async () => { + let subjectManagerAddress: Address; + let subjectContractSettings: any; + let subjectMethodologySettings: MethodologySettings; + let subjectExecutionSettings: ExecutionSettings; + let subjectIncentiveSettings: IncentiveSettings; + let subjectExchangeName: string; + let subjectExchangeSettings: ExchangeSettings; + + cacheBeforeEach(initializeRootScopeContracts); + + beforeEach(async () => { + subjectManagerAddress = baseManagerV2.address; + subjectContractSettings = { + setToken: setToken.address, + leverageModule: morphoLeverageModule.address, + collateralAsset: wsteth.address, + borrowAsset: usdc.address, + }; + subjectMethodologySettings = { + targetLeverageRatio: ether(2), + minLeverageRatio: ether(1.7), + maxLeverageRatio: ether(2.3), + recenteringSpeed: ether(0.05), + rebalanceInterval: BigNumber.from(86400), + }; + subjectExecutionSettings = { + unutilizedLeveragePercentage: ether(0.01), + twapCooldownPeriod: BigNumber.from(120), + slippageTolerance: ether(0.01), + }; + subjectIncentiveSettings = { + incentivizedTwapCooldownPeriod: BigNumber.from(60), + incentivizedSlippageTolerance: ether(0.05), + etherReward: ether(1), + incentivizedLeverageRatio: ether(3.5), + }; + subjectExchangeName = exchangeName; + const leverExchangeData = EMPTY_BYTES; + const deleverExchangeData = EMPTY_BYTES; + subjectExchangeSettings = { + twapMaxTradeSize: ether(0.1), + incentivizedTwapMaxTradeSize: ether(1), + exchangeLastTradeTimestamp: BigNumber.from(0), + leverExchangeData, + deleverExchangeData, + }; + }); + + async function subject(): Promise { + return await deployer.extensions.deployMorphoLeverageStrategyExtension( + subjectManagerAddress, + subjectContractSettings, + subjectMethodologySettings, + subjectExecutionSettings, + subjectIncentiveSettings, + [subjectExchangeName], + [subjectExchangeSettings], + ); + } + + it("should set overrideNoRebalanceInProgress flag", async () => { + const retrievedAdapter = await subject(); + + const overrideNoRebalanceInProgress = + await retrievedAdapter.overrideNoRebalanceInProgress(); + + expect(overrideNoRebalanceInProgress).to.be.false; + }); + + it("should set the manager address", async () => { + const retrievedAdapter = await subject(); + + const manager = await retrievedAdapter.manager(); + + expect(manager).to.eq(subjectManagerAddress); + }); + + it("should set the contract addresses", async () => { + const retrievedAdapter = await subject(); + const strategy = await retrievedAdapter.getStrategy(); + + expect(strategy.setToken).to.eq(subjectContractSettings.setToken); + expect(strategy.leverageModule).to.eq(subjectContractSettings.leverageModule); + expect(strategy.collateralAsset).to.eq(subjectContractSettings.collateralAsset); + expect(strategy.borrowAsset).to.eq(subjectContractSettings.borrowAsset); + }); + + it("should set the correct methodology parameters", async () => { + const retrievedAdapter = await subject(); + const methodology = await retrievedAdapter.getMethodology(); + + expect(methodology.targetLeverageRatio).to.eq( + subjectMethodologySettings.targetLeverageRatio, + ); + expect(methodology.minLeverageRatio).to.eq(subjectMethodologySettings.minLeverageRatio); + expect(methodology.maxLeverageRatio).to.eq(subjectMethodologySettings.maxLeverageRatio); + expect(methodology.recenteringSpeed).to.eq(subjectMethodologySettings.recenteringSpeed); + expect(methodology.rebalanceInterval).to.eq(subjectMethodologySettings.rebalanceInterval); + }); + + it("should set the correct execution parameters", async () => { + const retrievedAdapter = await subject(); + const execution = await retrievedAdapter.getExecution(); + + expect(execution.unutilizedLeveragePercentage).to.eq( + subjectExecutionSettings.unutilizedLeveragePercentage, + ); + expect(execution.twapCooldownPeriod).to.eq(subjectExecutionSettings.twapCooldownPeriod); + expect(execution.slippageTolerance).to.eq(subjectExecutionSettings.slippageTolerance); + }); + + it("should set the correct incentive parameters", async () => { + const retrievedAdapter = await subject(); + const incentive = await retrievedAdapter.getIncentive(); + + expect(incentive.incentivizedTwapCooldownPeriod).to.eq( + subjectIncentiveSettings.incentivizedTwapCooldownPeriod, + ); + expect(incentive.incentivizedSlippageTolerance).to.eq( + subjectIncentiveSettings.incentivizedSlippageTolerance, + ); + expect(incentive.etherReward).to.eq(subjectIncentiveSettings.etherReward); + expect(incentive.incentivizedLeverageRatio).to.eq( + subjectIncentiveSettings.incentivizedLeverageRatio, + ); + }); + + it("should set the correct exchange settings for the initial exchange", async () => { + const retrievedAdapter = await subject(); + const exchangeSettings = await retrievedAdapter.getExchangeSettings(subjectExchangeName); + + expect(exchangeSettings.leverExchangeData).to.eq(subjectExchangeSettings.leverExchangeData); + expect(exchangeSettings.deleverExchangeData).to.eq( + subjectExchangeSettings.deleverExchangeData, + ); + expect(exchangeSettings.twapMaxTradeSize).to.eq(subjectExchangeSettings.twapMaxTradeSize); + expect(exchangeSettings.incentivizedTwapMaxTradeSize).to.eq( + subjectExchangeSettings.incentivizedTwapMaxTradeSize, + ); + expect(exchangeSettings.exchangeLastTradeTimestamp).to.eq( + subjectExchangeSettings.exchangeLastTradeTimestamp, + ); + }); + + describe("when min leverage ratio is 0", async () => { + beforeEach(async () => { + subjectMethodologySettings.minLeverageRatio = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid min leverage"); + }); + }); + + describe("when min leverage ratio is above target", async () => { + beforeEach(async () => { + subjectMethodologySettings.minLeverageRatio = ether(2.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid min leverage"); + }); + }); + + describe("when max leverage ratio is below target", async () => { + beforeEach(async () => { + subjectMethodologySettings.maxLeverageRatio = ether(1.9); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid max leverage"); + }); + }); + + describe("when recentering speed is >100%", async () => { + beforeEach(async () => { + subjectMethodologySettings.recenteringSpeed = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid recentering speed"); + }); + }); + + describe("when recentering speed is 0%", async () => { + beforeEach(async () => { + subjectMethodologySettings.recenteringSpeed = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid recentering speed"); + }); + }); + + describe("when unutilizedLeveragePercentage is >100%", async () => { + beforeEach(async () => { + subjectExecutionSettings.unutilizedLeveragePercentage = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unutilized leverage must be <100%"); + }); + }); + + describe("when slippage tolerance is >100%", async () => { + beforeEach(async () => { + subjectExecutionSettings.slippageTolerance = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Slippage tolerance must be <100%"); + }); + }); + + describe("when incentivized slippage tolerance is >100%", async () => { + beforeEach(async () => { + subjectIncentiveSettings.incentivizedSlippageTolerance = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Incentivized slippage tolerance must be <100%", + ); + }); + }); + + describe("when incentivize leverage ratio is less than max leverage ratio", async () => { + beforeEach(async () => { + subjectIncentiveSettings.incentivizedLeverageRatio = ether(2.29); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Incentivized leverage ratio must be > max leverage ratio", + ); + }); + }); + + describe("when rebalance interval is shorter than TWAP cooldown period", async () => { + beforeEach(async () => { + subjectMethodologySettings.rebalanceInterval = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Rebalance interval must be greater than TWAP cooldown period", + ); + }); + }); + + describe("when TWAP cooldown period is shorter than incentivized TWAP cooldown period", async () => { + beforeEach(async () => { + subjectExecutionSettings.twapCooldownPeriod = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "TWAP cooldown must be greater than incentivized TWAP cooldown", + ); + }); + }); + + describe("when an exchange has a twapMaxTradeSize of 0", async () => { + beforeEach(async () => { + subjectExchangeSettings.twapMaxTradeSize = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Max TWAP trade size must not be 0"); + }); + }); + }); + + describe("#engage", async () => { + let destinationTokenQuantity: BigNumber; + let subjectCaller: Account; + let subjectExchangeName: string; + + context( + "when rebalance notional is greater than max trade size and greater than max borrow", + async () => { + let issueQuantity: BigNumber; + + const intializeContracts = async () => { + await initializeRootScopeContracts(); + + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + // await usdc.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + issueQuantity = ether(1); + + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address, { + gasLimit: 10000000, + }); + + destinationTokenQuantity = ether(0.5); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + await wsteth.transfer(tradeAdapterMock2.address, destinationTokenQuantity); + }; + + const initializeSubjectVariables = () => { + subjectCaller = owner; + subjectExchangeName = exchangeName; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.engage(subjectExchangeName); + } + + describe("when the collateral balance is not zero", () => { + cacheBeforeEach(intializeContracts); + beforeEach(initializeSubjectVariables); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(methodology.targetLeverageRatio); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = currentPositions[0]; + + // Get expected aTokens position size + const expectedFirstPositionUnit = + initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.be.gte(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.be.lte( + expectedFirstPositionUnit.mul(1001).div(1000), + ); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("positions should align with token balances", async () => { + await subject(); + await checkSetComponentsAgainstMorphoPosition(); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should emit Engaged event", async () => { + await expect(subject()).to.emit(leverageStrategyExtension, "Engaged"); + }); + + describe("when borrow balance is not 0", async () => { + beforeEach(async () => { + const setTokenSigner = await impersonateAccount(setToken.address); + await setBalance(setToken.address, ether(1)); + await wsteth.transfer(setToken.address, ether(1)); + await wsteth.connect(setTokenSigner).approve(morpho.address, ether(1)); + await morpho + .connect(setTokenSigner) + .supplyCollateral( + wstethUsdcMarketParams, + ether(1), + setToken.address, + EMPTY_BYTES, + ); + await morpho + .connect(setTokenSigner) + .borrow(wstethUsdcMarketParams, 1_000_000, 0, setToken.address, setToken.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Debt must be 0"); + }); + }); + + describe("when SetToken has 0 supply", async () => { + beforeEach(async () => { + await debtIssuanceModule.redeem(setToken.address, ether(1), owner.address); + }); + + it("should revert", async () => { + // Note: Different revert message because the enterCollateralPosition function revers already before the set token balance check + await expect(subject()).to.be.revertedWith("Collateral balance is 0"); + }); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + }); + }, + ); + + context( + "when rebalance notional is less than max trade size and greater than max borrow", + async () => { + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + await usdc.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(1.9), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.leverExchangeData, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + destinationTokenQuantity = ether(0.85); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.engage(subjectExchangeName); + } + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(methodology.targetLeverageRatio); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected aToken position unit + const expectedFirstPositionUnit = + initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); + expect(newFirstPosition.unit).to.eq(expectedFirstPositionUnit); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("positions should align with token balances", async () => { + await subject(); + await checkSetComponentsAgainstMorphoPosition(); + }); + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = currentPositions[1]; + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }, + ); + + context( + "when rebalance notional is less than max trade size and less than max borrow", + async () => { + before(async () => { + customTargetLeverageRatio = ether(1.25); // Change to 1.25x + customMinLeverageRatio = ether(1.1); + }); + + after(async () => { + customTargetLeverageRatio = undefined; + customMinLeverageRatio = undefined; + }); + + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + destinationTokenQuantity = ether(0.25); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.engage(subjectExchangeName); + } + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = currentPositions[0]; + + // Get expected wsteth position units + const expectedFirstPositionUnit = customTargetLeverageRatio; + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("positions should align with token balances", async () => { + await subject(); + await checkSetComponentsAgainstMorphoPosition(); + }); + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(1); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }, + ); + }); + + describe("#rebalance", async () => { + let destinationTokenQuantity: BigNumber; + let subjectCaller: Account; + let subjectExchangeName: string; + let ifEngaged: boolean; + + before(async () => { + ifEngaged = true; + subjectExchangeName = exchangeName; + }); + + const intializeContracts = async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + const maxIteration = 10; + let iteration = 0; + if (ifEngaged) { + // Engage to initial leverage + await leverageStrategyExtension.engage(subjectExchangeName); + while ( + (await leverageStrategyExtension.twapLeverageRatio()).gt(0) && + iteration < maxIteration + ) { + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + await leverageStrategyExtension.iterateRebalance(subjectExchangeName); + iteration++; + } + } + }; + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .rebalance(subjectExchangeName); + } + cacheBeforeEach(intializeContracts); + + context("when methodology settings are increased beyond default maximum", () => { + let newMethodology: MethodologySettings; + let newIncentive: IncentiveSettings; + beforeEach(() => { + subjectCaller = owner; + }); + cacheBeforeEach(async () => { + newIncentive = { + ...incentive, + incentivizedLeverageRatio: ether(9.1), + }; + await leverageStrategyExtension.setIncentiveSettings(newIncentive); + newMethodology = { + targetLeverageRatio: ether(8), + minLeverageRatio: ether(7), + maxLeverageRatio: ether(9), + recenteringSpeed: methodology.recenteringSpeed, + rebalanceInterval: methodology.rebalanceInterval, + }; + await leverageStrategyExtension.setMethodologySettings(newMethodology); + destinationTokenQuantity = ether(0.5); + await increaseTimeAsync(BigNumber.from(100000)); + + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + }); + + context("when current leverage ratio is below target (lever)", async () => { + cacheBeforeEach(async () => { + destinationTokenQuantity = ether(0.1); + await increaseTimeAsync(BigNumber.from(100000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(11).div(10); + usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + await morphoLeverageModule.sync(setToken.address); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected collateral token position units; + const expectedFirstPositionUnit = initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // Default + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should emit Rebalanced event", async () => { + await expect(subject()).to.emit(leverageStrategyExtension, "Rebalanced"); + }); + + describe("when rebalance interval has not elapsed but is below min leverage ratio and lower than max trade size", async () => { + cacheBeforeEach(async () => { + await subject(); + // ~1.6x leverage + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(6).div(5); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(1.9), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + destinationTokenQuantity = ether(1); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // cEther position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected aToken position unit + const expectedFirstPositionUnit = + initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // cEther position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + describe("when rebalance interval has not elapsed below min leverage ratio and greater than max trade size", async () => { + cacheBeforeEach(async () => { + await subject(); + // ~1.6x leverage + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(6).div(5); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + // > Max trade size + destinationTokenQuantity = ether(0.5); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.01), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + // await chainlinkCollateralPriceMock.setPrice(initialCollateralPrice.mul(6).div(5)); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio", async () => { + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const previousTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + await subject(); + + const currentTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + expect(previousTwapLeverageRatio).to.eq(ZERO); + expect(currentTwapLeverageRatio).to.eq(expectedNewLeverageRatio); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + await subject(); + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected aToken position units + const expectedFirstPositionUnit = initialPositions[0].unit.add(ether(0.5)); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + describe("when rebalance interval has not elapsed", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Cooldown not elapsed or not valid leverage ratio", + ); + }); + }); + + describe("when in a TWAP rebalance", async () => { + beforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(6).div(5); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.01), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + await wsteth.transfer(tradeAdapterMock.address, ether(0.01)); + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must call iterate"); + }); + }); + + describe("when borrow balance is 0", async () => { + beforeEach(async () => { + await usdc.approve(morpho.address, 10_000 * 10 ** 6); + const position = await morpho.position(marketId, setToken.address); + await morpho.repay( + wstethUsdcMarketParams, + 0, + position.borrowShares, + setToken.address, + EMPTY_BYTES, + ); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Borrow balance must exist"); + }); + }); + + describe("when caller is not an allowed trader", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Address not permitted to call"); + }); + }); + + describe("when caller is a contract", async () => { + let subjectTarget: Address; + let subjectCallData: string; + let subjectValue: BigNumber; + + let contractCaller: ContractCallerMock; + + beforeEach(async () => { + contractCaller = await deployer.setV2.deployContractCallerMock(); + + subjectTarget = leverageStrategyExtension.address; + subjectCallData = leverageStrategyExtension.interface.encodeFunctionData("rebalance", [ + subjectExchangeName, + ]); + subjectValue = ZERO; + }); + + async function subjectContractCaller(): Promise { + return await contractCaller.invoke(subjectTarget, subjectValue, subjectCallData); + } + + it("the trade reverts", async () => { + await expect(subjectContractCaller()).to.be.revertedWith("Caller must be EOA Address"); + }); + }); + + // describe("when SetToken has 0 supply", async () => { + // Note: This will fail when trying to redeem the whole set supply because of rounding error / inprescision in the asset / shares math + // TODO: Check if this is acceptable or we need to fix this (for example by using shares instead of asset units) + // beforeEach(async () => { + // await usdc.approve(debtIssuanceModule.address, MAX_UINT_256); + // // This does not revert + // // await debtIssuanceModule.redeem(setToken.address, ether(0.99999999), owner.address, {gasLimit: 5_000_000}); + // // This does revert + // await debtIssuanceModule.redeem(setToken.address, ether(1), owner.address, { + // gasLimit: 5_000_000, + // }); + // }); + // it("should revert", async () => { + // await expect(subject()).to.be.revertedWith("SetToken must have > 0 supply"); + // }); + // }); + }); + + context("when current leverage ratio is above target (delever)", async () => { + let sendQuantity: BigNumber; + cacheBeforeEach(async () => { + await tradeAdapterMock.withdraw(usdc.address); + await increaseTimeAsync(BigNumber.from(100000)); + // Reduce by 10% so need to delever + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(10).div(11); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + sendQuantity = BigNumber.from(100 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .rebalance(subjectExchangeName); + } + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + const { collateralTotalBalance } = await getBorrowAndCollateralBalances(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + // Get expected redeemed + const expectedCollateralAssetsRedeemed = calculateCollateralRebalanceUnits( + currentLeverageRatio, + expectedNewLeverageRatio, + collateralTotalBalance, + ether(1), // Total supply + ); + + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + expectedCollateralAssetsRedeemed, + ); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + describe("when rebalance interval has not elapsed above max leverage ratio and lower than max trade size", async () => { + let sendQuantity: BigNumber; + cacheBeforeEach(async () => { + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + // ~2.4x leverage + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(85).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(1.9), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + sendQuantity = BigNumber.from(100 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + }); + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + const { collateralTotalBalance } = await getBorrowAndCollateralBalances(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + // Get expected redeemed + const expectedCollateralAssetsRedeemed = calculateCollateralRebalanceUnits( + currentLeverageRatio, + expectedNewLeverageRatio, + collateralTotalBalance, + await setToken.totalSupply(), // Total supply + ); + + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + expectedCollateralAssetsRedeemed, + ); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + describe("when rebalance interval has not elapsed above max leverage ratio and greater than max trade size", async () => { + let newTWAPMaxTradeSize: BigNumber; + let sendQuantity: BigNumber; + + cacheBeforeEach(async () => { + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + + // ~2.4x leverage + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(85).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + // > Max trade size + newTWAPMaxTradeSize = ether(0.01); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: newTWAPMaxTradeSize, + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + // await chainlinkCollateralPriceMock.setPrice(initialCollateralPrice.mul(85).div(100)); + sendQuantity = BigNumber.from(100 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + }); + + it("should set the last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio", async () => { + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const previousTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + await subject(); + + const currentTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + expect(previousTwapLeverageRatio).to.eq(ZERO); + expect(currentTwapLeverageRatio).to.eq(expectedNewLeverageRatio); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Max TWAP collateral units + const expectedFirstPositionUnit = initialPositions[0].unit.sub(newTWAPMaxTradeSize); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + context("when using two exchanges", async () => { + let subjectExchangeToUse: string; + let sendQuantity: BigNumber; + + cacheBeforeEach(async () => { + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(2), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: exchangeSettings.leverExchangeData, + deleverExchangeData: exchangeSettings.deleverExchangeData, + }; + + await leverageStrategyExtension.updateEnabledExchange( + exchangeName, + newExchangeSettings, + ); + await leverageStrategyExtension.addEnabledExchange(exchangeName2, newExchangeSettings); + + // await chainlinkCollateralPriceMock.setPrice(initialCollateralPrice.mul(87).div(100)); + sendQuantity = BigNumber.from(100 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + await usdc.transfer(tradeAdapterMock2.address, sendQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + subjectExchangeToUse = exchangeName; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .rebalance(subjectExchangeToUse); + } + + describe("when leverage ratio is above max and rises further between rebalances", async () => { + it("should set the global and exchange timestamps correctly", async () => { + await subject(); + const timestamp1 = await getLastBlockTimestamp(); + + subjectExchangeToUse = exchangeName2; + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(82).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + await subject(); + const timestamp2 = await getLastBlockTimestamp(); + + expect(await leverageStrategyExtension.globalLastTradeTimestamp()).to.eq(timestamp2); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp1); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName2)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp2); + }); + }); + + describe("when performing the epoch rebalance and rebalance is called twice with different exchanges", async () => { + beforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Cooldown not elapsed or not valid leverage ratio", + ); + }); + }); + + describe("when leverage ratio is above max and rebalance is called twice with different exchanges", async () => { + beforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Cooldown not elapsed or not valid leverage ratio", + ); + }); + }); + }); + + describe("when above incentivized leverage ratio threshold", async () => { + beforeEach(async () => { + await subject(); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(65).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be below incentivized leverage ratio"); + }); + }); + + describe("when using an exchange that has not been added", async () => { + beforeEach(async () => { + subjectExchangeName = "NonExistentExchange"; + }); + + it("should revert", async () => { + await expect(subject()).to.revertedWith("Must be valid exchange"); + }); + }); + }); + + context("when not engaged", async () => { + async function subject(): Promise { + return leverageStrategyExtension.rebalance(subjectExchangeName); + } + + describe("when collateral balance is zero", async () => { + beforeEach(async () => { + subjectExchangeName = exchangeName; + ifEngaged = false; + await intializeContracts(); + }); + + after(async () => { + ifEngaged = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Collateral balance must be > 0"); + }); + }); + }); + }); + + describe("#iterateRebalance", async () => { + let destinationTokenQuantity: BigNumber; + let subjectCaller: Account; + let subjectExchangeName: string; + let ifEngaged: boolean; + let issueQuantity: BigNumber; + + before(async () => { + ifEngaged = true; + subjectExchangeName = exchangeName; + }); + + const intializeContracts = async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + if (ifEngaged) { + // Engage to initial leverage + await leverageStrategyExtension.engage(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + await leverageStrategyExtension.iterateRebalance(subjectExchangeName); + } + }; + + cacheBeforeEach(intializeContracts); + + context("when currently in the last chunk of a TWAP rebalance", async () => { + cacheBeforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(12).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + destinationTokenQuantity = ether(0.01); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: destinationTokenQuantity, + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + + await increaseTimeAsync(BigNumber.from(4000)); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .iterateRebalance(subjectExchangeName); + } + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should remove the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + await subject(); + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected aTokens minted + const expectedFirstPositionUnit = initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + context( + "when current leverage ratio is above target and middle of a TWAP rebalance", + async () => { + let preTwapLeverageRatio: BigNumber; + + cacheBeforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(12).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + destinationTokenQuantity = ether(0.0001); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: destinationTokenQuantity, + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + preTwapLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + // Initialize TWAP + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(4000)); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .iterateRebalance(subjectExchangeName); + } + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio", async () => { + const previousTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + await subject(); + + const currentTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + preTwapLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + expect(previousTwapLeverageRatio).to.gt(expectedNewLeverageRatio.mul(999).div(1000)); + expect(previousTwapLeverageRatio).to.lt(expectedNewLeverageRatio.mul(1001).div(1000)); + expect(currentTwapLeverageRatio).to.gt(expectedNewLeverageRatio.mul(999).div(1000)); + expect(currentTwapLeverageRatio).to.lt(expectedNewLeverageRatio.mul(1001).div(1000)); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + await subject(); + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected aTokens minted + const expectedFirstPositionUnit = + initialPositions[0].unit.add(destinationTokenQuantity); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should emit RebalanceIterated event", async () => { + await expect(subject()).to.emit(leverageStrategyExtension, "RebalanceIterated"); + }); + + describe("when price has moved advantageously towards target leverage ratio", async () => { + beforeEach(async () => { + await usdcEthOrackeMock.setPrice(initialCollateralPriceInverted); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should remove the TWAP leverage ratio", async () => { + const previousTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + await subject(); + + const currentTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + preTwapLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + expect(previousTwapLeverageRatio).to.gt(expectedNewLeverageRatio.mul(999).div(1000)); + expect(previousTwapLeverageRatio).to.lt(expectedNewLeverageRatio.mul(1001).div(1000)); + expect(currentTwapLeverageRatio).to.eq(ZERO); + }); + + it("should not update the positions on the SetToken", async () => { + const initialPositions = await setToken.getPositions(); + await subject(); + const currentPositions = await setToken.getPositions(); + + expect(currentPositions[0].unit).to.eq(initialPositions[0].unit); + expect(currentPositions[1].unit).to.eq(initialPositions[1].unit); + }); + }); + + describe("when above incentivized leverage ratio threshold", async () => { + beforeEach(async () => { + await subject(); + + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(65).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Must be below incentivized leverage ratio", + ); + }); + }); + + describe("when cooldown has not elapsed", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Cooldown not elapsed or not valid leverage ratio", + ); + }); + }); + + describe("when borrow balance is 0", async () => { + beforeEach(async () => { + await usdc.approve(morpho.address, 10_000 * 10 ** 6); + const position = await morpho.position(marketId, setToken.address); + await morpho.repay( + wstethUsdcMarketParams, + 0, + position.borrowShares, + setToken.address, + EMPTY_BYTES, + ); + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Borrow balance must exist"); + }); + }); + + describe("when caller is not an allowed trader", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Address not permitted to call"); + }); + }); + + describe("when caller is a contract", async () => { + let subjectTarget: Address; + let subjectCallData: string; + let subjectValue: BigNumber; + + let contractCaller: ContractCallerMock; + + beforeEach(async () => { + contractCaller = await deployer.setV2.deployContractCallerMock(); + + subjectTarget = leverageStrategyExtension.address; + subjectCallData = leverageStrategyExtension.interface.encodeFunctionData( + "iterateRebalance", + [subjectExchangeName], + ); + subjectValue = ZERO; + }); + + async function subjectContractCaller(): Promise { + return await contractCaller.invoke(subjectTarget, subjectValue, subjectCallData); + } + + it("the trade reverts", async () => { + await expect(subjectContractCaller()).to.be.revertedWith( + "Caller must be EOA Address", + ); + }); + }); + + // TODO: See above regarding inability to redeem 100% of the supply + // describe("when SetToken has 0 supply", async () => { + // beforeEach(async () => { + // await usdc.approve(debtIssuanceModule.address, MAX_UINT_256); + // await debtIssuanceModule.redeem(setToken.address, ether(1), owner.address); + // }); + + // it("should revert", async () => { + // await expect(subject()).to.be.revertedWith("SetToken must have > 0 supply"); + // }); + // }); + + describe("when using an exchange that has not been added", async () => { + beforeEach(async () => { + subjectExchangeName = "NonExistentExchange"; + }); + + it("should revert", async () => { + await expect(subject()).to.revertedWith("Must be valid exchange"); + }); + }); + }, + ); + + context( + "when current leverage ratio is below target and middle of a TWAP rebalance", + async () => { + let preTwapLeverageRatio: BigNumber; + + cacheBeforeEach(async () => { + await increaseTimeAsync(BigNumber.from(10000000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(9).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + destinationTokenQuantity = ether(0.0001); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: destinationTokenQuantity, + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + subjectExchangeName = exchangeName; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + preTwapLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(4000)); + await usdc.transfer(tradeAdapterMock.address, BigNumber.from(2500000)); + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .iterateRebalance(subjectExchangeName); + } + + describe("when price has moved advantageously towards target leverage ratio", async () => { + beforeEach(async () => { + await usdcEthOrackeMock.setPrice(initialCollateralPriceInverted); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should remove the TWAP leverage ratio", async () => { + const previousTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + await subject(); + + const currentTwapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + preTwapLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + expect(previousTwapLeverageRatio).to.lt(expectedNewLeverageRatio.mul(1001).div(1000)); + expect(previousTwapLeverageRatio).to.gt(expectedNewLeverageRatio.mul(999).div(1000)); + expect(currentTwapLeverageRatio).to.eq(ZERO); + }); + + it("should not update the positions on the SetToken", async () => { + const initialPositions = await setToken.getPositions(); + await subject(); + const currentPositions = await setToken.getPositions(); + + expect(currentPositions[0].unit).to.eq(initialPositions[0].unit); + expect(currentPositions[1].unit).to.eq(initialPositions[1].unit); + }); + }); + }, + ); + + context("when using two exchanges", async () => { + let subjectExchangeToUse: string; + + cacheBeforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(12).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + destinationTokenQuantity = ether(0.0001); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: destinationTokenQuantity, + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + await leverageStrategyExtension.addEnabledExchange(exchangeName2, newExchangeSettings); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + + // Initialize TWAP + await leverageStrategyExtension.connect(owner.wallet).rebalance(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(4000)); + await wsteth.transfer(tradeAdapterMock.address, destinationTokenQuantity); + await wsteth.transfer(tradeAdapterMock2.address, destinationTokenQuantity); + }); + + beforeEach(() => { + subjectCaller = owner; + subjectExchangeToUse = exchangeName; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .iterateRebalance(subjectExchangeToUse); + } + + describe("when in a twap rebalance and under target leverage ratio", async () => { + it("should set the global and exchange timestamps correctly", async () => { + await subject(); + const timestamp1 = await getLastBlockTimestamp(); + + subjectExchangeToUse = exchangeName2; + await subject(); + const timestamp2 = await getLastBlockTimestamp(); + + expect(await leverageStrategyExtension.globalLastTradeTimestamp()).to.eq(timestamp2); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp1); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName2)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp2); + }); + }); + }); + + context("when not in TWAP state", async () => { + async function subject(): Promise { + return leverageStrategyExtension.iterateRebalance(subjectExchangeName); + } + + describe("when collateral balance is zero", async () => { + beforeEach(async () => { + await increaseTimeAsync(BigNumber.from(100000)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Not in TWAP state"); + }); + }); + }); + + context("when not engaged", async () => { + async function subject(): Promise { + return leverageStrategyExtension.iterateRebalance(subjectExchangeName); + } + + describe("when collateral balance is zero", async () => { + beforeEach(async () => { + // Set collateral asset to cusdc with 0 balance + ifEngaged = false; + await intializeContracts(); + subjectCaller = owner; + }); + + after(async () => { + ifEngaged = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Collateral balance must be > 0"); + }); + }); + }); + }); + describe("#ripcord", async () => { + let transferredEth: BigNumber; + let subjectCaller: Account; + let subjectExchangeName: string; + let ifEngaged: boolean; + + before(async () => { + ifEngaged = true; + subjectExchangeName = exchangeName; + }); + + const intializeContracts = async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + if (ifEngaged) { + // Engage to initial leverage + await leverageStrategyExtension.engage(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + await leverageStrategyExtension.iterateRebalance(subjectExchangeName); + } + }; + + const initializeSubjectVariables = () => { + subjectCaller = owner; + }; + + cacheBeforeEach(intializeContracts); + beforeEach(initializeSubjectVariables); + + // increaseTime + context("when not in a TWAP rebalance", async () => { + let sendQuantity: BigNumber; + cacheBeforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + await increaseTimeAsync(BigNumber.from(100000)); + + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(8).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + sendQuantity = BigNumber.from(2000 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + + transferredEth = ether(1); + await owner.wallet.sendTransaction({ + to: leverageStrategyExtension.address, + value: transferredEth, + }); + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .ripcord(subjectExchangeName); + } + + describe("When borrowValue > collateralValue * liquidationThreshold * (1 - unutilizedLeveragPercentage)", () => { + beforeEach(async () => { + const { collateralTotalBalance, borrowAssets } = await getBorrowAndCollateralBalances(); + const collateralPrice = await morphoOracle.price(); + const executionSettings = await leverageStrategyExtension.getExecution(); + const unutilizedLeveragePercentage = executionSettings.unutilizedLeveragePercentage; + const collateralValue = preciseMul(collateralPrice, collateralTotalBalance); + const collateralFactor = preciseMul( + wstethUsdcMarketParams.lltv, + ether(1).sub(unutilizedLeveragePercentage), + ); + const borrowBalanceThreshold = preciseMul(collateralValue, collateralFactor).div( + ether(1), + ); + + const relativeIncrease = borrowBalanceThreshold.mul(1000).div(borrowAssets); + const currentUsdcEthPrice = await usdcEthOrackeMock.latestAnswer(); + const currentWstethPrice = await morphoOracle.price(); + const implicitWstethEthPrice = currentWstethPrice + .mul(currentUsdcEthPrice) + .div(ether(1)) + .div(ether(1)); + const relativeIncreaseUsdcEthPrice = relativeIncrease + .mul(10 ** 6) + .div(implicitWstethEthPrice); + const newUsdcEthPrice = currentUsdcEthPrice + .mul(relativeIncreaseUsdcEthPrice.add(1)) + .div(1000); + await usdcEthOrackeMock.setPrice(newUsdcEthPrice); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(exchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should not set the TWAP leverage ratio", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + const { collateralTotalBalance } = await getBorrowAndCollateralBalances(); + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + const expectedNewLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + + // Get expected wsteth redeemed + const expectedCollateralAssetsRedeemed = calculateCollateralRebalanceUnits( + currentLeverageRatio, + expectedNewLeverageRatio, + collateralTotalBalance, + ether(1), // Total supply + ); + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + expectedCollateralAssetsRedeemed, + ); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should transfer incentive", async () => { + const previousContractEthBalance = await getEthBalance(leverageStrategyExtension.address); + const previousOwnerEthBalance = await getEthBalance(owner.address); + + const txHash = await subject(); + const txReceipt = await ethers.provider.getTransactionReceipt(txHash.hash); + const currentContractEthBalance = await getEthBalance(leverageStrategyExtension.address); + const currentOwnerEthBalance = await getEthBalance(owner.address); + const expectedOwnerEthBalance = previousOwnerEthBalance + .add(incentive.etherReward) + .sub(txReceipt.gasUsed.mul(txHash.gasPrice)); + + expect(previousContractEthBalance).to.eq(transferredEth); + expect(currentContractEthBalance).to.eq(transferredEth.sub(incentive.etherReward)); + expect(expectedOwnerEthBalance).to.eq(currentOwnerEthBalance); + }); + + it("should emit RipcordCalled event", async () => { + await expect(subject()).to.emit(leverageStrategyExtension, "RipcordCalled"); + }); + + describe("when greater than incentivized max trade size", async () => { + let newIncentivizedMaxTradeSize: BigNumber; + + cacheBeforeEach(async () => { + newIncentivizedMaxTradeSize = ether(0.01); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.001), + incentivizedTwapMaxTradeSize: newIncentivizedMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(exchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Max TWAP collateral units + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + newIncentivizedMaxTradeSize, + ); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + describe("when incentivized cooldown period has not elapsed", async () => { + beforeEach(async () => { + await subject(); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(2).div(10); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("TWAP cooldown must have elapsed"); + }); + }); + }); + + describe("when greater than max borrow", async () => { + beforeEach(async () => { + // Set to above max borrow + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(65).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(exchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + const { + collateralTotalBalance: previousCollateralBalance, + borrowAssets: previousBorrowBalance, + } = await getBorrowAndCollateralBalances(); + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + const collateralPrice = await morphoOracle.price(); + const maxRedeemCollateral = calculateMaxBorrowForDeleverV3( + previousCollateralBalance, + collateralPrice, + previousBorrowBalance, + ); + + const expectedFirstPositionUnit = initialPositions[0].unit.sub(maxRedeemCollateral); + console.log("expectedFirstPositionUnit", expectedFirstPositionUnit.toString()); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }); + + describe("when below incentivized leverage ratio threshold", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(2); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be above incentivized leverage ratio"); + }); + }); + + describe("when borrow balance is 0", async () => { + beforeEach(async () => { + await usdc.approve(morpho.address, 10_000 * 10 ** 6); + const position = await morpho.position(marketId, setToken.address); + await morpho.repay( + wstethUsdcMarketParams, + 0, + position.borrowShares, + setToken.address, + EMPTY_BYTES, + ); + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Borrow balance must exist"); + }); + }); + + describe("when caller is a contract", async () => { + let subjectTarget: Address; + let subjectCallData: string; + let subjectValue: BigNumber; + + let contractCaller: ContractCallerMock; + + beforeEach(async () => { + contractCaller = await deployer.setV2.deployContractCallerMock(); + + subjectTarget = leverageStrategyExtension.address; + subjectCallData = leverageStrategyExtension.interface.encodeFunctionData("ripcord", [ + subjectExchangeName, + ]); + subjectValue = ZERO; + }); + + async function subjectContractCaller(): Promise { + return await contractCaller.invoke(subjectTarget, subjectValue, subjectCallData); + } + + it("the trade reverts", async () => { + await expect(subjectContractCaller()).to.be.revertedWith("Caller must be EOA Address"); + }); + }); + + describe("when SetToken has 0 supply", async () => { + // TODO: Review (see above) + // beforeEach(async () => { + // await usdc.approve(debtIssuanceModule.address, MAX_UINT_256); + // await debtIssuanceModule.redeem(setToken.address, ether(1), owner.address); + // }); + // it("should revert", async () => { + // await expect(subject()).to.be.revertedWith("SetToken must have > 0 supply"); + // }); + }); + + describe("when using an exchange that has not been added", async () => { + beforeEach(async () => { + subjectExchangeName = "NonExistentExchange"; + }); + + it("should revert", async () => { + await expect(subject()).to.revertedWith("Must be valid exchange"); + }); + }); + }); + + context("when in the midst of a TWAP rebalance", async () => { + let newIncentivizedMaxTradeSize: BigNumber; + + cacheBeforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + await increaseTimeAsync(BigNumber.from(100000)); + transferredEth = ether(1); + await owner.wallet.sendTransaction({ + to: leverageStrategyExtension.address, + value: transferredEth, + }); + + // > Max trade size + newIncentivizedMaxTradeSize = ether(0.001); + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.001), + incentivizedTwapMaxTradeSize: newIncentivizedMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + subjectExchangeName = exchangeName; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + + let initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + let newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + const sendTokenQuantity = BigNumber.from(500 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendTokenQuantity); + + // Start TWAP rebalance + await leverageStrategyExtension.rebalance(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(100)); + await usdc.transfer(tradeAdapterMock.address, sendTokenQuantity); + + // Set to above incentivized ratio + initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + newCollateralPrice = initialCollateralPrice.mul(50).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .ripcord(subjectExchangeName); + } + + it("should set the global last trade timestamp", async () => { + await subject(); + + const lastTradeTimestamp = await leverageStrategyExtension.globalLastTradeTimestamp(); + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the exchange's last trade timestamp", async () => { + await subject(); + + const exchangeSettings = + await leverageStrategyExtension.getExchangeSettings(exchangeName); + const lastTradeTimestamp = exchangeSettings.exchangeLastTradeTimestamp; + + expect(lastTradeTimestamp).to.eq(await getLastBlockTimestamp()); + }); + + it("should set the TWAP leverage ratio to 0", async () => { + await subject(); + + const twapLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + + expect(twapLeverageRatio).to.eq(ZERO); + }); + }); + + context("when using two exchanges", async () => { + let subjectExchangeToUse: string; + + cacheBeforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + await increaseTimeAsync(BigNumber.from(100000)); + + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + const sendTokenQuantity = BigNumber.from(1000 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendTokenQuantity); + await usdc.transfer(tradeAdapterMock2.address, sendTokenQuantity); + + await leverageStrategyExtension.updateEnabledExchange(exchangeName, exchangeSettings); + await leverageStrategyExtension.addEnabledExchange(exchangeName2, exchangeSettings); + await increaseTimeAsync(BigNumber.from(100000)); + }); + + beforeEach(() => { + subjectCaller = owner; + subjectExchangeToUse = exchangeName; + }); + + async function subject(): Promise { + return leverageStrategyExtension + .connect(subjectCaller.wallet) + .ripcord(subjectExchangeToUse); + } + + describe("when leverage ratio is above max and it drops further between ripcords", async () => { + it("should set the global and exchange timestamps correctly", async () => { + await subject(); + const timestamp1 = await getLastBlockTimestamp(); + + subjectExchangeToUse = exchangeName2; + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(60).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + await subject(); + const timestamp2 = await getLastBlockTimestamp(); + + expect(await leverageStrategyExtension.globalLastTradeTimestamp()).to.eq(timestamp2); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp1); + expect( + (await leverageStrategyExtension.getExchangeSettings(exchangeName2)) + .exchangeLastTradeTimestamp, + ).to.eq(timestamp2); + }); + }); + }); + + context("when not engaged", async () => { + async function subject(): Promise { + return leverageStrategyExtension.ripcord(subjectExchangeName); + } + describe("when collateral balance is zero", async () => { + beforeEach(async () => { + ifEngaged = false; + await intializeContracts(); + initializeSubjectVariables(); + }); + after(async () => { + ifEngaged = true; + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Collateral balance must be > 0"); + }); + }); + }); + }); + + describe("#disengage", async () => { + let subjectCaller: Account; + let subjectExchangeName: string; + let ifEngaged: boolean; + + context( + "when notional is greater than max trade size and total rebalance notional is greater than max borrow", + async () => { + before(async () => { + ifEngaged = true; + subjectExchangeName = exchangeName; + }); + + const intializeContracts = async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + if (ifEngaged) { + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + // Engage to initial leverage + await leverageStrategyExtension.engage(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + await leverageStrategyExtension.iterateRebalance(subjectExchangeName); + + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + const sendQuantity = BigNumber.from(2000 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + } + }; + + const initializeSubjectVariables = () => { + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.disengage(subjectExchangeName); + } + + describe("when engaged", () => { + cacheBeforeEach(intializeContracts); + beforeEach(initializeSubjectVariables); + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Max TWAP collateral units + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + exchangeSettings.twapMaxTradeSize, + ); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + + describe("when borrow balance is 0", async () => { + beforeEach(async () => { + await usdc.approve(morpho.address, 10_000 * 10 ** 6); + const position = await morpho.position(marketId, setToken.address); + await morpho.repay( + wstethUsdcMarketParams, + 0, + position.borrowShares, + setToken.address, + EMPTY_BYTES, + ); + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Borrow balance must exist"); + }); + }); + + // TODO: Review (see above) + // describe("when SetToken has 0 supply", async () => { + // beforeEach(async () => { + // await usdc.approve(debtIssuanceModule.address, MAX_UINT_256); + // await debtIssuanceModule.redeem(setToken.address, ether(1), owner.address); + // }); + + // it("should revert", async () => { + // await expect(subject()).to.be.revertedWith("SetToken must have > 0 supply"); + // }); + // }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + }); + + describe("when not engaged", () => { + describe("when collateral balance is zero", async () => { + beforeEach(async () => { + ifEngaged = false; + + await intializeContracts(); + initializeSubjectVariables(); + }); + + after(async () => { + ifEngaged = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Collateral balance must be > 0"); + }); + }); + }); + }, + ); + + context( + "when notional is less than max trade size and total rebalance notional is greater than max borrow", + async () => { + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Engage to initial leverage + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + await leverageStrategyExtension.engage(subjectExchangeName); + await increaseTimeAsync(BigNumber.from(4000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + await leverageStrategyExtension.iterateRebalance(subjectExchangeName); + + // Clear balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + const sendQuantity = BigNumber.from(2500 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(1.9), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + newExchangeSettings, + ); + + // Set price to reduce borrowing power + await usdcEthOrackeMock.setPrice(initialCollateralPriceInverted); + + subjectCaller = owner; + + const oldExecution = await leverageStrategyExtension.getExecution(); + const newExecution: ExecutionSettings = { + unutilizedLeveragePercentage: oldExecution.unutilizedLeveragePercentage, + twapCooldownPeriod: oldExecution.twapCooldownPeriod, + slippageTolerance: ether(0.05), + }; + await leverageStrategyExtension.setExecutionSettings(newExecution); + }); + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.disengage(subjectExchangeName); + } + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + const { + collateralTotalBalance: previousCollateralBalance, + borrowAssets: previousBorrowBalance, + } = await getBorrowAndCollateralBalances(); + + const collateralPrice = await morphoOracle.price(); + + await subject(); + + // wsteth position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + const maxRedeemCollateral = calculateMaxBorrowForDeleverV3( + previousCollateralBalance, + collateralPrice, + previousBorrowBalance, + ); + + const expectedFirstPositionUnit = initialPositions[0].unit.sub(maxRedeemCollateral); + + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should update the borrow position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + + await subject(); + + // wsteth position is increased + const currentPositions = await setToken.getPositions(); + const newSecondPosition = (await setToken.getPositions())[1]; + const { borrowAssets } = await getBorrowAndCollateralBalances(); + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(1); // External + expect(newSecondPosition.unit).to.eq(borrowAssets.mul(-1)); + expect(newSecondPosition.module).to.eq(morphoLeverageModule.address); + }); + }, + ); + + context( + "when notional is less than max trade size and total rebalance notional is less than max borrow", + async () => { + before(async () => { + customTargetLeverageRatio = ether(1.25); // Change to 1.25x + customMinLeverageRatio = ether(1.1); + }); + + after(async () => { + customTargetLeverageRatio = undefined; + customMinLeverageRatio = undefined; + }); + + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.25)); + + // Engage to initial leverage + await leverageStrategyExtension.engage(subjectExchangeName); + + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + + const { borrowAssets } = await getBorrowAndCollateralBalances(); + // Transfer more than the borrow balance to the exchange + await usdc.transfer(tradeAdapterMock.address, borrowAssets.add(1_000_000)); + subjectCaller = owner; + }); + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.disengage(subjectExchangeName); + } + + it("should update the collateral position on the SetToken correctly", async () => { + const initialPositions = await setToken.getPositions(); + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + + const { collateralTotalBalance } = await getBorrowAndCollateralBalances(); + + await subject(); + + // cEther position is decreased + const currentPositions = await setToken.getPositions(); + const newFirstPosition = (await setToken.getPositions())[0]; + + // Get expected cTokens redeemed + const expectedCollateralAssetsRedeemed = calculateMaxRedeemForDeleverToZero( + currentLeverageRatio, + ether(1), // 1x leverage + collateralTotalBalance, + ether(1), // Total supply + execution.slippageTolerance, + ); + + const expectedFirstPositionUnit = initialPositions[0].unit.sub( + expectedCollateralAssetsRedeemed, + ); + expect(initialPositions.length).to.eq(2); + expect(currentPositions.length).to.eq(2); + expect(newFirstPosition.component).to.eq(wsteth.address); + expect(newFirstPosition.positionState).to.eq(1); // External + expect(newFirstPosition.unit).to.gt(expectedFirstPositionUnit.mul(999).div(1000)); + expect(newFirstPosition.unit).to.lt(expectedFirstPositionUnit.mul(1001).div(1000)); + expect(newFirstPosition.module).to.eq(morphoLeverageModule.address); + }); + + it("should wipe out the debt on morpho", async () => { + await subject(); + + const { borrowAssets } = await getBorrowAndCollateralBalances(); + + expect(borrowAssets).to.eq(ZERO); + }); + + it("should remove any external positions on the borrow asset", async () => { + await subject(); + + const borrowAssetExternalModules = await setToken.getExternalPositionModules( + usdc.address, + ); + const borrowExternalUnit = await setToken.getExternalPositionRealUnit( + usdc.address, + morphoLeverageModule.address, + ); + const isPositionModule = await setToken.isExternalPositionModule( + usdc.address, + morphoLeverageModule.address, + ); + + expect(borrowAssetExternalModules.length).to.eq(0); + expect(borrowExternalUnit).to.eq(ZERO); + expect(isPositionModule).to.eq(false); + }); + + it("should update the borrow asset equity on the SetToken correctly", async () => { + await subject(); + + // The usdc position is positive and represents equity + const newSecondPosition = (await setToken.getPositions())[1]; + expect(newSecondPosition.component).to.eq(usdc.address); + expect(newSecondPosition.positionState).to.eq(0); // Default + expect(BigNumber.from(newSecondPosition.unit)).to.gt(ZERO); + expect(newSecondPosition.module).to.eq(ADDRESS_ZERO); + }); + }, + ); + }); + describe("#setOverrideNoRebalanceInProgress", async () => { + let subjectOverrideNoRebalanceInProgress: boolean; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectOverrideNoRebalanceInProgress = true; + subjectCaller = owner; + }; + + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.setOverrideNoRebalanceInProgress( + subjectOverrideNoRebalanceInProgress, + ); + } + + it("should set the flag correctly", async () => { + await subject(); + const isOverride = await leverageStrategyExtension.overrideNoRebalanceInProgress(); + expect(isOverride).to.eq(subjectOverrideNoRebalanceInProgress); + }); + + describe("when caller is not the operator", () => { + beforeEach(() => { + subjectCaller = nonOwner; + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + describe("when disabling override", () => { + beforeEach(async () => { + subjectOverrideNoRebalanceInProgress = false; + await leverageStrategyExtension + .connect(owner.wallet) + .setOverrideNoRebalanceInProgress(true); + }); + it("should set the flag correctly", async () => { + await subject(); + const isOverride = await leverageStrategyExtension.overrideNoRebalanceInProgress(); + expect(isOverride).to.eq(subjectOverrideNoRebalanceInProgress); + }); + }); + }); + + describe("#setMethodologySettings", async () => { + let subjectMethodologySettings: MethodologySettings; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectMethodologySettings = { + targetLeverageRatio: ether(2.1), + minLeverageRatio: ether(1.1), + maxLeverageRatio: ether(2.5), + recenteringSpeed: ether(0.1), + rebalanceInterval: BigNumber.from(43200), + }; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.setMethodologySettings(subjectMethodologySettings); + } + + describe("when rebalance is not in progress", () => { + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + describe("when targetLeverageRatio < 1 ", () => { + beforeEach(() => { + subjectMethodologySettings.targetLeverageRatio = ether(0.99); + subjectMethodologySettings.minLeverageRatio = ether(0.89); + }); + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Target leverage ratio must be >= 1e18"); + }); + }); + it("should set the correct methodology parameters", async () => { + await subject(); + const methodology = await leverageStrategyExtension.getMethodology(); + + expect(methodology.targetLeverageRatio).to.eq( + subjectMethodologySettings.targetLeverageRatio, + ); + expect(methodology.minLeverageRatio).to.eq(subjectMethodologySettings.minLeverageRatio); + expect(methodology.maxLeverageRatio).to.eq(subjectMethodologySettings.maxLeverageRatio); + expect(methodology.recenteringSpeed).to.eq(subjectMethodologySettings.recenteringSpeed); + expect(methodology.rebalanceInterval).to.eq(subjectMethodologySettings.rebalanceInterval); + }); + + it("should emit MethodologySettingsUpdated event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "MethodologySettingsUpdated") + .withArgs( + subjectMethodologySettings.targetLeverageRatio, + subjectMethodologySettings.minLeverageRatio, + subjectMethodologySettings.maxLeverageRatio, + subjectMethodologySettings.recenteringSpeed, + subjectMethodologySettings.rebalanceInterval, + ); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when min leverage ratio is 0", async () => { + beforeEach(async () => { + subjectMethodologySettings.minLeverageRatio = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid min leverage"); + }); + }); + + describe("when min leverage ratio is above target", async () => { + beforeEach(async () => { + subjectMethodologySettings.minLeverageRatio = ether(2.2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid min leverage"); + }); + }); + + describe("when max leverage ratio is below target", async () => { + beforeEach(async () => { + subjectMethodologySettings.maxLeverageRatio = ether(1.9); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid max leverage"); + }); + }); + + describe("when max leverage ratio is above incentivized leverage ratio", async () => { + beforeEach(async () => { + subjectMethodologySettings.maxLeverageRatio = ether(5); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Incentivized leverage ratio must be > max leverage ratio", + ); + }); + }); + + describe("when recentering speed is >100%", async () => { + beforeEach(async () => { + subjectMethodologySettings.recenteringSpeed = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid recentering speed"); + }); + }); + + describe("when recentering speed is 0%", async () => { + beforeEach(async () => { + subjectMethodologySettings.recenteringSpeed = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid recentering speed"); + }); + }); + + describe("when rebalance interval is shorter than TWAP cooldown period", async () => { + beforeEach(async () => { + subjectMethodologySettings.rebalanceInterval = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Rebalance interval must be greater than TWAP cooldown period", + ); + }); + }); + }); + + describe("when rebalance is in progress", async () => { + beforeEach(async () => { + await initializeRootScopeContracts(); + initializeSubjectVariables(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Rebalance is currently in progress"); + }); + + describe("when OverrideNoRebalanceInProgress is set to true", () => { + beforeEach(async () => { + await leverageStrategyExtension.setOverrideNoRebalanceInProgress(true); + }); + it("should set the correct methodology parameters", async () => { + await subject(); + const methodology = await leverageStrategyExtension.getMethodology(); + + expect(methodology.targetLeverageRatio).to.eq( + subjectMethodologySettings.targetLeverageRatio, + ); + expect(methodology.minLeverageRatio).to.eq(subjectMethodologySettings.minLeverageRatio); + expect(methodology.maxLeverageRatio).to.eq(subjectMethodologySettings.maxLeverageRatio); + expect(methodology.recenteringSpeed).to.eq(subjectMethodologySettings.recenteringSpeed); + expect(methodology.rebalanceInterval).to.eq( + subjectMethodologySettings.rebalanceInterval, + ); + }); + }); + }); + }); + + describe("#setExecutionSettings", async () => { + let subjectExecutionSettings: ExecutionSettings; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectExecutionSettings = { + unutilizedLeveragePercentage: ether(0.05), + twapCooldownPeriod: BigNumber.from(360), + slippageTolerance: ether(0.02), + }; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.setExecutionSettings(subjectExecutionSettings); + } + + describe("when rebalance is not in progress", () => { + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + it("should set the correct execution parameters", async () => { + await subject(); + const execution = await leverageStrategyExtension.getExecution(); + + expect(execution.unutilizedLeveragePercentage).to.eq( + subjectExecutionSettings.unutilizedLeveragePercentage, + ); + expect(execution.twapCooldownPeriod).to.eq(subjectExecutionSettings.twapCooldownPeriod); + expect(execution.slippageTolerance).to.eq(subjectExecutionSettings.slippageTolerance); + }); + + it("should emit ExecutionSettingsUpdated event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "ExecutionSettingsUpdated") + .withArgs( + subjectExecutionSettings.unutilizedLeveragePercentage, + subjectExecutionSettings.twapCooldownPeriod, + subjectExecutionSettings.slippageTolerance, + ); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when unutilizedLeveragePercentage is >100%", async () => { + beforeEach(async () => { + subjectExecutionSettings.unutilizedLeveragePercentage = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Unutilized leverage must be <100%"); + }); + }); + + describe("when slippage tolerance is >100%", async () => { + beforeEach(async () => { + subjectExecutionSettings.slippageTolerance = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Slippage tolerance must be <100%"); + }); + }); + + describe("when TWAP cooldown period is greater than rebalance interval", async () => { + beforeEach(async () => { + subjectExecutionSettings.twapCooldownPeriod = ether(1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Rebalance interval must be greater than TWAP cooldown period", + ); + }); + }); + + describe("when TWAP cooldown period is shorter than incentivized TWAP cooldown period", async () => { + beforeEach(async () => { + subjectExecutionSettings.twapCooldownPeriod = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "TWAP cooldown must be greater than incentivized TWAP cooldown", + ); + }); + }); + }); + + describe("when rebalance is in progress", async () => { + beforeEach(async () => { + await initializeRootScopeContracts(); + initializeSubjectVariables(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Rebalance is currently in progress"); + }); + + describe("when OverrideNoRebalanceInProgress is set to true", () => { + beforeEach(async () => { + await leverageStrategyExtension.setOverrideNoRebalanceInProgress(true); + }); + it("should set the correct execution parameters", async () => { + await subject(); + const execution = await leverageStrategyExtension.getExecution(); + + expect(execution.unutilizedLeveragePercentage).to.eq( + subjectExecutionSettings.unutilizedLeveragePercentage, + ); + expect(execution.twapCooldownPeriod).to.eq(subjectExecutionSettings.twapCooldownPeriod); + expect(execution.slippageTolerance).to.eq(subjectExecutionSettings.slippageTolerance); + }); + }); + }); + }); + + describe("#setIncentiveSettings", async () => { + let subjectIncentiveSettings: IncentiveSettings; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectIncentiveSettings = { + incentivizedTwapCooldownPeriod: BigNumber.from(30), + incentivizedSlippageTolerance: ether(0.1), + etherReward: ether(5), + incentivizedLeverageRatio: ether(3.2), + }; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.setIncentiveSettings(subjectIncentiveSettings); + } + + describe("when rebalance is not in progress", () => { + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + it("should set the correct incentive parameters", async () => { + await subject(); + const incentive = await leverageStrategyExtension.getIncentive(); + + expect(incentive.incentivizedTwapCooldownPeriod).to.eq( + subjectIncentiveSettings.incentivizedTwapCooldownPeriod, + ); + expect(incentive.incentivizedSlippageTolerance).to.eq( + subjectIncentiveSettings.incentivizedSlippageTolerance, + ); + expect(incentive.etherReward).to.eq(subjectIncentiveSettings.etherReward); + expect(incentive.incentivizedLeverageRatio).to.eq( + subjectIncentiveSettings.incentivizedLeverageRatio, + ); + }); + + it("should emit IncentiveSettingsUpdated event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "IncentiveSettingsUpdated") + .withArgs( + subjectIncentiveSettings.etherReward, + subjectIncentiveSettings.incentivizedLeverageRatio, + subjectIncentiveSettings.incentivizedSlippageTolerance, + subjectIncentiveSettings.incentivizedTwapCooldownPeriod, + ); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when incentivized TWAP cooldown period is greater than TWAP cooldown period", async () => { + beforeEach(async () => { + subjectIncentiveSettings.incentivizedTwapCooldownPeriod = ether(1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "TWAP cooldown must be greater than incentivized TWAP cooldown", + ); + }); + }); + + describe("when incentivized slippage tolerance is >100%", async () => { + beforeEach(async () => { + subjectIncentiveSettings.incentivizedSlippageTolerance = ether(1.1); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Incentivized slippage tolerance must be <100%", + ); + }); + }); + + describe("when incentivize leverage ratio is less than max leverage ratio", async () => { + beforeEach(async () => { + subjectIncentiveSettings.incentivizedLeverageRatio = ether(2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith( + "Incentivized leverage ratio must be > max leverage ratio", + ); + }); + }); + }); + + describe("when rebalance is in progress", async () => { + beforeEach(async () => { + await initializeRootScopeContracts(); + initializeSubjectVariables(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Rebalance is currently in progress"); + }); + describe("when OverrideNoRebalanceInProgress is set to true", () => { + beforeEach(async () => { + await leverageStrategyExtension.setOverrideNoRebalanceInProgress(true); + }); + it("should set the correct incentive parameters", async () => { + await subject(); + const incentive = await leverageStrategyExtension.getIncentive(); + + expect(incentive.incentivizedTwapCooldownPeriod).to.eq( + subjectIncentiveSettings.incentivizedTwapCooldownPeriod, + ); + expect(incentive.incentivizedSlippageTolerance).to.eq( + subjectIncentiveSettings.incentivizedSlippageTolerance, + ); + expect(incentive.etherReward).to.eq(subjectIncentiveSettings.etherReward); + expect(incentive.incentivizedLeverageRatio).to.eq( + subjectIncentiveSettings.incentivizedLeverageRatio, + ); + }); + }); + }); + }); + + describe("#addEnabledExchange", async () => { + let subjectExchangeName: string; + let subjectExchangeSettings: ExchangeSettings; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectExchangeName = "NewExchange"; + subjectExchangeSettings = { + twapMaxTradeSize: ether(100), + incentivizedTwapMaxTradeSize: ether(200), + exchangeLastTradeTimestamp: BigNumber.from(0), + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.addEnabledExchange( + subjectExchangeName, + subjectExchangeSettings, + ); + } + + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + it("should set the correct exchange parameters", async () => { + await subject(); + const exchange = await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + + expect(exchange.twapMaxTradeSize).to.eq(subjectExchangeSettings.twapMaxTradeSize); + expect(exchange.incentivizedTwapMaxTradeSize).to.eq( + subjectExchangeSettings.incentivizedTwapMaxTradeSize, + ); + expect(exchange.exchangeLastTradeTimestamp).to.eq(0); + expect(exchange.leverExchangeData).to.eq(subjectExchangeSettings.leverExchangeData); + expect(exchange.deleverExchangeData).to.eq(subjectExchangeSettings.deleverExchangeData); + }); + + it("should add exchange to enabledExchanges", async () => { + await subject(); + const finalExchanges = await leverageStrategyExtension.getEnabledExchanges(); + + expect(finalExchanges.length).to.eq(2); + expect(finalExchanges[1]).to.eq(subjectExchangeName); + }); + + it("should emit an ExchangeAdded event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "ExchangeAdded") + .withArgs( + subjectExchangeName, + subjectExchangeSettings.twapMaxTradeSize, + subjectExchangeSettings.exchangeLastTradeTimestamp, + subjectExchangeSettings.incentivizedTwapMaxTradeSize, + subjectExchangeSettings.leverExchangeData, + subjectExchangeSettings.deleverExchangeData, + ); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when exchange has already been added", async () => { + beforeEach(() => { + subjectExchangeName = exchangeName; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Exchange already enabled"); + }); + }); + + describe("when an exchange has a twapMaxTradeSize of 0", async () => { + beforeEach(async () => { + subjectExchangeSettings.twapMaxTradeSize = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Max TWAP trade size must not be 0"); + }); + }); + }); + + describe("#updateEnabledExchange", async () => { + let subjectExchangeName: string; + let subjectNewExchangeSettings: ExchangeSettings; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectExchangeName = exchangeName; + subjectNewExchangeSettings = { + twapMaxTradeSize: ether(101), + incentivizedTwapMaxTradeSize: ether(201), + exchangeLastTradeTimestamp: BigNumber.from(0), + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.updateEnabledExchange( + subjectExchangeName, + subjectNewExchangeSettings, + ); + } + + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + it("should set the correct exchange parameters", async () => { + await subject(); + const exchange = await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + + expect(exchange.twapMaxTradeSize).to.eq(subjectNewExchangeSettings.twapMaxTradeSize); + expect(exchange.incentivizedTwapMaxTradeSize).to.eq( + subjectNewExchangeSettings.incentivizedTwapMaxTradeSize, + ); + expect(exchange.exchangeLastTradeTimestamp).to.eq( + subjectNewExchangeSettings.exchangeLastTradeTimestamp, + ); + expect(exchange.leverExchangeData).to.eq(subjectNewExchangeSettings.leverExchangeData); + expect(exchange.deleverExchangeData).to.eq(subjectNewExchangeSettings.deleverExchangeData); + }); + + it("should not add duplicate entry to enabledExchanges", async () => { + await subject(); + const finalExchanges = await leverageStrategyExtension.getEnabledExchanges(); + + expect(finalExchanges.length).to.eq(1); + expect(finalExchanges[0]).to.eq(subjectExchangeName); + }); + + it("should emit an ExchangeUpdated event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "ExchangeUpdated") + .withArgs( + subjectExchangeName, + subjectNewExchangeSettings.twapMaxTradeSize, + subjectNewExchangeSettings.exchangeLastTradeTimestamp, + subjectNewExchangeSettings.incentivizedTwapMaxTradeSize, + subjectNewExchangeSettings.leverExchangeData, + subjectNewExchangeSettings.deleverExchangeData, + ); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when exchange has not already been added", async () => { + beforeEach(() => { + subjectExchangeName = "NewExchange"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Exchange not enabled"); + }); + }); + + describe("when an exchange has a twapMaxTradeSize of 0", async () => { + beforeEach(async () => { + subjectNewExchangeSettings.twapMaxTradeSize = ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Max TWAP trade size must not be 0"); + }); + }); + }); + + describe("#removeEnabledExchange", async () => { + let subjectExchangeName: string; + let subjectCaller: Account; + + const initializeSubjectVariables = () => { + subjectExchangeName = exchangeName; + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.removeEnabledExchange(subjectExchangeName); + } + + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + it("should set the exchange parameters to their default values", async () => { + await subject(); + const exchange = await leverageStrategyExtension.getExchangeSettings(subjectExchangeName); + + expect(exchange.twapMaxTradeSize).to.eq(0); + expect(exchange.incentivizedTwapMaxTradeSize).to.eq(0); + expect(exchange.exchangeLastTradeTimestamp).to.eq(0); + expect(exchange.leverExchangeData).to.eq(EMPTY_BYTES); + expect(exchange.deleverExchangeData).to.eq(EMPTY_BYTES); + }); + + it("should remove entry from enabledExchanges list", async () => { + await subject(); + const finalExchanges = await leverageStrategyExtension.getEnabledExchanges(); + + expect(finalExchanges.length).to.eq(0); + }); + + it("should emit an ExchangeRemoved event", async () => { + await expect(subject()) + .to.emit(leverageStrategyExtension, "ExchangeRemoved") + .withArgs(subjectExchangeName); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + + describe("when exchange has not already been added", async () => { + beforeEach(() => { + subjectExchangeName = "NewExchange"; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Exchange not enabled"); + }); + }); + }); + + describe("#withdrawEtherBalance", async () => { + let etherReward: BigNumber; + let subjectCaller: Account; + + const initializeSubjectVariables = async () => { + etherReward = ether(0.1); + // Send ETH to contract as reward + await owner.wallet.sendTransaction({ + to: leverageStrategyExtension.address, + value: etherReward, + }); + subjectCaller = owner; + }; + + async function subject(): Promise { + leverageStrategyExtension = leverageStrategyExtension.connect(subjectCaller.wallet); + return leverageStrategyExtension.withdrawEtherBalance(); + } + + describe("when rebalance is not in progress", () => { + cacheBeforeEach(initializeRootScopeContracts); + beforeEach(initializeSubjectVariables); + + it("should withdraw ETH balance on contract to operator", async () => { + const previousContractEthBalance = await getEthBalance(leverageStrategyExtension.address); + const previousOwnerEthBalance = await getEthBalance(owner.address); + + const txHash = await subject(); + const txReceipt = await ethers.provider.getTransactionReceipt(txHash.hash); + const currentContractEthBalance = await getEthBalance(leverageStrategyExtension.address); + const currentOwnerEthBalance = await getEthBalance(owner.address); + const expectedOwnerEthBalance = previousOwnerEthBalance + .add(etherReward) + .sub(txReceipt.gasUsed.mul(txHash.gasPrice)); + + expect(previousContractEthBalance).to.eq(etherReward); + expect(currentContractEthBalance).to.eq(ZERO); + expect(expectedOwnerEthBalance).to.eq(currentOwnerEthBalance); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be operator"); + }); + }); + }); + + describe("when rebalance is in progress", async () => { + beforeEach(async () => { + await initializeRootScopeContracts(); + initializeSubjectVariables(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Rebalance is currently in progress"); + }); + }); + }); + + describe("#getCurrentEtherIncentive", async () => { + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + await leverageStrategyExtension.iterateRebalance(exchangeName); + }); + + async function subject(): Promise { + return leverageStrategyExtension.getCurrentEtherIncentive(); + } + + describe("when above incentivized leverage ratio", async () => { + beforeEach(async () => { + await owner.wallet.sendTransaction({ + to: leverageStrategyExtension.address, + value: ether(1), + }); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(65).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return the correct value", async () => { + const etherIncentive = await subject(); + + expect(etherIncentive).to.eq(incentive.etherReward); + }); + + describe("when ETH balance is below ETH reward amount", async () => { + beforeEach(async () => { + await leverageStrategyExtension.withdrawEtherBalance(); + // Transfer 0.01 ETH to contract + await owner.wallet.sendTransaction({ + to: leverageStrategyExtension.address, + value: ether(0.01), + }); + }); + + it("should return the correct value", async () => { + const etherIncentive = await subject(); + + expect(etherIncentive).to.eq(ether(0.01)); + }); + }); + }); + + describe("when below incentivized leverage ratio", async () => { + beforeEach(async () => { + await usdcEthOrackeMock.setPrice(initialCollateralPriceInverted); + }); + + it("should return the correct value", async () => { + const etherIncentive = await subject(); + + expect(etherIncentive).to.eq(ZERO); + }); + }); + }); + describe("#shouldRebalance", async () => { + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + await leverageStrategyExtension.iterateRebalance(exchangeName); + }); + + async function subject(): Promise<[string[], number[]]> { + return leverageStrategyExtension.shouldRebalance(); + } + + context("when in the midst of a TWAP rebalance", async () => { + cacheBeforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + + // > Max trade size + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.001), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange(exchangeName, newExchangeSettings); + + // Set up new rebalance TWAP + const sendQuantity = BigNumber.from(5 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + + await increaseTimeAsync(BigNumber.from(100000)); + await leverageStrategyExtension.rebalance(exchangeName); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100)); + }); + + it("should return ripcord", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(THREE); + }); + }); + + describe("when below incentivized leverage ratio and regular TWAP cooldown has elapsed", async () => { + beforeEach(async () => { + // Set to below incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(90).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(4000)); + }); + + it("should return iterate rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(TWO); + }); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + + describe("when below incentivized leverage ratio and regular TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(90).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + }); + + context("when not in a TWAP rebalance", async () => { + describe("when above incentivized leverage ratio and cooldown period has elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100)); + }); + + it("should return ripcord", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(THREE); + }); + }); + + describe("when between max and min leverage ratio and rebalance interval has elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100000)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when above max leverage ratio but below incentivized leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(85).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when below min leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(140).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + + describe("when between max and min leverage ratio and rebalance interval has NOT elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + }); + }); + describe("#shouldRebalanceWithBounds", async () => { + let subjectMinLeverageRatio: BigNumber; + let subjectMaxLeverageRatio: BigNumber; + + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + await leverageStrategyExtension.iterateRebalance(exchangeName); + }); + + beforeEach(() => { + subjectMinLeverageRatio = ether(1.6); + subjectMaxLeverageRatio = ether(2.4); + }); + + async function subject(): Promise<[string[], number[]]> { + return leverageStrategyExtension.shouldRebalanceWithBounds( + subjectMinLeverageRatio, + subjectMaxLeverageRatio, + ); + } + + context("when in the midst of a TWAP rebalance", async () => { + beforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + + // > Max trade size + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.001), + incentivizedTwapMaxTradeSize: exchangeSettings.incentivizedTwapMaxTradeSize, + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange(exchangeName, newExchangeSettings); + + // Set up new rebalance TWAP + const sendQuantity = BigNumber.from(5 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100000)); + await leverageStrategyExtension.rebalance(exchangeName); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100)); + }); + + it("should return ripcord", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(THREE); + }); + }); + + describe("when below incentivized leverage ratio and regular TWAP cooldown has elapsed", async () => { + beforeEach(async () => { + // Set to below incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(90).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(4000)); + }); + + it("should return iterate rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(TWO); + }); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + + describe("when below incentivized leverage ratio and regular TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(90).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + }); + + context("when not in a TWAP rebalance", async () => { + describe("when above incentivized leverage ratio and cooldown period has elapsed", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100)); + }); + + it("should return ripcord", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(THREE); + }); + }); + + describe("when between max and min leverage ratio and rebalance interval has elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100000)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when above max leverage ratio but below incentivized leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(85).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when below min leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(140).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ONE); + }); + }); + + describe("when above incentivized leverage ratio and incentivized TWAP cooldown has NOT elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + + describe("when between max and min leverage ratio and rebalance interval has NOT elapsed", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should not rebalance", async () => { + const [exchangeNamesArray, shouldRebalanceArray] = await subject(); + + expect(exchangeNamesArray[0]).to.eq(exchangeName); + expect(shouldRebalanceArray[0]).to.eq(ZERO); + }); + }); + + describe("when custom min leverage ratio is above methodology min leverage ratio", async () => { + beforeEach(async () => { + subjectMinLeverageRatio = ether(1.9); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Custom bounds must be valid"); + }); + }); + + describe("when custom max leverage ratio is below methodology max leverage ratio", async () => { + beforeEach(async () => { + subjectMinLeverageRatio = ether(2.2); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Custom bounds must be valid"); + }); + }); + }); + }); + describe("#getChunkRebalanceNotional", async () => { + cacheBeforeEach(async () => { + await initializeRootScopeContracts(); + + // Approve tokens to issuance module and call issue + await wsteth.approve(debtIssuanceModule.address, ether(1000)); + + // Issue 1 SetToken + const issueQuantity = ether(1); + await debtIssuanceModule.issue(setToken.address, issueQuantity, owner.address); + + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + // Add allowed trader + await leverageStrategyExtension.updateCallerStatus([owner.address], [true]); + + // Add second exchange + const exchangeSettings2 = exchangeSettings; + exchangeSettings2.twapMaxTradeSize = ether(1); + exchangeSettings2.incentivizedTwapMaxTradeSize = ether(2); + await leverageStrategyExtension.addEnabledExchange(exchangeName2, exchangeSettings2); + + // Engage to initial leverage + await leverageStrategyExtension.engage(exchangeName); + await increaseTimeAsync(BigNumber.from(100000)); + await wsteth.transfer(tradeAdapterMock.address, ether(0.5)); + + await leverageStrategyExtension.iterateRebalance(exchangeName); + }); + + async function subject(): Promise<[BigNumber[], Address, Address]> { + return await leverageStrategyExtension.getChunkRebalanceNotional([ + exchangeName, + exchangeName2, + ]); + } + + context("when in the midst of a TWAP rebalance", async () => { + beforeEach(async () => { + // Withdraw balance of usdc from exchange contract from engage + await tradeAdapterMock.withdraw(usdc.address); + + // > Max trade size + const newExchangeSettings: ExchangeSettings = { + twapMaxTradeSize: ether(0.001), + incentivizedTwapMaxTradeSize: ether(0.002), + exchangeLastTradeTimestamp: exchangeSettings.exchangeLastTradeTimestamp, + leverExchangeData: EMPTY_BYTES, + deleverExchangeData: EMPTY_BYTES, + }; + await leverageStrategyExtension.updateEnabledExchange(exchangeName, newExchangeSettings); + + // Set up new rebalance TWAP + const sendQuantity = BigNumber.from(5 * 10 ** 6); + await usdc.transfer(tradeAdapterMock.address, sendQuantity); + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + await increaseTimeAsync(BigNumber.from(100000)); + await leverageStrategyExtension.rebalance(exchangeName); + }); + + describe("when above incentivized leverage ratio", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const newLeverageRatio = methodology.maxLeverageRatio; + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const expectedTotalRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + + expect(sellAsset).to.eq(strategy.collateralAsset); + expect(buyAsset).to.eq(strategy.borrowAsset); + expect(chunkRebalances[0]).to.eq(ether(0.002)); + expect(chunkRebalances[1]).to.gte(expectedTotalRebalance.sub(1)); + expect(chunkRebalances[1]).to.lte(expectedTotalRebalance.add(1)); + }); + }); + + describe("when below incentivized leverage ratio", async () => { + beforeEach(async () => { + // Set to below incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(90).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const newLeverageRatio = await leverageStrategyExtension.twapLeverageRatio(); + const expectedTotalRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + + expect(sellAsset).to.eq(strategy.collateralAsset); + expect(buyAsset).to.eq(strategy.borrowAsset); + expect(chunkRebalances[0]).to.eq(ether(0.001)); + // TODO: Had to add this rounding error tolerance, understand why + expect(chunkRebalances[1]).to.gte(expectedTotalRebalance.sub(1)); + expect(chunkRebalances[1]).to.lte(expectedTotalRebalance.add(1)); + }); + }); + }); + + context("when not in a TWAP rebalance", async () => { + beforeEach(async () => { + const exchangeSettings2 = exchangeSettings; + exchangeSettings2.twapMaxTradeSize = ether(0.001); + exchangeSettings2.incentivizedTwapMaxTradeSize = ether(0.002); + await leverageStrategyExtension.updateEnabledExchange(exchangeName2, exchangeSettings2); + }); + + describe("when above incentivized leverage ratio", async () => { + beforeEach(async () => { + // Set to above incentivized ratio + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(80).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const newLeverageRatio = methodology.maxLeverageRatio; + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const expectedTotalRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + + expect(sellAsset).to.eq(strategy.collateralAsset); + expect(buyAsset).to.eq(strategy.borrowAsset); + expect(chunkRebalances[0]).to.lte(expectedTotalRebalance.add(1)); + expect(chunkRebalances[0]).to.gte(expectedTotalRebalance.sub(1)); + expect(chunkRebalances[1]).to.eq(ether(0.002)); + }); + }); + + describe("when between max and min leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(99).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const newLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + const expectedTotalRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + + expect(sellAsset).to.eq(strategy.collateralAsset); + expect(buyAsset).to.eq(strategy.borrowAsset); + expect(chunkRebalances[0]).to.eq(expectedTotalRebalance); + expect(chunkRebalances[1]).to.eq(ether(0.001)); + }); + }); + + describe("when above max leverage ratio but below incentivized leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(85).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const newLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + const expectedTotalRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + + expect(sellAsset).to.eq(strategy.collateralAsset); + expect(buyAsset).to.eq(strategy.borrowAsset); + expect(chunkRebalances[0]).to.gte(expectedTotalRebalance.sub(1)); + expect(chunkRebalances[0]).to.lte(expectedTotalRebalance.add(1)); + expect(chunkRebalances[1]).to.eq(ether(0.001)); + }); + }); + + describe("when below min leverage ratio", async () => { + beforeEach(async () => { + const initialCollateralPrice = ether(1).div(initialCollateralPriceInverted); + const newCollateralPrice = initialCollateralPrice.mul(140).div(100); + await usdcEthOrackeMock.setPrice(ether(1).div(newCollateralPrice)); + }); + + it("should return correct total rebalance size and isLever boolean", async () => { + const [chunkRebalances, sellAsset, buyAsset] = await subject(); + + const currentLeverageRatio = await leverageStrategyExtension.getCurrentLeverageRatio(); + const newLeverageRatio = calculateNewLeverageRatio( + currentLeverageRatio, + methodology.targetLeverageRatio, + methodology.minLeverageRatio, + methodology.maxLeverageRatio, + methodology.recenteringSpeed, + ); + const totalCollateralRebalance = await calculateTotalRebalanceNotional( + currentLeverageRatio, + newLeverageRatio, + ); + // Multiply collateral by conversion rate + const currentCollateralPrice = (await morphoOracle.price()).div(ether(1)); + const expectedTotalRebalance = preciseMul( + totalCollateralRebalance, + currentCollateralPrice, + ); + + expect(sellAsset).to.eq(strategy.borrowAsset); + expect(buyAsset).to.eq(strategy.collateralAsset); + expect(chunkRebalances[0]).to.eq(expectedTotalRebalance); + expect(chunkRebalances[1]).to.eq(preciseMul(ether(0.001), currentCollateralPrice)); + }); + }); + }); + }); + }); +} diff --git a/utils/config.ts b/utils/config.ts index 67b5ca2c7..676736dca 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -15,7 +15,7 @@ export const arbitrumForkingConfig = { export const mainnetForkingConfig = { url: "https://eth-mainnet.alchemyapi.io/v2/" + process.env.ALCHEMY_TOKEN, - blockNumber: process.env.LATESTBLOCK ? undefined : 19740000, + blockNumber: process.env.LATESTBLOCK ? undefined : 20660000, }; export const forkingConfig = diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 4d3147495..8be503dab 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -39,6 +39,8 @@ import { AaveV3LeverageStrategyExtension, AaveV3LeverageStrategyExtension__factory, FlashMintLeveragedExtended__factory, + MorphoLeverageStrategyExtension, + MorphoLeverageStrategyExtension__factory, } from "../../typechain"; import { AirdropExtension__factory } from "../../typechain/factories/AirdropExtension__factory"; import { AuctionRebalanceExtension__factory } from "../../typechain/factories/AuctionRebalanceExtension__factory"; @@ -383,7 +385,7 @@ export default class DeployExtensions { setControllerAddress, debtIssuanceModuleAddress, stETHAddress, - curveStEthEthPoolAddress + curveStEthEthPoolAddress, ); } @@ -426,11 +428,10 @@ export default class DeployExtensions { setControllerAddress, debtIssuanceModuleAddress, stETHAddress, - curveStEthEthPoolAddress + curveStEthEthPoolAddress, ); } - public async deployExchangeIssuanceLeveragedForCompound( wethAddress: Address, quickRouterAddress: Address, @@ -511,19 +512,15 @@ export default class DeployExtensions { }, // @ts-ignore this._deployerSigner, - ).deploy( - setControllerAddress, - indexControllerAddress, - { - quickRouter: quickRouterAddress, - sushiRouter: sushiRouterAddress, - uniV3Router: uniV3RouterAddress, - uniV3Quoter: uniswapV3QuoterAddress, - curveAddressProvider: curveAddressProviderAddress, - curveCalculator: curveCalculatorAddress, - weth: wethAddress, - }, - ); + ).deploy(setControllerAddress, indexControllerAddress, { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }); } public async deployFlashMintNAV( @@ -691,6 +688,26 @@ export default class DeployExtensions { ); } + public async deployMorphoLeverageStrategyExtension( + manager: Address, + contractSettings: AaveContractSettings, + methdologySettings: MethodologySettings, + executionSettings: ExecutionSettings, + incentiveSettings: IncentiveSettings, + exchangeNames: string[], + exchangeSettings: ExchangeSettings[], + ): Promise { + return await new MorphoLeverageStrategyExtension__factory(this._deployerSigner).deploy( + manager, + contractSettings, + methdologySettings, + executionSettings, + incentiveSettings, + exchangeNames, + exchangeSettings, + ); + } + public async deployWrapExtension(manager: Address, wrapModule: Address): Promise { return await new WrapExtension__factory(this._deployerSigner).deploy(manager, wrapModule); } diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index ef56f3319..57ad6e679 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -49,6 +49,10 @@ import { AaveV3LeverageModule__factory, AaveV3, AaveV3__factory, + Morpho, + Morpho__factory, + MorphoLeverageModule, + MorphoLeverageModule__factory, } from "../../typechain"; import { WETH9, StandardTokenMock } from "../contracts/index"; import { ether } from "../common"; @@ -261,6 +265,10 @@ export default class DeploySetV2 { return await new AaveV3__factory(this._deployerSigner).deploy(); } + public async deployMorphoLib(): Promise { + return await new Morpho__factory(this._deployerSigner).deploy(); + } + public async deployAaveLeverageModule( controller: string, lendingPoolAddressesProvider: string, @@ -302,11 +310,33 @@ export default class DeploySetV2 { ).deploy(controller, lendingPoolAddressesProvider); } + public async deployMorphoLeverageModule( + controller: string, + morpho: string, + ): Promise { + const morphoLib = await this.deployMorphoLib(); + + const linkId = convertLibraryNameToLinkId( + "contracts/protocol/integration/lib/Morpho.sol:Morpho", + ); + + return await new MorphoLeverageModule__factory( + // @ts-ignore + { + [linkId]: morphoLib.address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy(controller, morpho); + } + public async deployAirdropModule(controller: Address): Promise { return await new AirdropModule__factory(this._deployerSigner).deploy(controller); } - public async deployAuctionRebalanceModuleV1(controller: Address): Promise { + public async deployAuctionRebalanceModuleV1( + controller: Address, + ): Promise { return await new AuctionRebalanceModuleV1__factory(this._deployerSigner).deploy(controller); } @@ -362,27 +392,39 @@ export default class DeploySetV2 { ); } - public async deployDebtIssuanceModuleV3(controller: Address, tokenTransferBuffer: BigNumberish): Promise { - return await new DebtIssuanceModuleV3__factory(this._deployerSigner).deploy(controller, tokenTransferBuffer); + public async deployDebtIssuanceModuleV3( + controller: Address, + tokenTransferBuffer: BigNumberish, + ): Promise { + return await new DebtIssuanceModuleV3__factory(this._deployerSigner).deploy( + controller, + tokenTransferBuffer, + ); } public async deployERC4626Oracle( vault: Address, underlyingFullUnit: BigNumber, - dataDescription: string): Promise { - return await new ERC4626Oracle__factory(this._deployerSigner).deploy(vault, underlyingFullUnit, dataDescription); + dataDescription: string, + ): Promise { + return await new ERC4626Oracle__factory(this._deployerSigner).deploy( + vault, + underlyingFullUnit, + dataDescription, + ); } public async deployOracleMock(initialValue: BigNumberish): Promise { return await new OracleMock__factory(this._deployerSigner).deploy(initialValue); } - public async deployPreciseUnitOracle( - dataDescription: string): Promise { + public async deployPreciseUnitOracle(dataDescription: string): Promise { return await new PreciseUnitOracle__factory(this._deployerSigner).deploy(dataDescription); } - public async deployRebasingComponentModule(controller: Address): Promise { + public async deployRebasingComponentModule( + controller: Address, + ): Promise { return await new RebasingComponentModule__factory(this._deployerSigner).deploy(controller); } diff --git a/utils/test/index.ts b/utils/test/index.ts index cafaa6b23..4e92917ab 100644 --- a/utils/test/index.ts +++ b/utils/test/index.ts @@ -17,4 +17,5 @@ export { mineBlockAsync, cacheBeforeEach, getTxFee, + convertPositionToNotional, } from "./testingUtils"; diff --git a/utils/test/testingUtils.ts b/utils/test/testingUtils.ts index c1a16bab5..4a04c88c4 100644 --- a/utils/test/testingUtils.ts +++ b/utils/test/testingUtils.ts @@ -8,6 +8,9 @@ import { BigNumber, ContractTransaction, Signer } from "ethers"; import { JsonRpcProvider } from "@ethersproject/providers"; import { Blockchain } from "../common"; import { forkingConfig } from "../config"; +import { + SetToken, +} from "../../typechain"; const provider = ethers.provider; // const blockchain = new Blockchain(provider); @@ -153,3 +156,10 @@ export function setBlockNumber(blockNumber: number, reset: boolean = true) { export async function getLastBlockTransaction(): Promise { return (await provider.getBlockWithTransactions("latest")).transactions[0]; } + +export async function convertPositionToNotional( + positionAmount: BigNumber, + setToken: SetToken, +): Promise { + return positionAmount.mul(await setToken.totalSupply()).div(BigNumber.from(10).pow(18)); +}