diff --git a/contracts/adapters/AaveMigrationExtension.sol b/contracts/adapters/AaveMigrationExtension.sol new file mode 100644 index 00000000..92756851 --- /dev/null +++ b/contracts/adapters/AaveMigrationExtension.sol @@ -0,0 +1,632 @@ +/* + 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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { FlashLoanSimpleReceiverBase } from "../lib/FlashLoanSimpleReceiverBase.sol"; +import { IPoolAddressesProvider } from "../interfaces/IPoolAddressesProvider.sol"; + +import { INonfungiblePositionManager } from "../interfaces/external/uniswap-v3/INonfungiblePositionManager.sol"; +import { IUniswapV3Pool } from "../interfaces/external/uniswap-v3/IUniswapV3Pool.sol"; + +import { BaseExtension } from "../lib/BaseExtension.sol"; +import { IBaseManager } from "../interfaces/IBaseManager.sol"; +import { IDebtIssuanceModule } from "../interfaces/IDebtIssuanceModule.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { ITradeModule } from "../interfaces/ITradeModule.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + +/** + * @title AaveMigrationExtension + * @author Index Coop + * @notice This extension facilitates the migration of a SetToken's position from an unwrapped collateral + * asset to another SetToken that consists solely of Aave's wrapped collateral asset. The migration is + * executed through several steps: obtaining a flash loan of the unwrapped collateral, minting the required + * quantity of the wrapped SetToken, adding liquidity to the Uniswap V3 pool, swapping the unwrapped + * collateral for the wrapped SetToken, removing liquidity from the Uniswap V3 pool, and finally, + * redeeming any excess wrapped SetToken. This process is specifically designed to efficiently migrate + * the SetToken's collateral using only the TradeModule on the SetToken. + */ +contract AaveMigrationExtension is BaseExtension, FlashLoanSimpleReceiverBase, IERC721Receiver { + using PreciseUnitMath for uint256; + using SafeCast for int256; + using SafeERC20 for IERC20; + using SafeMath for uint256; + + /* ============ Structs ============ */ + + struct DecodedParams { + uint256 supplyLiquidityAmount0Desired; + uint256 supplyLiquidityAmount1Desired; + uint256 supplyLiquidityAmount0Min; + uint256 supplyLiquidityAmount1Min; + uint256 tokenId; + string exchangeName; + uint256 underlyingTradeUnits; + uint256 wrappedSetTokenTradeUnits; + bytes exchangeData; + uint256 redeemLiquidityAmount0Min; + uint256 redeemLiquidityAmount1Min; + bool isUnderlyingToken0; + } + + /* ========== State Variables ========= */ + + ISetToken public immutable setToken; + IERC20 public immutable underlyingToken; + IERC20 public immutable aaveToken; + ISetToken public immutable wrappedSetToken; + ITradeModule public immutable tradeModule; + IDebtIssuanceModule public immutable issuanceModule; + INonfungiblePositionManager public immutable nonfungiblePositionManager; + + uint256[] public tokenIds; // UniV3 LP Token IDs + + /* ============ Constructor ============ */ + + /** + * @notice Initializes the AaveMigrationExtension with immutable migration variables. + * @param _manager BaseManager contract for managing the SetToken's operations and permissions. + * @param _underlyingToken Address of the underlying token to be migrated. + * @param _aaveToken Address of Aave's wrapped collateral asset. + * @param _wrappedSetToken SetToken that consists solely of Aave's wrapped collateral asset. + * @param _tradeModule TradeModule address for executing trades on behalf of the SetToken. + * @param _issuanceModule IssuanceModule address for managing issuance and redemption of the Wrapped SetToken. + * @param _nonfungiblePositionManager Uniswap V3's NonFungiblePositionManager for managing liquidity positions. + * @param _addressProvider Aave V3's Pool Address Provider, used for accessing the Aave lending pool. + */ + constructor( + IBaseManager _manager, + IERC20 _underlyingToken, + IERC20 _aaveToken, + ISetToken _wrappedSetToken, + ITradeModule _tradeModule, + IDebtIssuanceModule _issuanceModule, + INonfungiblePositionManager _nonfungiblePositionManager, + IPoolAddressesProvider _addressProvider + ) + public + BaseExtension(_manager) + FlashLoanSimpleReceiverBase(_addressProvider) + { + manager = _manager; + setToken = manager.setToken(); + underlyingToken = _underlyingToken; + aaveToken = _aaveToken; + wrappedSetToken = _wrappedSetToken; + tradeModule = _tradeModule; + issuanceModule = _issuanceModule; + nonfungiblePositionManager = _nonfungiblePositionManager; + } + + /* ========== External Functions ========== */ + + /** + * @notice OPERATOR ONLY: Initializes the Set Token on the Trade Module. + */ + function initialize() external onlyOperator { + bytes memory data = abi.encodeWithSelector(tradeModule.initialize.selector, setToken); + invokeManager(address(tradeModule), data); + } + + /** + * @notice OPERATOR ONLY: Executes a trade on a supported DEX. + * @dev Although the SetToken units are passed in for the send and receive quantities, the total quantity + * sent and received is the quantity of SetToken units multiplied by the SetToken totalSupply. + * @param _exchangeName The human-readable name of the exchange in the integrations registry. + * @param _sendToken The address of the token being sent to the exchange. + * @param _sendQuantity The amount of the token (in SetToken units) being sent to the exchange. + * @param _receiveToken The address of the token being received from the exchange. + * @param _minReceiveQuantity The minimum amount of the receive token (in SetToken units) expected from the exchange. + * @param _data Arbitrary data used to construct the trade call data. + */ + function trade( + string memory _exchangeName, + address _sendToken, + uint256 _sendQuantity, + address _receiveToken, + uint256 _minReceiveQuantity, + bytes memory _data + ) + external + onlyOperator + { + _trade( + _exchangeName, + _sendToken, + _sendQuantity, + _receiveToken, + _minReceiveQuantity, + _data + ); + } + + /** + * @notice OPERATOR ONLY: Mints a new liquidity position in the Uniswap V3 pool. + * @param _amount0Desired The desired amount of token0 to be added as liquidity. + * @param _amount1Desired The desired amount of token1 to be added as liquidity. + * @param _amount0Min The minimum amount of token0 to be added as liquidity. + * @param _amount1Min The minimum amount of token1 to be added as liquidity. + * @param _tickLower The lower end of the desired tick range for the position. + * @param _tickUpper The upper end of the desired tick range for the position. + * @param _fee The fee tier of the Uniswap V3 pool in which to add liquidity. + * @param _isUnderlyingToken0 True if the underlying token is token0, false if it is token1. + */ + function mintLiquidityPosition( + uint256 _amount0Desired, + uint256 _amount1Desired, + uint256 _amount0Min, + uint256 _amount1Min, + int24 _tickLower, + int24 _tickUpper, + uint24 _fee, + bool _isUnderlyingToken0 + ) + external + onlyOperator + { + _mintLiquidityPosition( + _amount0Desired, + _amount1Desired, + _amount0Min, + _amount1Min, + _tickLower, + _tickUpper, + _fee, + _isUnderlyingToken0 + ); + } + + /** + * @notice OPERATOR ONLY: Increases liquidity position in the Uniswap V3 pool. + * @param _amount0Desired The desired amount of token0 to be added as liquidity. + * @param _amount1Desired The desired amount of token1 to be added as liquidity. + * @param _amount0Min The minimum amount of token0 to be added as liquidity. + * @param _amount1Min The minimum amount of token1 to be added as liquidity. + * @param _tokenId The ID of the UniV3 LP Token for which liquidity is being increased. + * @param _isUnderlyingToken0 True if the underlying token is token0, false if it is token1. + * @return liquidity The new liquidity amount as a result of the increase. + */ + function increaseLiquidityPosition( + uint256 _amount0Desired, + uint256 _amount1Desired, + uint256 _amount0Min, + uint256 _amount1Min, + uint256 _tokenId, + bool _isUnderlyingToken0 + ) + external + onlyOperator + returns (uint128 liquidity) + { + liquidity = _increaseLiquidityPosition( + _amount0Desired, + _amount1Desired, + _amount0Min, + _amount1Min, + _tokenId, + _isUnderlyingToken0 + ); + } + + /** + * @notice OPERATOR ONLY: Decreases and collects from a liquidity position in the Uniswap V3 pool. + * @param _tokenId The ID of the UniV3 LP Token for which liquidity is being decreased. + * @param _liquidity The amount of liquidity to decrease. + * @param _amount0Min The minimum amount of token0 that should be accounted for the burned liquidity. + * @param _amount1Min The minimum amount of token1 that should be accounted for the burned liquidity. + */ + function decreaseLiquidityPosition( + uint256 _tokenId, + uint128 _liquidity, + uint256 _amount0Min, + uint256 _amount1Min + ) + external + onlyOperator + { + _decreaseLiquidityPosition( + _tokenId, + _liquidity, + _amount0Min, + _amount1Min + ); + } + + /** + * @notice OPERATOR ONLY: Migrates a SetToken's position from an unwrapped collateral asset to another SetToken + * that consists solely of Aave's wrapped collateral asset + * @param _decodedParams The decoded migration parameters. + * @param _underlyingLoanAmount The amount of unwrapped collateral asset to be borrowed via flash loan. + * @param _maxSubsidy The maximum amount of unwrapped collateral asset to be transferred to the Extension as a subsidy. + * @return underlyingOutputAmount The amount of unwrapped collateral asset returned to the operator. + */ + function migrate( + DecodedParams memory _decodedParams, + uint256 _underlyingLoanAmount, + uint256 _maxSubsidy + ) + external + onlyOperator + returns (uint256 underlyingOutputAmount) + { + // Subsidize the migration + if (_maxSubsidy > 0) { + underlyingToken.transferFrom(msg.sender, address(this), _maxSubsidy); + } + + // Encode migration parameters for flash loan callback + bytes memory params = abi.encode(_decodedParams); + + // Request flash loan for the underlying token + POOL.flashLoanSimple( + address(this), + address(underlyingToken), + _underlyingLoanAmount, + params, + 0 + ); + + // Return remaining underlying token to the operator + underlyingOutputAmount = _returnExcessUnderlying(); + } + + /** + * @dev Callback function for Aave V3 flash loan, executed post-loan. It decodes the provided parameters, conducts the migration, and repays the flash loan. + * @param amount The amount borrowed. + * @param premium The additional fee charged for the flash loan. + * @param initiator The initiator of the flash loan. + * @param params Encoded migration parameters. + * @return True if the operation is successful. + */ + function executeOperation( + address, // asset + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) + external + override + returns (bool) + { + require(msg.sender == address(POOL), "MigrationExtension: invalid flashloan sender"); + require(initiator == address(this), "MigrationExtension: invalid flashloan initiator"); + + // Decode parameters and migrate + DecodedParams memory decodedParams = abi.decode(params, (DecodedParams)); + _migrate(decodedParams); + + underlyingToken.approve(address(POOL), amount + premium); + return true; + } + + /** + * @notice Receives ERC721 tokens, required for Uniswap V3 LP NFT handling. + * @dev Callback function for ERC721 token transfers, enabling the contract to receive Uniswap V3 LP NFTs. Always returns the selector to indicate successful receipt. + * @return The selector of the `onERC721Received` function. + */ + function onERC721Received( + address, // operator + address, // from + uint256, // tokenId + bytes calldata // data + ) + external + override + returns (bytes4) + { + return this.onERC721Received.selector; + } + + /** + * @notice OPERATOR ONLY: Transfers any residual balances to the operator's address. + * @dev This function is intended to recover tokens that might have been left behind + * due to the migration process or any other operation. It ensures that the contract + * does not retain any assets inadvertently. Only callable by the operator. + * @param _token The address of the token to be swept. + */ + function sweepTokens(address _token) external onlyOperator { + IERC20 token = IERC20(_token); + uint256 balance = token.balanceOf(address(this)); + require(balance > 0, "MigrationExtension: no balance to sweep"); + token.transfer(manager.operator(), balance); + } + + /* ========== Internal Functions ========== */ + + /** + * @dev Conducts the actual migration steps utilizing the decoded parameters from the flash loan callback. + * @param decodedParams The decoded set of parameters needed for migration. + */ + function _migrate(DecodedParams memory decodedParams) internal { + uint256 wrappedSetTokenSupplyLiquidityAmount = decodedParams.isUnderlyingToken0 + ? decodedParams.supplyLiquidityAmount1Desired + : decodedParams.supplyLiquidityAmount0Desired; + + _issueRequiredWrappedSetToken(wrappedSetTokenSupplyLiquidityAmount); + + uint128 liquidity = _increaseLiquidityPosition( + decodedParams.supplyLiquidityAmount0Desired, + decodedParams.supplyLiquidityAmount1Desired, + decodedParams.supplyLiquidityAmount0Min, + decodedParams.supplyLiquidityAmount1Min, + decodedParams.tokenId, + decodedParams.isUnderlyingToken0 + ); + + _trade( + decodedParams.exchangeName, + address(underlyingToken), + decodedParams.underlyingTradeUnits, + address(wrappedSetToken), + decodedParams.wrappedSetTokenTradeUnits, + decodedParams.exchangeData + ); + + _decreaseLiquidityPosition( + decodedParams.tokenId, + liquidity, + decodedParams.redeemLiquidityAmount0Min, + decodedParams.redeemLiquidityAmount1Min + ); + + _redeemExcessWrappedSetToken(); + } + + /** + * @dev Internal function to execute trades. This function constructs the trade call data and invokes the trade module + * to execute the trade. The SetToken units for send and receive quantities are automatically scaled up by the SetToken's + * total supply. + * @param _exchangeName The human-readable name of the exchange in the integrations registry. + * @param _sendToken The address of the token being sent to the exchange. + * @param _sendQuantity The amount of the token (in SetToken units) being sent to the exchange. + * @param _receiveToken The address of the token being received from the exchange. + * @param _minReceiveQuantity The minimum amount of the receive token (in SetToken units) expected from the exchange. + * @param _data Arbitrary data used to construct the trade call data. + */ + function _trade( + string memory _exchangeName, + address _sendToken, + uint256 _sendQuantity, + address _receiveToken, + uint256 _minReceiveQuantity, + bytes memory _data + ) + internal + { + bytes memory callData = abi.encodeWithSignature( + "trade(address,string,address,uint256,address,uint256,bytes)", + setToken, + _exchangeName, + _sendToken, + _sendQuantity, + _receiveToken, + _minReceiveQuantity, + _data + ); + invokeManager(address(tradeModule), callData); + } + + /** + * @dev Issues the required amount of wrapped SetToken for the liquidity increase + * @param _wrappedSetTokenSupplyLiquidityAmount The amount of wrapped SetToken to be supplied to the pool. + */ + function _issueRequiredWrappedSetToken(uint256 _wrappedSetTokenSupplyLiquidityAmount) internal { + uint256 wrappedSetTokenBalance = wrappedSetToken.balanceOf(address(this)); + if (_wrappedSetTokenSupplyLiquidityAmount > wrappedSetTokenBalance) { + uint256 wrappedSetTokenIssueAmount = _wrappedSetTokenSupplyLiquidityAmount.sub(wrappedSetTokenBalance); + (address[] memory underlyingAssets ,uint256[] memory underlyingUnits,) = issuanceModule.getRequiredComponentIssuanceUnits( + wrappedSetToken, + wrappedSetTokenIssueAmount + ); + require(underlyingAssets.length == 1, "MigrationExtension: invalid wrapped SetToken composition"); + require(underlyingAssets[0] == address(aaveToken), "MigrationExtension: wrapped SetToken underlying mismatch"); + + // Supply underlying for Aave wrapped token + underlyingToken.approve(address(POOL), underlyingUnits[0]); + POOL.supply( + address(underlyingToken), + underlyingUnits[0], + address(this), + 0 + ); + + // Issue wrapped SetToken + aaveToken.approve(address(issuanceModule), wrappedSetTokenIssueAmount); + issuanceModule.issue(wrappedSetToken, wrappedSetTokenIssueAmount, address(this)); + } + } + + /** + * @dev Redeems any excess wrapped SetToken after liquidity decrease + */ + function _redeemExcessWrappedSetToken() internal { + uint256 wrappedSetTokenBalance = wrappedSetToken.balanceOf(address(this)); + if (wrappedSetTokenBalance > 0) { + // Redeem wrapped SetToken + wrappedSetToken.approve(address(issuanceModule), wrappedSetTokenBalance); + issuanceModule.redeem(wrappedSetToken, wrappedSetTokenBalance, address(this)); + + // Withdraw underlying from Aave + uint256 aaveBalance = aaveToken.balanceOf(address(this)); + aaveToken.approve(address(POOL), aaveBalance); + POOL.withdraw( + address(underlyingToken), + aaveBalance, + address(this) + ); + } + } + + /** + * @dev Internal function to mint a new liquidity position in the Uniswap V3 pool. + * Calls Uniswap's `mint` function with specified parameters. + * @param _amount0Desired The desired amount of token0 to be added as liquidity. + * @param _amount1Desired The desired amount of token1 to be added as liquidity. + * @param _amount0Min The minimum amount of token0 to be added as liquidity. + * @param _amount1Min The minimum amount of token1 to be added as liquidity. + * @param _tickLower The lower end of the desired tick range for the position. + * @param _tickUpper The upper end of the desired tick range for the position. + * @param _fee The fee tier of the Uniswap V3 pool in which to add liquidity. + * @param _isUnderlyingToken0 True if the underlying token is token0, false if it is token1. + */ + function _mintLiquidityPosition( + uint256 _amount0Desired, + uint256 _amount1Desired, + uint256 _amount0Min, + uint256 _amount1Min, + int24 _tickLower, + int24 _tickUpper, + uint24 _fee, + bool _isUnderlyingToken0 + ) internal { + // Sort tokens and amounts + ( + address token0, + address token1, + uint256 underlyingAmount, + uint256 wrappedSetTokenAmount + ) = _isUnderlyingToken0 + ? (address(underlyingToken), address(wrappedSetToken), _amount0Desired, _amount1Desired) + : (address(wrappedSetToken), address(underlyingToken), _amount1Desired, _amount0Desired); + + // Approve tokens + if (underlyingAmount > 0) { + underlyingToken.approve(address(nonfungiblePositionManager), underlyingAmount); + } + if (wrappedSetTokenAmount > 0) { + wrappedSetToken.approve(address(nonfungiblePositionManager), wrappedSetTokenAmount); + } + + // Mint liquidity position + INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({ + token0: token0, + token1: token1, + fee: _fee, + tickLower: _tickLower, + tickUpper: _tickUpper, + amount0Desired: _amount0Desired, + amount1Desired: _amount1Desired, + amount0Min: _amount0Min, + amount1Min: _amount1Min, + recipient: address(this), + deadline: block.timestamp + }); + (uint256 tokenId,,,) = nonfungiblePositionManager.mint(mintParams); + tokenIds.push(tokenId); + } + + /** + * @dev Internal function to increase liquidity in a Uniswap V3 pool position. + * Calls Uniswap's `increaseLiquidity` function with specified parameters. + * @param _amount0Desired The desired amount of token0 to be added as liquidity. + * @param _amount1Desired The desired amount of token1 to be added as liquidity. + * @param _amount0Min The minimum amount of token0 to be added as liquidity. + * @param _amount1Min The minimum amount of token1 to be added as liquidity. + * @param _tokenId The ID of the UniV3 LP Token for which liquidity is being increased. + * @param _isUnderlyingToken0 True if the underlying token is token0, false if it is token1. + * @return liquidity The new liquidity amount as a result of the increase. + */ + function _increaseLiquidityPosition( + uint256 _amount0Desired, + uint256 _amount1Desired, + uint256 _amount0Min, + uint256 _amount1Min, + uint256 _tokenId, + bool _isUnderlyingToken0 + ) + internal + returns (uint128 liquidity) + { + (uint256 underlyingAmount, uint256 wrappedSetTokenAmount) = _isUnderlyingToken0 + ? (_amount0Desired, _amount1Desired) + : (_amount1Desired, _amount0Desired); + + // Approve tokens + if (underlyingAmount > 0) { + underlyingToken.approve(address(nonfungiblePositionManager), underlyingAmount); + } + if (wrappedSetTokenAmount > 0) { + wrappedSetToken.approve(address(nonfungiblePositionManager), wrappedSetTokenAmount); + } + + // Increase liquidity + INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: _tokenId, + amount0Desired: _amount0Desired, + amount1Desired: _amount1Desired, + amount0Min: _amount0Min, + amount1Min: _amount1Min, + deadline: block.timestamp + }); + (liquidity,,) = nonfungiblePositionManager.increaseLiquidity(increaseParams); + } + + /** + * @dev Internal function to decrease liquidity and collect fees for a Uniswap V3 position. + * Calls Uniswap's `decreaseLiquidity` and `collect` functions with specified parameters. + * @param _tokenId The ID of the UniV3 LP Token for which liquidity is being decreased. + * @param _liquidity The amount by which liquidity will be decreased. + * @param _amount0Min The minimum amount of token0 that should be accounted for the burned liquidity. + * @param _amount1Min The minimum amount of token1 that should be accounted for the burned liquidity. + */ + function _decreaseLiquidityPosition( + uint256 _tokenId, + uint128 _liquidity, + uint256 _amount0Min, + uint256 _amount1Min + ) internal { + // Decrease liquidity + INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: _tokenId, + liquidity: _liquidity, + amount0Min: _amount0Min, + amount1Min: _amount1Min, + deadline: block.timestamp + }); + nonfungiblePositionManager.decreaseLiquidity(decreaseParams); + + // Collect liquidity and fees + INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({ + tokenId: _tokenId, + recipient: address(this), + amount0Max: type(uint128).max, + amount1Max: type(uint128).max + }); + nonfungiblePositionManager.collect(params); + } + + /** + * @dev Internal function to return any remaining unwrapped collateral asset to the operator. + * @return underlyingOutputAmount The amount of unwrapped collateral asset returned to the operator. + */ + function _returnExcessUnderlying() internal returns (uint256 underlyingOutputAmount) { + underlyingOutputAmount = underlyingToken.balanceOf(address(this)); + if (underlyingOutputAmount > 0) { + underlyingToken.transfer(msg.sender, underlyingOutputAmount); + } + } +} diff --git a/contracts/interfaces/IFlashLoanSimpleReceiver.sol b/contracts/interfaces/IFlashLoanSimpleReceiver.sol new file mode 100644 index 00000000..03a67f50 --- /dev/null +++ b/contracts/interfaces/IFlashLoanSimpleReceiver.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.6.10; + +import {IPoolAddressesProvider} from "./IPoolAddressesProvider.sol"; +import {IPool} from "./IPool.sol"; + +/** + * @title IFlashLoanSimpleReceiver + * @author Aave + * @notice Defines the basic interface of a flashloan-receiver contract. + * @dev Implement this interface to develop a flashloan-compatible flashLoanReceiver contract + */ +interface IFlashLoanSimpleReceiver { + /** + * @notice Executes an operation after receiving the flash-borrowed asset + * @dev Ensure that the contract can return the debt + premium, e.g., has + * enough funds to repay and has approved the Pool to pull the total amount + * @param asset The address of the flash-borrowed asset + * @param amount The amount of the flash-borrowed asset + * @param premium The fee of the flash-borrowed asset + * @param initiator The address of the flashloan initiator + * @param params The byte-encoded params passed when initiating the flashloan + * @return True if the execution of the operation succeeds, false otherwise + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); + + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); + + function POOL() external view returns (IPool); +} diff --git a/contracts/interfaces/external/uniswap-v3/INonfungiblePositionManager.sol b/contracts/interfaces/external/uniswap-v3/INonfungiblePositionManager.sol new file mode 100644 index 00000000..e80c0d99 --- /dev/null +++ b/contracts/interfaces/external/uniswap-v3/INonfungiblePositionManager.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +/// @title Non-fungible token for positions +/// @notice Wraps Uniswap V3 positions in a non-fungible token interface which allows for them to be transferred +/// and authorized. +/// https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/INonfungiblePositionManager.sol +interface INonfungiblePositionManager { + /// @notice Returns the position information associated with a given token ID. + /// @dev Throws if the token ID is not valid. + /// @param tokenId The ID of the token that represents the position + /// @return nonce The nonce for permits + /// @return operator The address that is approved for spending + /// @return token0 The address of the token0 for a specific pool + /// @return token1 The address of the token1 for a specific pool + /// @return fee The fee associated with the pool + /// @return tickLower The lower end of the tick range for the position + /// @return tickUpper The higher end of the tick range for the position + /// @return liquidity The liquidity of the position + /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position + /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position + /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation + /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + /// @notice Creates a new position wrapped in a NFT + /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized + /// a method does not exist, i.e. the pool is assumed to be initialized. + /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata + /// @return tokenId The ID of the token that represents the minted position + /// @return liquidity The amount of liquidity for this position + /// @return amount0 The amount of token0 + /// @return amount1 The amount of token1 + function mint(MintParams calldata params) + external + payable + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + + struct IncreaseLiquidityParams { + uint256 tokenId; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` + /// @param params tokenId The ID of the token for which liquidity is being increased, + /// amount0Desired The desired amount of token0 to be spent, + /// amount1Desired The desired amount of token1 to be spent, + /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, + /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, + /// deadline The time by which the transaction must be included to effect the change + /// @return liquidity The new liquidity amount as a result of the increase + /// @return amount0 The amount of token0 to acheive resulting liquidity + /// @return amount1 The amount of token1 to acheive resulting liquidity + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); + + struct DecreaseLiquidityParams { + uint256 tokenId; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + /// @notice Decreases the amount of liquidity in a position and accounts it to the position + /// @param params tokenId The ID of the token for which liquidity is being decreased, + /// amount The amount by which liquidity will be decreased, + /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + /// deadline The time by which the transaction must be included to effect the change + /// @return amount0 The amount of token0 accounted to the position's tokens owed + /// @return amount1 The amount of token1 accounted to the position's tokens owed + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint256 amount0, uint256 amount1); + + struct CollectParams { + uint256 tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient + /// @param params tokenId The ID of the NFT for which tokens are being collected, + /// recipient The account that should receive the tokens, + /// amount0Max The maximum amount of token0 to collect, + /// amount1Max The maximum amount of token1 to collect + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); + + /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens + /// must be collected first. + /// @param tokenId The ID of the token that is being burned + function burn(uint256 tokenId) external payable; +} diff --git a/contracts/interfaces/external/uniswap-v3/IUniswapV3Pool.sol b/contracts/interfaces/external/uniswap-v3/IUniswapV3Pool.sol new file mode 100644 index 00000000..1fdcb5be --- /dev/null +++ b/contracts/interfaces/external/uniswap-v3/IUniswapV3Pool.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +/// @title Permissionless pool actions +/// @notice Contains pool methods that can be called by anyone +/// https://github.com/Uniswap/v3-core/blob/main/contracts/interfaces/pool/IUniswapV3PoolActions.sol +interface IUniswapV3Pool { + /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas + /// when accessed externally. + /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value + /// tick The current tick of the pool, i.e. according to the last tick transition that was run. + /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick + /// boundary. + /// observationIndex The index of the last oracle observation that was written, + /// observationCardinality The current maximum number of observations stored in the pool, + /// observationCardinalityNext The next maximum number of observations, to be updated when the observation. + /// feeProtocol The protocol fee for both tokens of the pool. + /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 + /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. + /// unlocked Whether the pool is currently locked to reentrancy + function slot0() + external + view + returns ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked + ); + + /// @notice Sets the initial price for the pool + /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value + /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 + function initialize(uint160 sqrtPriceX96) external; + + /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position + /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback + /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends + /// on tickLower, tickUpper, the amount of liquidity, and the current price. + /// @param recipient The address for which the liquidity will be created + /// @param tickLower The lower tick of the position in which to add liquidity + /// @param tickUpper The upper tick of the position in which to add liquidity + /// @param amount The amount of liquidity to mint + /// @param data Any data that should be passed through to the callback + /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback + /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback + function mint( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount, + bytes calldata data + ) external returns (uint256 amount0, uint256 amount1); + + /// @notice Collects tokens owed to a position + /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. + /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or + /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the + /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. + /// @param recipient The address which should receive the fees collected + /// @param tickLower The lower tick of the position for which to collect fees + /// @param tickUpper The upper tick of the position for which to collect fees + /// @param amount0Requested How much token0 should be withdrawn from the fees owed + /// @param amount1Requested How much token1 should be withdrawn from the fees owed + /// @return amount0 The amount of fees collected in token0 + /// @return amount1 The amount of fees collected in token1 + function collect( + address recipient, + int24 tickLower, + int24 tickUpper, + uint128 amount0Requested, + uint128 amount1Requested + ) external returns (uint128 amount0, uint128 amount1); + + /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position + /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 + /// @dev Fees must be collected separately via a call to #collect + /// @param tickLower The lower tick of the position for which to burn liquidity + /// @param tickUpper The upper tick of the position for which to burn liquidity + /// @param amount How much liquidity to burn + /// @return amount0 The amount of token0 sent to the recipient + /// @return amount1 The amount of token1 sent to the recipient + function burn( + int24 tickLower, + int24 tickUpper, + uint128 amount + ) external returns (uint256 amount0, uint256 amount1); + + /// @notice Swap token0 for token1, or token1 for token0 + /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback + /// @param recipient The address to receive the output of the swap + /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 + /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) + /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this + /// value after the swap. If one for zero, the price cannot be greater than this value after the swap + /// @param data Any data to be passed through to the callback + /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive + /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) external returns (int256 amount0, int256 amount1); + + /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback + /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback + /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling + /// with 0 amount{0,1} and sending the donation amount(s) from the callback + /// @param recipient The address which will receive the token0 and token1 amounts + /// @param amount0 The amount of token0 to send + /// @param amount1 The amount of token1 to send + /// @param data Any data to be passed through to the callback + function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes calldata data + ) external; + + /// @notice Increase the maximum number of price and liquidity observations that this pool will store + /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to + /// the input observationCardinalityNext. + /// @param observationCardinalityNext The desired minimum number of observations for the pool to store + function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; +} diff --git a/contracts/lib/FlashLoanSimpleReceiverBase.sol b/contracts/lib/FlashLoanSimpleReceiverBase.sol new file mode 100644 index 00000000..fc9ae807 --- /dev/null +++ b/contracts/lib/FlashLoanSimpleReceiverBase.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.6.10; + +import {IFlashLoanSimpleReceiver} from "../interfaces/IFlashLoanSimpleReceiver.sol"; +import {IPoolAddressesProvider} from "../interfaces/IPoolAddressesProvider.sol"; +import {IPool} from "../interfaces/IPool.sol"; + +/** + * @title FlashLoanSimpleReceiverBase + * @author Aave + * @notice Base contract to develop a flashloan-receiver contract. + */ +abstract contract FlashLoanSimpleReceiverBase is IFlashLoanSimpleReceiver { + IPoolAddressesProvider public immutable override ADDRESSES_PROVIDER; + IPool public immutable override POOL; + + constructor(IPoolAddressesProvider provider) public { + ADDRESSES_PROVIDER = provider; + POOL = IPool(provider.getPool()); + } +} diff --git a/test/integration/ethereum/aaveMigrationExtensionBtc2x.spec.ts b/test/integration/ethereum/aaveMigrationExtensionBtc2x.spec.ts new file mode 100644 index 00000000..59ccba0a --- /dev/null +++ b/test/integration/ethereum/aaveMigrationExtensionBtc2x.spec.ts @@ -0,0 +1,435 @@ +import "module-alias/register"; +import { BigNumber, Signer } from "ethers"; +import { JsonRpcSigner } from "@ethersproject/providers"; +import { Account } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { bitcoin, ether, getAccounts, getWaffleExpect, preciseDiv, preciseMul } from "@utils/index"; +import { ONE, ZERO } from "@utils/constants"; +import { + addSnapshotBeforeRestoreAfterEach, + increaseTimeAsync, + setBlockNumber, +} from "@utils/test/testingUtils"; +import { impersonateAccount } from "./utils"; +import { + AaveMigrationExtension, + BaseManagerV2__factory, + BaseManagerV2, + DebtIssuanceModuleV2, + DebtIssuanceModuleV2__factory, + FlexibleLeverageStrategyExtension, + FlexibleLeverageStrategyExtension__factory, + IERC20, + IERC20__factory, + SetToken, + SetToken__factory, + TradeModule__factory, + TradeModule, + UniswapV3ExchangeAdapter, + UniswapV3ExchangeAdapter__factory, + WrapExtension, +} from "../../../typechain"; + +const expect = getWaffleExpect(); + +const contractAddresses = { + addressProvider: "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + debtIssuanceModuleV2: "0x04b59F9F09750C044D7CfbC177561E409085f0f3", + flexibleLeverageStrategyExtension: "0xFD4eA597E8346a6723FA4A06a31E4b6F7F37e9Ad", + tradeModule: "0x90F765F63E7DC5aE97d6c576BF693FB6AF41C129", + uniswapV3ExchangeAdapter: "0xcC327D928925584AB00Fe83646719dEAE15E0424", + uniswapV3NonfungiblePositionManager: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", + uniswapV3Pool: "0xb1DD5eb0A64004E9Bbee68ca64AE0ccE8c7bB867", + wrapModule: "0xbe4aEdE1694AFF7F1827229870f6cf3d9e7a999c", +}; + +const tokenAddresses = { + aEthWbtc: "0x5Ee5bf7ae06D1Be5997A1A72006FE6C607eC6DE8", + cwbtc: "0xccF4429DB6322D5C611ee964527D42E5d685DD6a", + btc2x: "0xD2AC55cA3Bbd2Dd1e9936eC640dCb4b745fDe759", + btc2xfli: "0x0B498ff89709d3838a063f1dFA463091F9801c2b", + usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + wbtc: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", +}; + +const keeperAddresses = { + btc2xfliKeeper: "0xCBEb906f46eA0b9D7e7e75379fAFbceACd1aAeff", + btc2xDeployer: "0x37e6365d4f6aE378467b0e24c9065Ce5f06D70bF", +}; + +if (process.env.INTEGRATIONTEST) { + describe("AaveMigrationExtension - BTC2x-FLI Integration Test", async () => { + let owner: Account; + let operator: Signer; + let keeper: Signer; + let deployer: DeployHelper; + + let btc2xfli: SetToken; + let baseManager: BaseManagerV2; + let tradeModule: TradeModule; + + let btc2x: SetToken; + let debtIssuanceModuleV2: DebtIssuanceModuleV2; + + let wbtc: IERC20; + let cwbtc: IERC20; + let usdc: IERC20; + let aEthWbtc: IERC20; + + let migrationExtension: AaveMigrationExtension; + + setBlockNumber(19276457); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + // Setup collateral tokens + wbtc = IERC20__factory.connect(tokenAddresses.wbtc, owner.wallet); + cwbtc = IERC20__factory.connect(tokenAddresses.cwbtc, owner.wallet); + usdc = IERC20__factory.connect(tokenAddresses.usdc, owner.wallet); + aEthWbtc = IERC20__factory.connect(tokenAddresses.aEthWbtc, owner.wallet); + + // Setup BTC2x-FLI contracts + btc2xfli = SetToken__factory.connect(tokenAddresses.btc2xfli, owner.wallet); + baseManager = BaseManagerV2__factory.connect(await btc2xfli.manager(), owner.wallet); + operator = await impersonateAccount(await baseManager.operator()); + baseManager = baseManager.connect(operator); + tradeModule = TradeModule__factory.connect(contractAddresses.tradeModule, owner.wallet); + keeper = await impersonateAccount(keeperAddresses.btc2xfliKeeper); + + // Setup BTC2x contracts + btc2x = SetToken__factory.connect(tokenAddresses.btc2x, owner.wallet); + debtIssuanceModuleV2 = DebtIssuanceModuleV2__factory.connect( + contractAddresses.debtIssuanceModuleV2, + owner.wallet + ); + + // Deploy Migration Extension + migrationExtension = await deployer.extensions.deployAaveMigrationExtension( + baseManager.address, + wbtc.address, + aEthWbtc.address, + btc2x.address, + tradeModule.address, + debtIssuanceModuleV2.address, + contractAddresses.uniswapV3NonfungiblePositionManager, + contractAddresses.addressProvider, + ); + migrationExtension = migrationExtension.connect(operator); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when the product is de-levered", () => { + let flexibleLeverageStrategyExtension: FlexibleLeverageStrategyExtension; + + before(async () => { + flexibleLeverageStrategyExtension = FlexibleLeverageStrategyExtension__factory.connect( + contractAddresses.flexibleLeverageStrategyExtension, + operator, + ); + + // Adjust slippage tolerance for the test environment + const oldExecution = await flexibleLeverageStrategyExtension.getExecution(); + const newExecution = { + unutilizedLeveragePercentage: oldExecution.unutilizedLeveragePercentage, + slippageTolerance: ether(0.5), // Increased slippage tolerance + twapCooldownPeriod: oldExecution.twapCooldownPeriod, + }; + flexibleLeverageStrategyExtension.setExecutionSettings(newExecution); + + // Configure leverage strategy for 1x leverage + const oldMethodology = await flexibleLeverageStrategyExtension.getMethodology(); + const newMethodology = { + targetLeverageRatio: ether(1), + minLeverageRatio: ether(0.9), + maxLeverageRatio: ether(1), + recenteringSpeed: oldMethodology.recenteringSpeed, + rebalanceInterval: oldMethodology.rebalanceInterval, + }; + flexibleLeverageStrategyExtension.setMethodologySettings(newMethodology); + + // Verify initial leverage ratio is within expected range (1.75 to 2.25) + const startingLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(startingLeverage.gt(ether(1.75)) && startingLeverage.lt(ether(2.25))); + + // Perform first rebalance, should lower leverage ratio + await flexibleLeverageStrategyExtension + .connect(keeper) + .rebalance("UniswapV3ExchangeAdapter"); + const firstRebalanceLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(firstRebalanceLeverage.lt(startingLeverage)); + increaseTimeAsync(oldExecution.twapCooldownPeriod); + + // Iterate rebalance, should lower leverage ratio + await flexibleLeverageStrategyExtension + .connect(keeper) + .iterateRebalance("UniswapV3ExchangeAdapter"); + const secondRebalanceLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(secondRebalanceLeverage.lt(firstRebalanceLeverage)); + increaseTimeAsync(oldExecution.twapCooldownPeriod); + + // Disengage from the strategy, should have 1 leverage ratio + await flexibleLeverageStrategyExtension + .connect(operator) + .disengage("UniswapV3ExchangeAdapter"); + }); + + it("should have leverage ratio of 1", async () => { + const endingLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(endingLeverage.eq(ether(1))); + }); + + it("should have cWBTC and USDC as equity components", async () => { + const components = await btc2xfli.getComponents(); + expect(components).to.deep.equal([tokenAddresses.cwbtc, tokenAddresses.usdc]); + }); + + context("when the cWBTC is unwrapped", () => { + let wrapExtension: WrapExtension; + + before(async () => { + // Deploy Wrap Extension + wrapExtension = await deployer.extensions.deployWrapExtension( + baseManager.address, + contractAddresses.wrapModule, + ); + + // Add Wrap Module + await baseManager.addModule(contractAddresses.wrapModule); + + // Add Wrap Extension + await baseManager.addExtension(wrapExtension.address); + + // Initialize Wrap Extension + await wrapExtension.connect(operator).initialize(); + + // Unwrap cETH + await wrapExtension + .connect(operator) + .unwrap( + wbtc.address, + cwbtc.address, + await btc2xfli.getTotalComponentRealUnits(cwbtc.address), + "CompoundWrapAdapter", + ); + }); + + it("should have wBTC as a component instead of cWBTC", async () => { + const components = await btc2xfli.getComponents(); + await expect(components).to.deep.equal([tokenAddresses.wbtc, tokenAddresses.usdc]); + }); + + context("when migration extension is added as extension", () => { + before(async () => { + // Add Migration Extension + await baseManager.addExtension(migrationExtension.address); + }); + + it("should have the MigrationExtension added as an extension", async () => { + expect(await baseManager.isExtension(migrationExtension.address)).to.be.true; + }); + + context("when the trade module is added and initialized", () => { + before(async () => { + // Add Trade Module + await baseManager.addModule(tradeModule.address); + + // Initialize Trade Module via Migration Extension + await migrationExtension.initialize(); + }); + + it("should have the TradeModule added as a module", async () => { + expect(await btc2xfli.moduleStates(tradeModule.address)).to.equal(2); + }); + + context("when the USDC equity is traded away", () => { + let uniswapV3ExchangeAdapter: UniswapV3ExchangeAdapter; + + before(async () => { + uniswapV3ExchangeAdapter = UniswapV3ExchangeAdapter__factory.connect( + contractAddresses.uniswapV3ExchangeAdapter, + operator, + ); + + const exchangeName = "UniswapV3ExchangeAdapter"; + const usdcUnit = await btc2xfli.getDefaultPositionRealUnit(usdc.address); + const exchangeData = await uniswapV3ExchangeAdapter.generateDataParam( + [tokenAddresses.usdc, tokenAddresses.wbtc], + [BigNumber.from(500)], + ); + + // Trade USDC for WBTC via Migration Extension + await migrationExtension.trade( + exchangeName, + usdc.address, + usdcUnit, + wbtc.address, + 0, + exchangeData, + ); + }); + + it("should remove USDC as a component", async () => { + const components = await btc2xfli.getComponents(); + await expect(components).to.deep.equal([tokenAddresses.wbtc]); + }); + + context("when the Uniswap V3 liquidity position is minted", () => { + before(async () => { + const tickLower = 292660; + const tickUpper = 292670; + const fee = 500; + + const underlyingAmount = 0; + const wrappedSetTokenAmount = ether(0.01); + + const isUnderlyingToken0 = true; + + await btc2x + .connect(await impersonateAccount(keeperAddresses.btc2xDeployer)) + .transfer(migrationExtension.address, wrappedSetTokenAmount); + + // Mint liquidity position via Migration Extension + await migrationExtension.mintLiquidityPosition( + underlyingAmount, + wrappedSetTokenAmount, + ZERO, + ZERO, + tickLower, + tickUpper, + fee, + isUnderlyingToken0 + ); + }); + + it("should seed the liquidity position", async () => { + const tokenId = await migrationExtension.tokenIds(0); + expect(tokenId).to.be.gt(0); + }); + + context("when the migration is ready", () => { + let underlyingLoanAmount: BigNumber; + let supplyLiquidityAmount0Desired: BigNumber; + let supplyLiquidityAmount1Desired: BigNumber; + let supplyLiquidityAmount0Min: BigNumber; + let supplyLiquidityAmount1Min: BigNumber; + let tokenId: BigNumber; + let exchangeName: string; + let underlyingTradeUnits: BigNumber; + let wrappedSetTokenTradeUnits: BigNumber; + let exchangeData: string; + let maxSubsidy: BigNumber; + let redeemLiquidityAmount0Min: BigNumber; + let redeemLiquidityAmount1Min: BigNumber; + let isUnderlyingToken0: boolean; + + let wbtcWhale: JsonRpcSigner; + + before(async () => { + const setTokenTotalSupply = await btc2xfli.totalSupply(); + const wrappedPositionUnits = await btc2x.getDefaultPositionRealUnit( + aEthWbtc.address, + ); + const wrappedExchangeRate = preciseDiv(ether(1), wrappedPositionUnits); + + // BTC2x-FLI trade parameters + underlyingTradeUnits = await btc2xfli.getDefaultPositionRealUnit(wbtc.address); + wrappedSetTokenTradeUnits = preciseMul( + preciseMul(wrappedExchangeRate, ether(0.995)), + underlyingTradeUnits, + ); + exchangeName = "UniswapV3ExchangeAdapter"; + exchangeData = await uniswapV3ExchangeAdapter.generateDataParam( + [tokenAddresses.wbtc, tokenAddresses.btc2x], + [BigNumber.from(500)], + ); + + // Flash loan parameters + underlyingLoanAmount = preciseMul(underlyingTradeUnits, setTokenTotalSupply); + + // Uniswap V3 liquidity parameters + supplyLiquidityAmount1Desired = preciseMul( + preciseDiv(ether(1), wrappedPositionUnits), + underlyingLoanAmount, + ); + supplyLiquidityAmount1Min = ZERO; + supplyLiquidityAmount0Min = ZERO; + supplyLiquidityAmount0Desired = ZERO; + tokenId = await migrationExtension.tokenIds(0); + redeemLiquidityAmount0Min = ZERO; + redeemLiquidityAmount1Min = ZERO; + isUnderlyingToken0 = true; + + // Subsidize 0.01 WBTC to the migration extension + maxSubsidy = bitcoin(0.01); + wbtcWhale = await impersonateAccount( + "0xe74b28c2eAe8679e3cCc3a94d5d0dE83CCB84705", + ); + await wbtc.connect(wbtcWhale).transfer(await operator.getAddress(), maxSubsidy); + await wbtc.connect(operator).approve(migrationExtension.address, maxSubsidy); + }); + + it("should be able to migrate atomically", async () => { + const operatorAddress = await operator.getAddress(); + const operatorWbtcBalanceBefore = await wbtc.balanceOf(operatorAddress); + + // Verify starting components and units + const startingComponents = await btc2xfli.getComponents(); + const startingUnit = await btc2xfli.getDefaultPositionRealUnit( + tokenAddresses.wbtc, + ); + expect(startingComponents).to.deep.equal([tokenAddresses.wbtc]); + expect(startingUnit).to.eq(underlyingTradeUnits); + + // Get the expected subsidy + const decodedParams = { + supplyLiquidityAmount0Desired, + supplyLiquidityAmount1Desired, + supplyLiquidityAmount0Min, + supplyLiquidityAmount1Min, + tokenId, + exchangeName, + underlyingTradeUnits, + wrappedSetTokenTradeUnits, + exchangeData, + redeemLiquidityAmount0Min, + redeemLiquidityAmount1Min, + isUnderlyingToken0, + }; + const expectedOutput = await migrationExtension.callStatic.migrate( + decodedParams, + underlyingLoanAmount, + maxSubsidy + ); + expect(expectedOutput).to.be.gt(0); + + // Migrate atomically via Migration Extension + await migrationExtension.migrate( + decodedParams, + underlyingLoanAmount, + maxSubsidy + ); + + // Verify operator WBTC balance change + const operatorWbtcBalanceAfter = await wbtc.balanceOf(operatorAddress); + expect(operatorWbtcBalanceBefore.sub(operatorWbtcBalanceAfter)).to.be.gte(maxSubsidy.sub(expectedOutput).sub(ONE)); + + // Verify ending components and units + const endingComponents = await btc2xfli.getComponents(); + const endingUnit = await btc2xfli.getDefaultPositionRealUnit( + tokenAddresses.btc2x, + ); + expect(endingComponents).to.deep.equal([tokenAddresses.btc2x]); + expect(endingUnit).to.be.gt(wrappedSetTokenTradeUnits); + }); + }); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/test/integration/ethereum/aaveMigrationExtensionEth2x.spec.ts b/test/integration/ethereum/aaveMigrationExtensionEth2x.spec.ts new file mode 100644 index 00000000..f2a57e88 --- /dev/null +++ b/test/integration/ethereum/aaveMigrationExtensionEth2x.spec.ts @@ -0,0 +1,447 @@ +import "module-alias/register"; +import { ethers } from "hardhat"; +import { BigNumber, Signer } from "ethers"; +import { JsonRpcSigner } from "@ethersproject/providers"; +import { Account } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { ether, getAccounts, getWaffleExpect, preciseDiv, preciseMul } from "@utils/index"; +import { ONE, ZERO } from "@utils/constants"; +import { + addSnapshotBeforeRestoreAfterEach, + increaseTimeAsync, + setBlockNumber, +} from "@utils/test/testingUtils"; +import { impersonateAccount } from "./utils"; +import { + AaveMigrationExtension, + BaseManagerV2__factory, + BaseManagerV2, + DebtIssuanceModuleV2, + DebtIssuanceModuleV2__factory, + FlexibleLeverageStrategyExtension, + FlexibleLeverageStrategyExtension__factory, + IERC20, + IERC20__factory, + IWETH, + SetToken, + SetToken__factory, + TradeModule__factory, + TradeModule, + UniswapV3ExchangeAdapter, + UniswapV3ExchangeAdapter__factory, + WrapExtension, +} from "../../../typechain"; + +const expect = getWaffleExpect(); + +const contractAddresses = { + addressProvider: "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + debtIssuanceModuleV2: "0x04b59F9F09750C044D7CfbC177561E409085f0f3", + flexibleLeverageStrategyExtension: "0x9bA41A2C5175d502eA52Ff9A666f8a4fc00C00A1", + tradeModule: "0x90F765F63E7DC5aE97d6c576BF693FB6AF41C129", + uniswapV3ExchangeAdapter: "0xcC327D928925584AB00Fe83646719dEAE15E0424", + uniswapV3NonfungiblePositionManager: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", + uniswapV3Pool: "0xF44D4d68C2Ea473C93c1F3d2C81E900535d73843", + wrapModule: "0xbe4aEdE1694AFF7F1827229870f6cf3d9e7a999c", +}; + +const tokenAddresses = { + aEthWeth: "0x4d5F47FA6A74757f35C14fD3a6Ef8E3C9BC514E8", + ceth: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + eth2x: "0x65c4C0517025Ec0843C9146aF266A2C5a2D148A2", + eth2xfli: "0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD", + usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", +}; + +const keeperAddresses = { + eth2xfliKeeper: "0xEa80829C827f1633A46E7EA6026Ed693cA54eebD", + eth2xDeployer: "0x37e6365d4f6aE378467b0e24c9065Ce5f06D70bF", +}; + +if (process.env.INTEGRATIONTEST) { + describe("AaveMigrationExtension - ETH2x-FLI Integration Test", async () => { + let owner: Account; + let operator: Signer; + let keeper: Signer; + let deployer: DeployHelper; + + let eth2xfli: SetToken; + let baseManager: BaseManagerV2; + let tradeModule: TradeModule; + + let eth2x: SetToken; + let debtIssuanceModuleV2: DebtIssuanceModuleV2; + + let weth: IWETH; + let ceth: IERC20; + let usdc: IERC20; + let aEthWeth: IERC20; + + let migrationExtension: AaveMigrationExtension; + + setBlockNumber(19271340); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + // Setup collateral tokens + weth = (await ethers.getContractAt("IWETH", tokenAddresses.weth)) as IWETH; + ceth = IERC20__factory.connect(tokenAddresses.ceth, owner.wallet); + usdc = IERC20__factory.connect(tokenAddresses.usdc, owner.wallet); + aEthWeth = IERC20__factory.connect(tokenAddresses.aEthWeth, owner.wallet); + + // Setup ETH2x-FLI contracts + eth2xfli = SetToken__factory.connect(tokenAddresses.eth2xfli, owner.wallet); + baseManager = BaseManagerV2__factory.connect(await eth2xfli.manager(), owner.wallet); + operator = await impersonateAccount(await baseManager.operator()); + baseManager = baseManager.connect(operator); + tradeModule = TradeModule__factory.connect(contractAddresses.tradeModule, owner.wallet); + keeper = await impersonateAccount(keeperAddresses.eth2xfliKeeper); + + // Setup ETH2x contracts + eth2x = SetToken__factory.connect(tokenAddresses.eth2x, owner.wallet); + debtIssuanceModuleV2 = DebtIssuanceModuleV2__factory.connect( + contractAddresses.debtIssuanceModuleV2, + owner.wallet, + ); + + // Deploy Migration Extension + migrationExtension = await deployer.extensions.deployAaveMigrationExtension( + baseManager.address, + weth.address, + aEthWeth.address, + eth2x.address, + tradeModule.address, + debtIssuanceModuleV2.address, + contractAddresses.uniswapV3NonfungiblePositionManager, + contractAddresses.addressProvider, + ); + migrationExtension = migrationExtension.connect(operator); + }); + + addSnapshotBeforeRestoreAfterEach(); + + context("when the product is de-levered", () => { + let flexibleLeverageStrategyExtension: FlexibleLeverageStrategyExtension; + + before(async () => { + flexibleLeverageStrategyExtension = FlexibleLeverageStrategyExtension__factory.connect( + contractAddresses.flexibleLeverageStrategyExtension, + operator, + ); + + // Adjust slippage tolerance for the test environment + const oldExecution = await flexibleLeverageStrategyExtension.getExecution(); + const newExecution = { + unutilizedLeveragePercentage: oldExecution.unutilizedLeveragePercentage, + slippageTolerance: ether(0.15), // Increased slippage tolerance + twapCooldownPeriod: oldExecution.twapCooldownPeriod, + }; + flexibleLeverageStrategyExtension.setExecutionSettings(newExecution); + + // Configure leverage strategy for 1x leverage + const oldMethodology = await flexibleLeverageStrategyExtension.getMethodology(); + const newMethodology = { + targetLeverageRatio: ether(1), + minLeverageRatio: ether(0.9), + maxLeverageRatio: ether(1), + recenteringSpeed: oldMethodology.recenteringSpeed, + rebalanceInterval: oldMethodology.rebalanceInterval, + }; + flexibleLeverageStrategyExtension.setMethodologySettings(newMethodology); + + // Verify initial leverage ratio is within expected range (1.75 to 2.25) + const startingLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(startingLeverage.gt(ether(1.75)) && startingLeverage.lt(ether(2.25))); + + // Perform first rebalance, should lower leverage ratio + await flexibleLeverageStrategyExtension + .connect(keeper) + .rebalance("UniswapV3ExchangeAdapter"); + const firstRebalanceLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(firstRebalanceLeverage.lt(startingLeverage)); + increaseTimeAsync(oldExecution.twapCooldownPeriod); + + // Iterate rebalance, should lower leverage ratio + await flexibleLeverageStrategyExtension + .connect(keeper) + .iterateRebalance("UniswapV3ExchangeAdapter"); + const secondRebalanceLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(secondRebalanceLeverage.lt(firstRebalanceLeverage)); + increaseTimeAsync(oldExecution.twapCooldownPeriod); + + // Iterate rebalance, should lower leverage ratio + await flexibleLeverageStrategyExtension + .connect(keeper) + .iterateRebalance("UniswapV3ExchangeAdapter"); + const thirdRebalanceLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(thirdRebalanceLeverage.lt(secondRebalanceLeverage)); + increaseTimeAsync(oldExecution.twapCooldownPeriod); + + // Disengage from the strategy, should have 1 leverage ratio + await flexibleLeverageStrategyExtension + .connect(operator) + .disengage("UniswapV3ExchangeAdapter"); + }); + + it("should have leverage ratio of 1", async () => { + const endingLeverage = await flexibleLeverageStrategyExtension.getCurrentLeverageRatio(); + expect(endingLeverage.eq(ether(1))); + }); + + it("should have cETH and USDC as equity components", async () => { + const components = await eth2xfli.getComponents(); + expect(components).to.deep.equal([tokenAddresses.ceth, tokenAddresses.usdc]); + }); + + context("when the cETH is unwrapped", () => { + let wrapExtension: WrapExtension; + + before(async () => { + // Deploy Wrap Extension + wrapExtension = await deployer.extensions.deployWrapExtension( + baseManager.address, + contractAddresses.wrapModule, + ); + + // Add Wrap Module + await baseManager.addModule(contractAddresses.wrapModule); + + // Add Wrap Extension + await baseManager.addExtension(wrapExtension.address); + + // Initialize Wrap Extension + await wrapExtension.connect(operator).initialize(); + + // Unwrap cETH + await wrapExtension + .connect(operator) + .unwrapWithEther( + ceth.address, + await eth2xfli.getTotalComponentRealUnits(ceth.address), + "CompoundWrapAdapter", + ); + }); + + it("should have WETH as a component instead of cETH", async () => { + const components = await eth2xfli.getComponents(); + await expect(components).to.deep.equal([tokenAddresses.weth, tokenAddresses.usdc]); + }); + + context("when migration extension is added as extension", () => { + before(async () => { + // Add Migration Extension + await baseManager.addExtension(migrationExtension.address); + }); + + it("should have the MigrationExtension added as an extension", async () => { + expect(await baseManager.isExtension(migrationExtension.address)).to.be.true; + }); + + context("when the trade module is added and initialized", () => { + before(async () => { + // Add Trade Module + await baseManager.addModule(tradeModule.address); + + // Initialize Trade Module via Migration Extension + await migrationExtension.initialize(); + }); + + it("should have the TradeModule added as a module", async () => { + expect(await eth2xfli.moduleStates(tradeModule.address)).to.equal(2); + }); + + context("when the USDC equity is traded away", () => { + let uniswapV3ExchangeAdapter: UniswapV3ExchangeAdapter; + + before(async () => { + uniswapV3ExchangeAdapter = UniswapV3ExchangeAdapter__factory.connect( + contractAddresses.uniswapV3ExchangeAdapter, + operator, + ); + + const exchangeName = "UniswapV3ExchangeAdapter"; + const usdcUnit = await eth2xfli.getDefaultPositionRealUnit(usdc.address); + const exchangeData = await uniswapV3ExchangeAdapter.generateDataParam( + [tokenAddresses.usdc, tokenAddresses.weth], + [BigNumber.from(500)], + ); + + // Trade USDC for WETH via Migration Extension + await migrationExtension.trade( + exchangeName, + usdc.address, + usdcUnit, + weth.address, + 0, + exchangeData, + ); + }); + + it("should remove USDC as a component", async () => { + const components = await eth2xfli.getComponents(); + await expect(components).to.deep.equal([tokenAddresses.weth]); + }); + + context("when the Uniswap V3 liquidity position is minted", () => { + before(async () => { + const tickLower = -34114; + const tickUpper = -34113; + const fee = 100; + + const underlyingAmount = 0; + const wrappedSetTokenAmount = ether(0.01); + + const isUnderlyingToken0 = false; + + await eth2x + .connect(await impersonateAccount(keeperAddresses.eth2xDeployer)) + .transfer(migrationExtension.address, wrappedSetTokenAmount); + + // Mint liquidity position via Migration Extension + await migrationExtension.mintLiquidityPosition( + wrappedSetTokenAmount, + underlyingAmount, + ZERO, + ZERO, + tickLower, + tickUpper, + fee, + isUnderlyingToken0 + ); + }); + + it("should seed the liquidity position", async () => { + const tokenId = await migrationExtension.tokenIds(0); + expect(tokenId).to.be.gt(0); + }); + + context("when the migration is ready", () => { + let underlyingLoanAmount: BigNumber; + let supplyLiquidityAmount0Desired: BigNumber; + let supplyLiquidityAmount1Desired: BigNumber; + let supplyLiquidityAmount0Min: BigNumber; + let supplyLiquidityAmount1Min: BigNumber; + let tokenId: BigNumber; + let exchangeName: string; + let underlyingTradeUnits: BigNumber; + let wrappedSetTokenTradeUnits: BigNumber; + let exchangeData: string; + let maxSubsidy: BigNumber; + let redeemLiquidityAmount0Min: BigNumber; + let redeemLiquidityAmount1Min: BigNumber; + let isUnderlyingToken0: boolean; + + let wethWhale: JsonRpcSigner; + + before(async () => { + const setTokenTotalSupply = await eth2xfli.totalSupply(); + const wrappedPositionUnits = await eth2x.getDefaultPositionRealUnit( + aEthWeth.address, + ); + const wrappedExchangeRate = preciseDiv(ether(1), wrappedPositionUnits); + maxSubsidy = ether(3.205); + + // ETH2x-FLI trade parameters + underlyingTradeUnits = await eth2xfli.getDefaultPositionRealUnit(weth.address); + wrappedSetTokenTradeUnits = preciseMul( + preciseMul(wrappedExchangeRate, ether(0.999)), + underlyingTradeUnits, + ); + exchangeName = "UniswapV3ExchangeAdapter"; + exchangeData = await uniswapV3ExchangeAdapter.generateDataParam( + [tokenAddresses.weth, tokenAddresses.eth2x], + [BigNumber.from(100)], + ); + + // Flash loan parameters + underlyingLoanAmount = preciseMul(underlyingTradeUnits, setTokenTotalSupply); + + // Uniswap V3 liquidity parameters + supplyLiquidityAmount1Desired = ZERO; + supplyLiquidityAmount1Min = ZERO; + supplyLiquidityAmount0Desired = preciseMul( + preciseDiv(ether(1), wrappedPositionUnits), + underlyingLoanAmount, + ); + supplyLiquidityAmount0Min = preciseMul( + supplyLiquidityAmount0Desired, + ether(0.99), + ); + tokenId = await migrationExtension.tokenIds(0); + redeemLiquidityAmount0Min = ZERO; + redeemLiquidityAmount1Min = ZERO; + isUnderlyingToken0 = false; + + // Subsidize 3.205 WETH to the migration extension + wethWhale = await impersonateAccount( + "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6", + ); + await weth.connect(wethWhale).transfer(await operator.getAddress(), maxSubsidy); + await weth.connect(operator).approve(migrationExtension.address, maxSubsidy); + }); + + it("should be able to migrate atomically", async () => { + const operatorAddress = await operator.getAddress(); + const operatorWethBalanceBefore = await weth.balanceOf(operatorAddress); + + // Verify starting components and units + const startingComponents = await eth2xfli.getComponents(); + const startingUnit = await eth2xfli.getDefaultPositionRealUnit( + tokenAddresses.weth, + ); + expect(startingComponents).to.deep.equal([tokenAddresses.weth]); + expect(startingUnit).to.eq(underlyingTradeUnits); + + // Get the expected subsidy + const decodedParams = { + supplyLiquidityAmount0Desired, + supplyLiquidityAmount1Desired, + supplyLiquidityAmount0Min, + supplyLiquidityAmount1Min, + tokenId, + exchangeName, + underlyingTradeUnits, + wrappedSetTokenTradeUnits, + exchangeData, + redeemLiquidityAmount0Min, + redeemLiquidityAmount1Min, + isUnderlyingToken0, + }; + const expectedOutput = await migrationExtension.callStatic.migrate( + decodedParams, + underlyingLoanAmount, + maxSubsidy + ); + expect(expectedOutput).to.lt(maxSubsidy); + + // Migrate atomically via Migration Extension + await migrationExtension.migrate( + decodedParams, + underlyingLoanAmount, + maxSubsidy + ); + + // Verify operator WETH balance change + const operatorWethBalanceAfter = await weth.balanceOf(operatorAddress); + expect(operatorWethBalanceBefore.sub(operatorWethBalanceAfter)).to.be.gte(maxSubsidy.sub(expectedOutput).sub(ONE)); + + // Verify ending components and units + const endingComponents = await eth2xfli.getComponents(); + const endingUnit = await eth2xfli.getDefaultPositionRealUnit( + tokenAddresses.eth2x, + ); + expect(endingComponents).to.deep.equal([tokenAddresses.eth2x]); + expect(endingUnit).to.be.gt(wrappedSetTokenTradeUnits); + }); + }); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 15a791cc..c1e47aaf 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -1,4 +1,5 @@ export { AaveLeverageStrategyExtension } from "../../typechain/AaveLeverageStrategyExtension"; +export { AaveMigrationExtension } from "../../typechain/AaveMigrationExtension"; export { AirdropExtension } from "../../typechain/AirdropExtension"; export { AuctionRebalanceExtension } from "../../typechain/AuctionRebalanceExtension"; export { AirdropIssuanceHook } from "../../typechain/AirdropIssuanceHook"; diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index c64d1c99..9e2c0120 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -24,6 +24,7 @@ import { FeeSplitExtension, GIMExtension, GovernanceExtension, + AaveMigrationExtension, OptimisticAuctionRebalanceExtensionV1, StreamingFeeSplitExtension, WrapExtension, @@ -53,6 +54,7 @@ import { FlexibleLeverageStrategyExtension__factory } from "../../typechain/fact import { GIMExtension__factory } from "../../typechain/factories/GIMExtension__factory"; import { GovernanceExtension__factory } from "../../typechain/factories/GovernanceExtension__factory"; import { FixedRebalanceExtension__factory } from "../../typechain/factories/FixedRebalanceExtension__factory"; +import { AaveMigrationExtension__factory } from "../../typechain/factories/AaveMigrationExtension__factory"; import { OptimisticAuctionRebalanceExtensionV1__factory } from "../../typechain/factories/OptimisticAuctionRebalanceExtensionV1__factory"; import { StakeWiseReinvestmentExtension__factory } from "../../typechain/factories/StakeWiseReinvestmentExtension__factory"; import { StreamingFeeSplitExtension__factory } from "../../typechain/factories/StreamingFeeSplitExtension__factory"; @@ -453,6 +455,28 @@ export default class DeployExtensions { return await new WrapExtension__factory(this._deployerSigner).deploy(manager, wrapModule); } + public async deployAaveMigrationExtension( + manager: Address, + underlyingToken: Address, + aaveToken: Address, + wrappedSetToken: Address, + tradeModule: Address, + issuanceModule: Address, + nonfungiblePositionManager: Address, + addressProvider: Address + ): Promise { + return await new AaveMigrationExtension__factory(this._deployerSigner).deploy( + manager, + underlyingToken, + aaveToken, + wrappedSetToken, + tradeModule, + issuanceModule, + nonfungiblePositionManager, + addressProvider + ); + } + public async deployFlashMintWrappedExtension( wethAddress: Address, quickRouterAddress: Address, diff --git a/utils/test/testingUtils.ts b/utils/test/testingUtils.ts index 23d26117..4f7ed637 100644 --- a/utils/test/testingUtils.ts +++ b/utils/test/testingUtils.ts @@ -115,7 +115,7 @@ export async function impersonateAccount(address: string): Promise { return await ethers.provider.getSigner(address); } -export function setBlockNumber(blockNumber: number) { +export function setBlockNumber(blockNumber: number, reset: boolean = true) { before(async () => { await network.provider.request({ method: "hardhat_reset", @@ -130,17 +130,19 @@ export function setBlockNumber(blockNumber: number) { }); }); after(async () => { - await network.provider.request({ - method: "hardhat_reset", - params: [ - { - forking: { - jsonRpcUrl: forkingConfig.url, - blockNumber: forkingConfig.blockNumber, + if (reset) { + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: forkingConfig.url, + blockNumber: forkingConfig.blockNumber, + }, }, - }, - ], - }); + ], + }); + } }); }