diff --git a/contracts/exchangeIssuance/FlashMintLeveraged.sol b/contracts/exchangeIssuance/FlashMintLeveraged.sol new file mode 100644 index 00000000..350cb224 --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintLeveraged.sol @@ -0,0 +1,1242 @@ +/* + Copyright 2022 Index Cooperative + + 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 { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import { IAToken } from "../interfaces/IAToken.sol"; +import { IAaveLeverageModule } from "../interfaces/IAaveLeverageModule.sol"; +import { IDebtIssuanceModule } from "../interfaces/IDebtIssuanceModule.sol"; +import { IController } from "../interfaces/IController.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; +import { UniSushiV2Library } from "../../external/contracts/UniSushiV2Library.sol"; +import { FlashLoanReceiverBaseV2 } from "../../external/contracts/aaveV2/FlashLoanReceiverBaseV2.sol"; +import { DEXAdapter } from "./DEXAdapter.sol"; +import {IVault, IFlashLoanRecipient} from "../interfaces/external/balancer-v2/IVault.sol"; +import {IPool} from "../interfaces/IPool.sol"; + +/** + * @title FlashMintLeveraged + * @author Index Coop + * + * Contract for issuing and redeeming a leveraged Set Token + * Supports all tokens with one collateral Position in the form of an AToken and one debt position + * Both the collateral as well as the debt token have to be available for flashloan from balancer and be + * tradeable against each other on Sushi / Quickswap + */ +contract FlashMintLeveraged is ReentrancyGuard, IFlashLoanRecipient{ + + using DEXAdapter for DEXAdapter.Addresses; + using Address for address payable; + using SafeMath for uint256; + using PreciseUnitMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for ISetToken; + + /* ============ Structs ============ */ + + struct LeveragedTokenData { + address collateralAToken; + address collateralToken; + uint256 collateralAmount; + address debtToken; + uint256 debtAmount; + } + + + + struct DecodedParams { + ISetToken setToken; + uint256 setAmount; + address originalSender; + bool isIssuance; + address paymentToken; + uint256 limitAmount; + LeveragedTokenData leveragedTokenData; + DEXAdapter.SwapData collateralAndDebtSwapData; + DEXAdapter.SwapData paymentTokenSwapData; + } + + /* ============ Constants ============= */ + + uint256 constant private MAX_UINT256 = type(uint256).max; + uint256 public constant ROUNDING_ERROR_MARGIN = 2; + + /* ============ State Variables ============ */ + + IController public immutable setController; + IDebtIssuanceModule public immutable debtIssuanceModule; + IAaveLeverageModule public immutable aaveLeverageModule; + DEXAdapter.Addresses public addresses; + IVault public immutable balancerV2Vault; + IPool public immutable LENDING_POOL; + address private flashLoanBenefactor; + + /* ============ Events ============ */ + + event FlashMint( + address indexed _recipient, // The recipient address of the issued SetTokens + ISetToken indexed _setToken, // The issued SetToken + address indexed _inputToken, // The address of the input asset(ERC20/ETH) used to issue the SetTokens + uint256 _amountInputToken, // The amount of input tokens used for issuance + uint256 _amountSetIssued // The amount of SetTokens received by the recipient + ); + + event FlashRedeem( + address indexed _recipient, // The recipient address which redeemed the SetTokens + ISetToken indexed _setToken, // The redeemed SetToken + address indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient + uint256 _amountSetRedeemed, // The amount of SetTokens redeemed for output tokens + uint256 _amountOutputToken // The amount of output tokens received by the recipient + ); + + /* ============ Modifiers ============ */ + + modifier onlyBalancerV2Vault() { + require(msg.sender == address(balancerV2Vault), "ExchangeIssuance: BalancerV2 Vault ONLY"); + _; + } + + modifier isValidPath( + address[] memory _path, + address _inputToken, + address _outputToken + ) + { + if(_inputToken != _outputToken){ + require( + _path[0] == _inputToken || (_inputToken == addresses.weth && _path[0] == DEXAdapter.ETH_ADDRESS), + "ExchangeIssuance: INPUT_TOKEN_NOT_IN_PATH" + ); + require( + _path[_path.length-1] == _outputToken || + (_outputToken == addresses.weth && _path[_path.length-1] == DEXAdapter.ETH_ADDRESS), + "ExchangeIssuance: OUTPUT_TOKEN_NOT_IN_PATH" + ); + } + _; + } + + + /* ============ Constructor ============ */ + + /** + * Sets various contract addresses + * + * @param _addresses dex adapter addreses + * @param _setController SetToken controller used to verify a given token is a set + * @param _debtIssuanceModule DebtIssuanceModule used to issue and redeem tokens + * @param _aaveLeverageModule AaveLeverageModule to sync before every issuance / redemption + * @param _aaveV3Pool Address of address provider for aaves addresses + * @param _vault Balancer Vault to flashloan from + */ + constructor( + DEXAdapter.Addresses memory _addresses, + IController _setController, + IDebtIssuanceModule _debtIssuanceModule, + IAaveLeverageModule _aaveLeverageModule, + address _aaveV3Pool, + address _vault + ) + public + { + setController = _setController; + debtIssuanceModule = _debtIssuanceModule; + aaveLeverageModule = _aaveLeverageModule; + addresses = _addresses; + LENDING_POOL = IPool(_aaveV3Pool); + balancerV2Vault = IVault(_vault); + } + + /* ============ External Functions ============ */ + + /** + * Returns the collateral / debt token addresses and amounts for a leveraged index + * + * @param _setToken Address of the SetToken to be issued / redeemed + * @param _setAmount Amount of SetTokens to issue / redeem + * @param _isIssuance Boolean indicating if the SetToken is to be issued or redeemed + * + * @return Struct containing the collateral / debt token addresses and amounts + */ + function getLeveragedTokenData( + ISetToken _setToken, + uint256 _setAmount, + bool _isIssuance + ) + external + view + returns (LeveragedTokenData memory) + { + return _getLeveragedTokenData(_setToken, _setAmount, _isIssuance); + } + + /** + * Runs all the necessary approval functions required for a given ERC20 token. + * This function can be called when a new token is added to a SetToken during a + * rebalance. + * + * @param _token Address of the token which needs approval + */ + function approveToken(IERC20 _token) external { + _approveToken(_token); + } + + /** + * Gets the input cost of issuing a given amount of a set token. This + * function is not marked view, but should be static called from frontends. + * This constraint is due to the need to interact with the Uniswap V3 quoter + * contract and call sync on AaveLeverageModule. Note: If the two SwapData + * paths contain the same tokens, there will be a slight error introduced + * in the result. + * + * @param _setToken the set token to issue + * @param _setAmount amount of set tokens + * @param _swapDataDebtForCollateral swap data for the debt to collateral swap + * @param _swapDataInputToken swap data for the input token to collateral swap + * + * @return the amount of input tokens required to perfrom the issuance + */ + function getIssueExactSet( + ISetToken _setToken, + uint256 _setAmount, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + external + returns (uint256) + { + aaveLeverageModule.sync(_setToken); + LeveragedTokenData memory issueInfo = _getLeveragedTokenData(_setToken, _setAmount, true); + uint256 collateralOwed = issueInfo.collateralAmount.preciseMul(1.0009 ether); + uint256 borrowSaleProceeds = DEXAdapter.getAmountOut(addresses, _swapDataDebtForCollateral, issueInfo.debtAmount); + collateralOwed = collateralOwed.sub(borrowSaleProceeds); + return DEXAdapter.getAmountIn(addresses, _swapDataInputToken, collateralOwed); + } + + /** + * Gets the proceeds of a redemption of a given amount of a set token. This + * function is not marked view, but should be static called from frontends. + * This constraint is due to the need to interact with the Uniswap V3 quoter + * contract and call sync on AaveLeverageModule. Note: If the two SwapData + * paths contain the same tokens, there will be a slight error introduced + * in the result. + * + * @param _setToken the set token to issue + * @param _setAmount amount of set tokens + * @param _swapDataCollateralForDebt swap data for the collateral to debt swap + * @param _swapDataOutputToken swap data for the collateral token to the output token + * + * @return amount of _outputToken that would be obtained from the redemption + */ + function getRedeemExactSet( + ISetToken _setToken, + uint256 _setAmount, + DEXAdapter.SwapData memory _swapDataCollateralForDebt, + DEXAdapter.SwapData memory _swapDataOutputToken + ) + external + returns (uint256) + { + aaveLeverageModule.sync(_setToken); + LeveragedTokenData memory redeemInfo = _getLeveragedTokenData(_setToken, _setAmount, false); + uint256 debtOwed = redeemInfo.debtAmount.preciseMul(1.0009 ether); + uint256 debtPurchaseCost = DEXAdapter.getAmountIn(addresses, _swapDataCollateralForDebt, debtOwed); + uint256 extraCollateral = redeemInfo.collateralAmount.sub(debtPurchaseCost); + return DEXAdapter.getAmountOut(addresses, _swapDataOutputToken, extraCollateral); + } + + /** + * Trigger redemption of set token to pay the user with Eth + * + * @param _setToken Set token to redeem + * @param _setAmount Amount to redeem + * @param _minAmountOutputToken Minimum amount of ETH to send to the user + * @param _swapDataCollateralForDebt Data (token path and fee levels) describing the swap from Collateral Token to Debt Token + * @param _swapDataOutputToken Data (token path and fee levels) describing the swap from Collateral Token to Eth + */ + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _setAmount, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapDataCollateralForDebt, + DEXAdapter.SwapData memory _swapDataOutputToken + ) + external + nonReentrant + { + _initiateRedemption( + _setToken, + _setAmount, + DEXAdapter.ETH_ADDRESS, + _minAmountOutputToken, + _swapDataCollateralForDebt, + _swapDataOutputToken + ); + } + + /** + * Trigger redemption of set token to pay the user with an arbitrary ERC20 + * + * @param _setToken Set token to redeem + * @param _setAmount Amount to redeem + * @param _outputToken Address of the ERC20 token to send to the user + * @param _minAmountOutputToken Minimum amount of output token to send to the user + * @param _swapDataCollateralForDebt Data (token path and fee levels) describing the swap from Collateral Token to Debt Token + * @param _swapDataOutputToken Data (token path and fee levels) describing the swap from Collateral Token to Output token + */ + function redeemExactSetForERC20( + ISetToken _setToken, + uint256 _setAmount, + address _outputToken, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapDataCollateralForDebt, + DEXAdapter.SwapData memory _swapDataOutputToken + ) + external + nonReentrant + { + _initiateRedemption( + _setToken, + _setAmount, + _outputToken, + _minAmountOutputToken, + _swapDataCollateralForDebt, + _swapDataOutputToken + ); + } + + /** + * Trigger issuance of set token paying with any arbitrary ERC20 token + * + * @param _setToken Set token to issue + * @param _setAmount Amount to issue + * @param _inputToken Input token to pay with + * @param _maxAmountInputToken Maximum amount of input token to spend + * @param _swapDataDebtForCollateral Data (token addresses and fee levels) to describe the swap path from Debt to collateral token + * @param _swapDataInputToken Data (token addresses and fee levels) to describe the swap path from input to collateral token + */ + function issueExactSetFromERC20( + ISetToken _setToken, + uint256 _setAmount, + address _inputToken, + uint256 _maxAmountInputToken, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + external + nonReentrant + { + _initiateIssuance( + _setToken, + _setAmount, + _inputToken, + _maxAmountInputToken, + _swapDataDebtForCollateral, + _swapDataInputToken + ); + } + + /** + * Trigger issuance of set token paying with Eth + * + * @param _setToken Set token to issue + * @param _setAmount Amount to issue + * @param _swapDataDebtForCollateral Data (token addresses and fee levels) to describe the swap path from Debt to collateral token + * @param _swapDataInputToken Data (token addresses and fee levels) to describe the swap path from eth to collateral token + */ + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _setAmount, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + external + payable + nonReentrant + { + _initiateIssuance( + _setToken, + _setAmount, + DEXAdapter.ETH_ADDRESS, + msg.value, + _swapDataDebtForCollateral, + _swapDataInputToken + ); + } + + /** + * This is the callback function that will be called by the Balancerv2 Pool after flashloaned tokens have been sent + * to this contract. + * After exiting this function the Vault enforces that we transfer back the loaned tokens + interest. If that check fails + * the whole transaction gets reverted + * + * @param tokens Addresses of all assets that were borrowed + * @param amounts Amounts that were borrowed + * @param feeAmounts Interest to be paid on top of borrowed amount + * @param userData Encoded bytestring of other parameters from the original contract call to be used downstream + * + */ + function receiveFlashLoan( + IERC20[] memory tokens, + uint256[] memory amounts, + uint256[] memory feeAmounts, + bytes memory userData + ) + external + override + onlyBalancerV2Vault + { + + DecodedParams memory decodedParams = abi.decode(userData, (DecodedParams)); + require(flashLoanBenefactor == decodedParams.originalSender, "Flashloan not initiated by this contract"); + + if(decodedParams.isIssuance){ + _performIssuance(address(tokens[0]), amounts[0], feeAmounts[0], decodedParams); + } else { + _performRedemption(address(tokens[0]), amounts[0], feeAmounts[0], decodedParams); + } + + for(uint256 i = 0; i < tokens.length; i++) { + tokens[i].safeTransfer(address(balancerV2Vault), amounts[i]+ feeAmounts[i]); + } + + } + + /** + * Runs all the necessary approval functions required for a list of ERC20 tokens. + * + * @param _tokens Addresses of the tokens which need approval + */ + function approveTokens(IERC20[] memory _tokens) external { + for (uint256 i = 0; i < _tokens.length; i++) { + _approveToken(_tokens[i]); + } + } + + /** + * Runs all the necessary approval functions required before issuing + * or redeeming a SetToken. This function need to be called only once before the first time + * this smart contract is used on any particular SetToken. + * + * @param _setToken Address of the SetToken being initialized + */ + function approveSetToken(ISetToken _setToken) external { + LeveragedTokenData memory leveragedTokenData = _getLeveragedTokenData(_setToken, 1 ether, true); + + _approveToken(IERC20(leveragedTokenData.collateralAToken)); + _approveTokenToLendingPool(IERC20(leveragedTokenData.collateralToken)); + + _approveToken(IERC20(leveragedTokenData.debtToken)); + _approveTokenToLendingPool(IERC20(leveragedTokenData.debtToken)); + } + + /* ============ Internal Functions ============ */ + + /** + * Performs all the necessary steps for issuance using the collateral tokens obtained in the flashloan + * + * @param _collateralToken Address of the underlying collateral token that was loaned + * @param _collateralTokenAmountNet Amount of collateral token that was received as flashloan + * @param _premium Premium / Interest that has to be returned to the lending pool on top of the loaned amount + * @param _decodedParams Struct containing token addresses / amounts to perform issuance + */ + function _performIssuance( + address _collateralToken, + uint256 _collateralTokenAmountNet, + uint256 _premium, + DecodedParams memory _decodedParams + ) + internal + { + // Deposit collateral token obtained from flashloan to get the respective aToken position required for issuance + _depositCollateralToken(_collateralToken, _collateralTokenAmountNet); + + // Issue set using the aToken returned by deposit step + _issueSet(_decodedParams.setToken, _decodedParams.setAmount, _decodedParams.originalSender); + // Obtain necessary collateral tokens to repay flashloan + uint amountInputTokenSpent = _obtainCollateralTokens( + _collateralToken, + _collateralTokenAmountNet + _premium, + _decodedParams + ); + require(amountInputTokenSpent <= _decodedParams.limitAmount, "ExchangeIssuance: INSUFFICIENT INPUT AMOUNT"); + } + + /** + * Performs all the necessary steps for redemption using the debt tokens obtained in the flashloan + * + * @param _debtToken Address of the debt token that was loaned + * @param _debtTokenAmountNet Amount of debt token that was received as flashloan + * @param _premium Premium / Interest that has to be returned to the lending pool on top of the loaned amount + * @param _decodedParams Struct containing token addresses / amounts to perform redemption + */ + function _performRedemption( + address _debtToken, + uint256 _debtTokenAmountNet, + uint256 _premium, + DecodedParams memory _decodedParams + ) + internal + { + // Redeem set using debt tokens obtained from flashloan + _redeemSet( + _decodedParams.setToken, + _decodedParams.setAmount, + _decodedParams.originalSender + ); + // Withdraw underlying collateral token from the aToken position returned by redeem step + _withdrawCollateralToken( + _decodedParams.leveragedTokenData.collateralToken, + _decodedParams.leveragedTokenData.collateralAmount - ROUNDING_ERROR_MARGIN + ); + // Obtain debt tokens required to repay flashloan by swapping the underlying collateral tokens obtained in withdraw step + uint256 collateralTokenSpent = _swapCollateralForDebtToken( + _debtTokenAmountNet + _premium, + _debtToken, + _decodedParams.leveragedTokenData.collateralAmount, + _decodedParams.leveragedTokenData.collateralToken, + _decodedParams.collateralAndDebtSwapData + ); + // Liquidate remaining collateral tokens for the payment token specified by user + uint256 amountOutputToken = _liquidateCollateralTokens( + collateralTokenSpent, + _decodedParams.setToken, + _decodedParams.setAmount, + _decodedParams.originalSender, + _decodedParams.paymentToken, + _decodedParams.limitAmount, + _decodedParams.leveragedTokenData.collateralToken, + _decodedParams.leveragedTokenData.collateralAmount - 2*ROUNDING_ERROR_MARGIN, + _decodedParams.paymentTokenSwapData + ); + require(amountOutputToken >= _decodedParams.limitAmount, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); + } + + + /** + * Returns the collateral / debt token addresses and amounts for a leveraged index + * + * @param _setToken Address of the SetToken to be issued / redeemed + * @param _setAmount Amount of SetTokens to issue / redeem + * @param _isIssuance Boolean indicating if the SetToken is to be issued or redeemed + * + * @return Struct containing the collateral / debt token addresses and amounts + */ + function _getLeveragedTokenData( + ISetToken _setToken, + uint256 _setAmount, + bool _isIssuance + ) + internal + view + returns (LeveragedTokenData memory) + { + address[] memory components; + uint256[] memory equityPositions; + uint256[] memory debtPositions; + + + if(_isIssuance){ + (components, equityPositions, debtPositions) = debtIssuanceModule.getRequiredComponentIssuanceUnits(_setToken, _setAmount); + } else { + (components, equityPositions, debtPositions) = debtIssuanceModule.getRequiredComponentRedemptionUnits(_setToken, _setAmount); + } + + require(components.length == 2, "ExchangeIssuance: TOO MANY COMPONENTS"); + require(equityPositions[0] == 0 || equityPositions[1] == 0, "ExchangeIssuance: TOO MANY EQUITY POSITIONS"); + require(debtPositions[0] == 0 || debtPositions[1] == 0, "ExchangeIssuance: TOO MANY DEBT POSITIONS"); + + if(equityPositions[0] > 0){ + return LeveragedTokenData( + components[0], + IAToken(components[0]).UNDERLYING_ASSET_ADDRESS(), + equityPositions[0] + ROUNDING_ERROR_MARGIN, + components[1], + debtPositions[1] + ); + } else { + return LeveragedTokenData( + components[1], + IAToken(components[1]).UNDERLYING_ASSET_ADDRESS(), + equityPositions[1] + ROUNDING_ERROR_MARGIN, + components[0], + debtPositions[0] + ); + } + } + + + + /** + * Approves max amount of given token to all exchange routers and the debt issuance module + * + * @param _token Address of the token to be approved + */ + function _approveToken(IERC20 _token) internal { + _safeApprove(_token, address(debtIssuanceModule), MAX_UINT256); + } + + /** + * Initiates a flashloan call with the correct parameters for issuing set tokens in the callback + * Borrows correct amount of collateral token and and forwards encoded memory to controll issuance in the callback. + * + * @param _setToken Address of the SetToken being initialized + * @param _setAmount Amount of the SetToken being initialized + * @param _inputToken Address of the input token to pay with + * @param _maxAmountInputToken Maximum amount of input token to pay + * @param _swapDataDebtForCollateral Data (token addresses and fee levels) to describe the swap path from Debt to collateral token + * @param _swapDataInputToken Data (token addresses and fee levels) to describe the swap path from input to collateral token + */ + function _initiateIssuance( + ISetToken _setToken, + uint256 _setAmount, + address _inputToken, + uint256 _maxAmountInputToken, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + internal + { + aaveLeverageModule.sync(_setToken); + LeveragedTokenData memory leveragedTokenData = _getLeveragedTokenData(_setToken, _setAmount, true); + + address[] memory assets = new address[](1); + assets[0] = leveragedTokenData.collateralToken; + uint[] memory amounts = new uint[](1); + amounts[0] = leveragedTokenData.collateralAmount; + + bytes memory params = abi.encode( + DecodedParams( + _setToken, + _setAmount, + msg.sender, + true, + _inputToken, + _maxAmountInputToken, + leveragedTokenData, + _swapDataDebtForCollateral, + _swapDataInputToken + ) + ); + + _flashloan(assets, amounts, params); + + } + + /** + * Initiates a flashloan call with the correct parameters for redeeming set tokens in the callback + * + * @param _setToken Address of the SetToken to redeem + * @param _setAmount Amount of the SetToken to redeem + * @param _outputToken Address of token to return to the user + * @param _minAmountOutputToken Minimum amount of output token to receive + * @param _swapDataCollateralForDebt Data (token path and fee levels) describing the swap from Collateral Token to Debt Token + * @param _swapDataOutputToken Data (token path and fee levels) describing the swap from Collateral Token to Output token + */ + function _initiateRedemption( + ISetToken _setToken, + uint256 _setAmount, + address _outputToken, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapDataCollateralForDebt, + DEXAdapter.SwapData memory _swapDataOutputToken + ) + internal + { + aaveLeverageModule.sync(_setToken); + LeveragedTokenData memory leveragedTokenData = _getLeveragedTokenData(_setToken, _setAmount, false); + + address[] memory assets = new address[](1); + assets[0] = leveragedTokenData.debtToken; + uint[] memory amounts = new uint[](1); + amounts[0] = leveragedTokenData.debtAmount; + + bytes memory params = abi.encode( + DecodedParams( + _setToken, + _setAmount, + msg.sender, + false, + _outputToken, + _minAmountOutputToken, + leveragedTokenData, + _swapDataCollateralForDebt, + _swapDataOutputToken + ) + ); + + _flashloan(assets, amounts, params); + + } + + /** + * Gets rid of the obtained collateral tokens from redemption by either sending them to the user + * directly or converting them to the payment token and sending those out. + * + * @param _collateralTokenSpent Amount of collateral token spent to obtain the debt token required for redemption + * @param _setToken Address of the SetToken to be issued + * @param _setAmount Amount of SetTokens to issue + * @param _originalSender Address of the user who initiated the redemption + * @param _outputToken Address of token to return to the user + * @param _collateralToken Address of the collateral token to sell + * @param _collateralAmount Amount of collateral token to sell + * @param _minAmountOutputToken Minimum amount of output token to return to the user + * @param _swapData Struct containing path and fee data for swap + * + * @return Amount of output token returned to the user + */ + function _liquidateCollateralTokens( + uint256 _collateralTokenSpent, + ISetToken _setToken, + uint256 _setAmount, + address _originalSender, + address _outputToken, + uint256 _minAmountOutputToken, + address _collateralToken, + uint256 _collateralAmount, + DEXAdapter.SwapData memory _swapData + ) + internal + returns (uint256) + { + require(_collateralAmount >= _collateralTokenSpent, "ExchangeIssuance: OVERSPENT COLLATERAL TOKEN"); + uint256 amountToReturn = _collateralAmount.sub(_collateralTokenSpent); + uint256 outputAmount; + if(_outputToken == DEXAdapter.ETH_ADDRESS){ + outputAmount = _liquidateCollateralTokensForETH( + _collateralToken, + amountToReturn, + _originalSender, + _minAmountOutputToken, + _swapData + ); + } else { + outputAmount = _liquidateCollateralTokensForERC20( + _collateralToken, + amountToReturn, + _originalSender, + IERC20(_outputToken), + _minAmountOutputToken, + _swapData + ); + } + emit FlashMint(_originalSender, _setToken, _outputToken, _setAmount, outputAmount); + return outputAmount; + } + + /** + * Returns the collateralToken directly to the user + * + * @param _collateralToken Address of the the collateral token + * @param _collateralRemaining Amount of the collateral token remaining after buying required debt tokens + * @param _originalSender Address of the original sender to return the tokens to + */ + function _returnCollateralTokensToSender( + address _collateralToken, + uint256 _collateralRemaining, + address _originalSender + ) + internal + { + IERC20(_collateralToken).transfer(_originalSender, _collateralRemaining); + } + + /** + * Sells the collateral tokens for the selected output ERC20 and returns that to the user + * + * @param _collateralToken Address of the collateral token + * @param _collateralRemaining Amount of the collateral token remaining after buying required debt tokens + * @param _originalSender Address of the original sender to return the tokens to + * @param _outputToken Address of token to return to the user + * @param _minAmountOutputToken Minimum amount of output token to return to the user + * @param _swapData Data (token path and fee levels) describing the swap path from Collateral Token to Output token + * + * @return Amount of output token returned to the user + */ + function _liquidateCollateralTokensForERC20( + address _collateralToken, + uint256 _collateralRemaining, + address _originalSender, + IERC20 _outputToken, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapData + ) + internal + returns (uint256) + { + if(address(_outputToken) == _collateralToken){ + _returnCollateralTokensToSender(_collateralToken, _collateralRemaining, _originalSender); + return _collateralRemaining; + } + uint256 outputTokenAmount = _swapCollateralForOutputToken( + _collateralToken, + _collateralRemaining, + address(_outputToken), + _minAmountOutputToken, + _swapData + ); + _outputToken.transfer(_originalSender, outputTokenAmount); + return outputTokenAmount; + } + + /** + * Sells the remaining collateral tokens for weth, withdraws that and returns native eth to the user + * + * @param _collateralToken Address of the collateral token + * @param _collateralRemaining Amount of the collateral token remaining after buying required debt tokens + * @param _originalSender Address of the original sender to return the eth to + * @param _minAmountOutputToken Minimum amount of output token to return to user + * @param _swapData Data (token path and fee levels) describing the swap path from Collateral Token to eth + * + * @return Amount of eth returned to the user + */ + function _liquidateCollateralTokensForETH( + address _collateralToken, + uint256 _collateralRemaining, + address _originalSender, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapData + ) + internal + isValidPath(_swapData.path, _collateralToken, addresses.weth) + returns(uint256) + { + uint256 ethAmount = _swapCollateralForOutputToken( + _collateralToken, + _collateralRemaining, + addresses.weth, + _minAmountOutputToken, + _swapData + ); + if (ethAmount > 0) { + IWETH(addresses.weth).withdraw(ethAmount); + (payable(_originalSender)).sendValue(ethAmount); + } + return ethAmount; + } + + /** + * Obtains the tokens necessary to return the flashloan by swapping the debt tokens obtained + * from issuance and making up the shortfall using the users funds. + * + * @param _collateralToken collateral token to obtain + * @param _amountRequired Amount of collateralToken required to repay the flashloan + * @param _decodedParams Struct containing decoded data from original call passed through via flashloan + * + * @return Amount of input token spent + */ + function _obtainCollateralTokens( + address _collateralToken, + uint256 _amountRequired, + DecodedParams memory _decodedParams + ) + internal + returns (uint256) + { + uint collateralTokenObtained = _swapDebtForCollateralToken( + _collateralToken, + _decodedParams.leveragedTokenData.debtToken, + _decodedParams.leveragedTokenData.debtAmount, + _decodedParams.collateralAndDebtSwapData + ); + + uint collateralTokenShortfall = _amountRequired.sub(collateralTokenObtained) + ROUNDING_ERROR_MARGIN; + uint amountInputToken; + + if(_decodedParams.paymentToken == DEXAdapter.ETH_ADDRESS){ + amountInputToken = _makeUpShortfallWithETH( + _collateralToken, + collateralTokenShortfall, + _decodedParams.originalSender, + _decodedParams.limitAmount, + _decodedParams.paymentTokenSwapData + ); + } else { + amountInputToken = _makeUpShortfallWithERC20( + _collateralToken, + collateralTokenShortfall, + _decodedParams.originalSender, + IERC20(_decodedParams.paymentToken), + _decodedParams.limitAmount, + _decodedParams.paymentTokenSwapData + ); + } + emit FlashRedeem( + _decodedParams.originalSender, + _decodedParams.setToken, + _decodedParams.paymentToken, + amountInputToken, + _decodedParams.setAmount + ); + return amountInputToken; + } + + /** + * Issues set token using the previously obtained collateral token + * Results in debt token being returned to the contract + * + * @param _setToken Address of the SetToken to be issued + * @param _setAmount Amount of SetTokens to issue + * @param _originalSender Adress that initiated the token issuance, which will receive the set tokens + */ + function _issueSet(ISetToken _setToken, uint256 _setAmount, address _originalSender) internal { + debtIssuanceModule.issue(_setToken, _setAmount, _originalSender); + } + + /** + * Redeems set token using the previously obtained debt token + * Results in collateral token being returned to the contract + * + * @param _setToken Address of the SetToken to be redeemed + * @param _setAmount Amount of SetTokens to redeem + * @param _originalSender Adress that initiated the token redemption which is the source of the set tokens to be redeemed + */ + function _redeemSet(ISetToken _setToken, uint256 _setAmount, address _originalSender) internal { + _setToken.safeTransferFrom(_originalSender, address(this), _setAmount); + debtIssuanceModule.redeem(_setToken, _setAmount, address(this)); + } + + /** + * Transfers the shortfall between the amount of tokens required to return flashloan and what was obtained + * from swapping the debt tokens from the users address + * + * @param _token Address of the token to transfer from user + * @param _shortfall Collateral token shortfall required to return the flashloan + * @param _originalSender Adress that initiated the token issuance, which is the adresss form which to transfer the tokens + */ + function _transferShortfallFromSender( + address _token, + uint256 _shortfall, + address _originalSender + ) + internal + { + if(_shortfall>0){ + IERC20(_token).safeTransferFrom(_originalSender, address(this), _shortfall); + } + } + + /** + * Makes up the collateral token shortfall with user specified ERC20 token + * + * @param _collateralToken Address of the collateral token + * @param _collateralTokenShortfall Shortfall of collateral token that was not covered by selling the debt tokens + * @param _originalSender Address of the original sender to return the tokens to + * @param _inputToken Input token to pay with + * @param _maxAmountInputToken Maximum amount of input token to spend + * + * @return Amount of input token spent + */ + function _makeUpShortfallWithERC20( + address _collateralToken, + uint256 _collateralTokenShortfall, + address _originalSender, + IERC20 _inputToken, + uint256 _maxAmountInputToken, + DEXAdapter.SwapData memory _swapData + ) + internal + returns (uint256) + { + if(address(_inputToken) == _collateralToken){ + _transferShortfallFromSender(_collateralToken, _collateralTokenShortfall, _originalSender); + return _collateralTokenShortfall; + } else { + _inputToken.transferFrom(_originalSender, address(this), _maxAmountInputToken); + uint256 amountInputToken = _swapInputForCollateralToken( + _collateralToken, + _collateralTokenShortfall, + address(_inputToken), + _maxAmountInputToken, + _swapData + ); + if(amountInputToken < _maxAmountInputToken){ + _inputToken.transfer(_originalSender, _maxAmountInputToken.sub(amountInputToken)); + } + return amountInputToken; + } + } + + /** + * Makes up the collateral token shortfall with native eth + * + * @param _collateralToken Address of the collateral token + * @param _collateralTokenShortfall Shortfall of collateral token that was not covered by selling the debt tokens + * @param _originalSender Address of the original sender to return the tokens to + * @param _maxAmountEth Maximum amount of eth to pay + * + * @return Amount of eth spent + */ + function _makeUpShortfallWithETH( + address _collateralToken, + uint256 _collateralTokenShortfall, + address _originalSender, + uint256 _maxAmountEth, + DEXAdapter.SwapData memory _swapData + + ) + internal + returns(uint256) + { + IWETH(addresses.weth).deposit{value: _maxAmountEth}(); + + uint256 amountEth = _swapInputForCollateralToken( + _collateralToken, + _collateralTokenShortfall, + addresses.weth, + _maxAmountEth, + _swapData + ); + + if(_maxAmountEth > amountEth){ + uint256 amountEthReturn = _maxAmountEth.sub(amountEth); + IWETH(addresses.weth).withdraw(amountEthReturn); + (payable(_originalSender)).sendValue(amountEthReturn); + } + return amountEth; + } + + /** + * Swaps the debt tokens obtained from issuance for the collateral + * + * @param _collateralToken Address of the collateral token buy + * @param _debtToken Address of the debt token to sell + * @param _debtAmount Amount of debt token to sell + * @param _swapData Struct containing path and fee data for swap + * + * @return Amount of collateral token obtained + */ + function _swapDebtForCollateralToken( + address _collateralToken, + address _debtToken, + uint256 _debtAmount, + DEXAdapter.SwapData memory _swapData + ) + internal + isValidPath(_swapData.path, _debtToken, _collateralToken) + returns (uint256) + { + return addresses.swapExactTokensForTokens( + _debtAmount, + // minAmountOut is 0 here since we are going to make up the shortfall with the input token. + // Sandwich protection is provided by the check at the end against _maxAmountInputToken parameter specified by the user + 0, + _swapData + ); + } + + /** + * Acquires debt tokens needed for flashloan repayment by swapping a portion of the collateral tokens obtained from redemption + * + * @param _debtAmount Amount of debt token to buy + * @param _debtToken Address of debt token + * @param _collateralAmount Amount of collateral token available to spend / used as maxAmountIn parameter + * @param _collateralToken Address of collateral token + * @param _swapData Struct containing path and fee data for swap + * + * @return Amount of collateral token spent + */ + function _swapCollateralForDebtToken( + uint256 _debtAmount, + address _debtToken, + uint256 _collateralAmount, + address _collateralToken, + DEXAdapter.SwapData memory _swapData + ) + internal + isValidPath(_swapData.path, _collateralToken, _debtToken) + returns (uint256) + { + return addresses.swapTokensForExactTokens( + _debtAmount, + _collateralAmount, + _swapData + ); + } + + /** + * Acquires the required amount of collateral tokens by swapping the input tokens + * Does nothing if collateral and input token are indentical + * + * @param _collateralToken Address of collateral token + * @param _amountRequired Remaining amount of collateral token required to repay flashloan, after having swapped debt tokens for collateral + * @param _inputToken Address of input token to swap + * @param _maxAmountInputToken Maximum amount of input token to spend + * @param _swapData Data (token addresses and fee levels) describing the swap path + * + * @return Amount of input token spent + */ + function _swapInputForCollateralToken( + address _collateralToken, + uint256 _amountRequired, + address _inputToken, + uint256 _maxAmountInputToken, + DEXAdapter.SwapData memory _swapData + ) + internal + isValidPath( + _swapData.path, + _inputToken, + _collateralToken + ) + returns (uint256) + { + if(_collateralToken == _inputToken) return _amountRequired; + return addresses.swapTokensForExactTokens( + _amountRequired, + _maxAmountInputToken, + _swapData + ); + } + + + /** + * Swaps the collateral tokens obtained from redemption for the selected output token + * If both tokens are the same, does nothing + * + * @param _collateralToken Address of collateral token + * @param _collateralTokenAmount Amount of colalteral token to swap + * @param _outputToken Address of the ERC20 token to swap into + * @param _minAmountOutputToken Minimum amount of output token to return to the user + * @param _swapData Data (token addresses and fee levels) describing the swap path + * + * @return Amount of output token obtained + */ + function _swapCollateralForOutputToken( + address _collateralToken, + uint256 _collateralTokenAmount, + address _outputToken, + uint256 _minAmountOutputToken, + DEXAdapter.SwapData memory _swapData + ) + internal + isValidPath(_swapData.path, _collateralToken, _outputToken) + returns (uint256) + { + return addresses.swapExactTokensForTokens( + _collateralTokenAmount, + _minAmountOutputToken, + _swapData + ); + } + + + + /** + * Deposit collateral to aave to obtain collateralAToken for issuance + * + * @param _collateralToken Address of collateral token + * @param _depositAmount Amount to deposit + */ + function _depositCollateralToken( + address _collateralToken, + uint256 _depositAmount + ) internal { + LENDING_POOL.deposit(_collateralToken, _depositAmount, address(this), 0); + } + + /** + * Convert collateralAToken from set redemption to collateralToken by withdrawing underlying from Aave + * + * @param _collateralToken Address of the collateralToken to withdraw from Aave lending pool + * @param _collateralAmount Amount of collateralToken to withdraw + */ + function _withdrawCollateralToken( + address _collateralToken, + uint256 _collateralAmount + ) internal { + LENDING_POOL.withdraw(_collateralToken, _collateralAmount, address(this)); + } + + /** + * Sets a max approval limit for an ERC20 token, provided the current allowance + * is less than the required allownce. + * + * @param _token Token to approve + * @param _spender Spender address to approve + * @param _requiredAllowance Target allowance to set + */ + function _safeApprove( + IERC20 _token, + address _spender, + uint256 _requiredAllowance + ) + internal + { + uint256 allowance = _token.allowance(address(this), _spender); + if (allowance < _requiredAllowance) { + _token.safeIncreaseAllowance(_spender, MAX_UINT256 - allowance); + } + } + + /** + * Approves max amount of token to lending pool + * + * @param _token Address of the token to approve + */ + function _approveTokenToLendingPool( + IERC20 _token + ) + internal + { + uint256 allowance = _token.allowance(address(this), address(LENDING_POOL)); + if (allowance > 0) { + _token.approve(address(LENDING_POOL), 0); + } + _token.approve(address(LENDING_POOL), MAX_UINT256); + } + + /** + * Triggers the flashloan from the BalancerV2 Vault + * + * @param assets Addresses of tokens to loan + * @param amounts Amounts to loan + * @param params Encoded memory to forward to the executeOperation method + */ + function _flashloan( + address[] memory assets, + uint256[] memory amounts, + bytes memory params + ) + internal + { + require(flashLoanBenefactor == address(0), "Flashloan already taken"); + flashLoanBenefactor = msg.sender; + balancerV2Vault.flashLoan(this, assets, amounts, params); + flashLoanBenefactor = address(0); + } + + /** + * Redeems a given amount of SetToken. + * + * @param _setToken Address of the SetToken to be redeemed + * @param _amount Amount of SetToken to be redeemed + */ + function _redeemExactSet(ISetToken _setToken, uint256 _amount) internal returns (uint256) { + _setToken.safeTransferFrom(msg.sender, address(this), _amount); + debtIssuanceModule.redeem(_setToken, _amount, address(this)); + } + receive() external payable {} +} diff --git a/contracts/interfaces/external/balancer-v2/IVault.sol b/contracts/interfaces/external/balancer-v2/IVault.sol new file mode 100644 index 00000000..96510e9d --- /dev/null +++ b/contracts/interfaces/external/balancer-v2/IVault.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.6.10 <0.9.0; +pragma experimental ABIEncoderV2; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IFlashLoanRecipient { + /** + * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. + * + * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this + * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the + * Vault, or else the entire flash loan will revert. + * + * `userData` is the same value passed in the `IVault.flashLoan` call. + */ + function receiveFlashLoan( + IERC20[] memory tokens, + uint256[] memory amounts, + uint256[] memory feeAmounts, + bytes memory userData + ) external; +} + +/** + * Stripped down interface of IVault. + * https://github.com/balancer/balancer-v2-monorepo/blob/master/pkg/interfaces/contracts/vault/IVault.sol + */ +interface IVault { + // Swaps + // + // Users can swap tokens with Pools by calling the `swap` and `batchSwap` functions. To do this, + // they need not trust Pool contracts in any way: all security checks are made by the Vault. They must however be + // aware of the Pools' pricing algorithms in order to estimate the prices Pools will quote. + // + // The `swap` function executes a single swap, while `batchSwap` can perform multiple swaps in sequence. + // In each individual swap, tokens of one kind are sent from the sender to the Pool (this is the 'token in'), + // and tokens of another kind are sent from the Pool to the recipient in exchange (this is the 'token out'). + // More complex swaps, such as one token in to multiple tokens out can be achieved by batching together + // individual swaps. + // + // There are two swap kinds: + // - 'given in' swaps, where the amount of tokens in (sent to the Pool) is known, and the Pool determines (via the + // `onSwap` hook) the amount of tokens out (to send to the recipient). + // - 'given out' swaps, where the amount of tokens out (received from the Pool) is known, and the Pool determines + // (via the `onSwap` hook) the amount of tokens in (to receive from the sender). + // + // Additionally, it is possible to chain swaps using a placeholder input amount, which the Vault replaces with + // the calculated output of the previous swap. If the previous swap was 'given in', this will be the calculated + // tokenOut amount. If the previous swap was 'given out', it will use the calculated tokenIn amount. These extended + // swaps are known as 'multihop' swaps, since they 'hop' through a number of intermediate tokens before arriving at + // the final intended token. + // + // In all cases, tokens are only transferred in and out of the Vault (or withdrawn from and deposited into Internal + // Balance) after all individual swaps have been completed, and the net token balance change computed. This makes + // certain swap patterns, such as multihops, or swaps that interact with the same token pair in multiple Pools, cost + // much less gas than they would otherwise. + // + // It also means that under certain conditions it is possible to perform arbitrage by swapping with multiple + // Pools in a way that results in net token movement out of the Vault (profit), with no tokens being sent in (only + // updating the Pool's internal accounting). + // + // To protect users from front-running or the market changing rapidly, they supply a list of 'limits' for each token + // involved in the swap, where either the maximum number of tokens to send (by passing a positive value) or the + // minimum amount of tokens to receive (by passing a negative value) is specified. + // + // Additionally, a 'deadline' timestamp can also be provided, forcing the swap to fail if it occurs after + // this point in time (e.g. if the transaction failed to be included in a block promptly). + // + // If interacting with Pools that hold WETH, it is possible to both send and receive ETH directly: the Vault will do + // the wrapping and unwrapping. To enable this mechanism, the IAsset sentinel value (the zero address) must be + // passed in the `assets` array instead of the WETH address. Note that it is possible to combine ETH and WETH in the + // same swap. Any excess ETH will be sent back to the caller (not the sender, which is relevant for relayers). + // + // Finally, Internal Balance can be used when either sending or receiving tokens. + + enum SwapKind { + GIVEN_IN, + GIVEN_OUT + } + + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is 'given in' (the number of tokens to send to the Pool is known), it returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is 'given out' (the number of tokens to take from the Pool is known), it returns the amount of tokens + * sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` struct. + * + * Emits a `Swap` event. + */ + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); + + /** + * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on + * the `kind` value. + * + * `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address). + * Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + address assetIn; + address assetOut; + uint256 amount; + bytes userData; + } + + /** + * @dev Performs a series of swaps with one or multiple Pools. In each individual swap, the caller determines either + * the amount of tokens sent to or received from the Pool, depending on the `kind` value. + * + * Returns an array with the net Vault asset balance deltas. Positive amounts represent tokens (or ETH) sent to the + * Vault, and negative amounts represent tokens (or ETH) sent by the Vault. Each delta corresponds to the asset at + * the same index in the `assets` array. + * + * Swaps are executed sequentially, in the order specified by the `swaps` array. Each array element describes a + * Pool, the token to be sent to this Pool, the token to receive from it, and an amount that is either `amountIn` or + * `amountOut` depending on the swap kind. + * + * Multihop swaps can be executed by passing an `amount` value of zero for a swap. This will cause the amount in/out + * of the previous swap to be used as the amount in for the current one. In a 'given in' swap, 'tokenIn' must equal + * the previous swap's `tokenOut`. For a 'given out' swap, `tokenOut` must equal the previous swap's `tokenIn`. + * + * The `assets` array contains the addresses of all assets involved in the swaps. These are either token addresses, + * or the IAsset sentinel value for ETH (the zero address). Each entry in the `swaps` array specifies tokens in and + * out by referencing an index in `assets`. Note that Pools never interact with ETH directly: it will be wrapped to + * or unwrapped from WETH by the Vault. + * + * Internal Balance usage, sender, and recipient are determined by the `funds` struct. The `limits` array specifies + * the minimum or maximum amount of each token the vault is allowed to transfer. + * + * `batchSwap` can be used to make a single swap, like `swap` does, but doing so requires more gas than the + * equivalent `swap` call. + * + * Emits `Swap` events. + */ + function batchSwap( + SwapKind kind, + BatchSwapStep[] memory swaps, + address[] memory assets, + FundManagement memory funds, + int256[] memory limits, + uint256 deadline + ) external payable returns (int256[] memory); + + /** + * @dev Data for each individual swap executed by `batchSwap`. The asset in and out fields are indexes into the + * `assets` array passed to that function, and ETH assets are converted to WETH. + * + * If `amount` is zero, the multihop mechanism is used to determine the actual amount based on the amount in/out + * from the previous swap, depending on the swap kind. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } + + /** + * @dev Emitted for each individual swap performed by `swap` or `batchSwap`. + */ + event Swap( + bytes32 indexed poolId, + IERC20 indexed tokenIn, + IERC20 indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); + + /** + * @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the + * `recipient` account. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20 + * transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender` + * must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of + * `joinPool`. + * + * If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of + * transferred. This matches the behavior of `exitPool`. + * + * Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a + * revert. + */ + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + /** + * @dev Simulates a call to `batchSwap`, returning an array of Vault asset deltas. Calls to `swap` cannot be + * simulated directly, but an equivalent `batchSwap` call can and will yield the exact same result. + * + * Each element in the array corresponds to the asset at the same index, and indicates the number of tokens (or ETH) + * the Vault would take from the sender (if positive) or send to the recipient (if negative). The arguments it + * receives are the same that an equivalent `batchSwap` call would receive. + * + * Unlike `batchSwap`, this function performs no checks on the sender or recipient field in the `funds` struct. + * This makes it suitable to be called by off-chain applications via eth_call without needing to hold tokens, + * approve them for the Vault, or even know a user's address. + * + * Note that this function is not 'view' (due to implementation details): the client code must explicitly execute + * eth_call instead of eth_sendTransaction. + */ + function queryBatchSwap( + SwapKind kind, + BatchSwapStep[] memory swaps, + address[] memory assets, + FundManagement memory funds + ) external returns (int256[] memory assetDeltas); + + // Flash Loans + + /** + * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, + * and then reverting unless the tokens plus a proportional protocol fee have been returned. + * + * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount + * for each token contract. `tokens` must be sorted in ascending order. + * + * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the + * `receiveFlashLoan` call. + * + * Emits `FlashLoan` events. + */ + function flashLoan( + IFlashLoanRecipient recipient, + address[] memory tokens, + uint256[] memory amounts, + bytes memory userData + ) external; + + /** + * @dev Emitted for each individual flash loan performed by `flashLoan`. + */ + event FlashLoan(IFlashLoanRecipient indexed recipient, IERC20 indexed token, uint256 amount, uint256 feeAmount); + +} \ No newline at end of file diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 67ed495b..ee7eb1c2 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -7,6 +7,9 @@ export const PRODUCTION_ADDRESSES = { dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F", weth: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", icEth: "0x7C07F7aBe10CE8e33DC6C5aD68FE033085256A84", + icReth: "0xe8888Cdbc0A5958C29e7D91DAE44897c7e64F9BC", + rETH: "0xae78736Cd615f374D3085123A210448E74Fc6393", + aEthrETH : "0xCc9EE9483f662091a1de4795249E24aC0aC2630f", aSTETH: "0x1982b2F5814301d4e9a8b0201555376e62F82428", ETH2xFli: "0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD", cEther: "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5", @@ -32,6 +35,7 @@ export const PRODUCTION_ADDRESSES = { ethAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", pools: { stEthEth: "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", + rEthEth: "0x0f3159811670c117c372428D4E69AC32325e4D0F", }, }, sushiswap: { @@ -44,6 +48,9 @@ export const PRODUCTION_ADDRESSES = { router: "0xE592427A0AEce92De3Edee1F18E0157C05861564", quoter: "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", }, + balancerv2: { + vault: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + }, }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", @@ -58,12 +65,18 @@ export const PRODUCTION_ADDRESSES = { notionalTradeModule: "0x600d9950c6ecAef98Cc42fa207E92397A6c43416", tradeModule: "0xFaAB3F8f3678f68AA0d307B66e71b636F82C28BF", airdropModule: "0x09b9e7c7e2daf40fCb286fE6b863e517d5d5c40F", + aaveV3LeverageStrategyExtension: "0x7d3f7EDD04916F3Cb2bC6740224c636B9AE43200", + aaveV3LeverageModule: "0x71E932715F5987077ADC5A7aA245f38841E0DcBe", }, lending: { aave: { addressProvider: "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", lendingPool: "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", }, + aaveV3: { + addressProvider: "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + lendingPool: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", + }, notional: { wrappedfCashFactory: "0x5D051DeB5db151C2172dCdCCD42e6A2953E27261", notionalV2: "0x1344a36a1b56144c3bc62e7757377d288fde0369", diff --git a/test/integration/ethereum/flashMintLeveraged.spec.ts b/test/integration/ethereum/flashMintLeveraged.spec.ts new file mode 100644 index 00000000..54b0b6b3 --- /dev/null +++ b/test/integration/ethereum/flashMintLeveraged.spec.ts @@ -0,0 +1,466 @@ +import "module-alias/register"; +import { Account, Address } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { getAccounts, getWaffleExpect, preciseMul } from "@utils/index"; +import { setBlockNumber } from "@utils/test/testingUtils"; +import { ethers } from "hardhat"; +import { BigNumber, utils } from "ethers"; +import { FlashMintLeveraged } from "@utils/contracts/index"; +import { + IWETH, + StandardTokenMock, + IDebtIssuanceModule, + IERC20__factory, + AaveV3LeverageStrategyExtension__factory +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES, STAGING_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO, MAX_UINT_256 } from "@utils/constants"; +import { ether } from "@utils/index"; +import { impersonateAccount } from "./utils"; + +const expect = getWaffleExpect(); + +enum Exchange { + None, + Sushiswap, + Quickswap, + UniV3, + Curve, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintLeveraged - Integration Test", async () => { + const addresses = process.env.USE_STAGING_ADDRESSES ? STAGING_ADDRESSES : PRODUCTION_ADDRESSES; + let owner: Account; + let deployer: DeployHelper; + + let rEth: StandardTokenMock; + let setToken: StandardTokenMock; + let weth: IWETH; + + // const collateralTokenAddress = addresses.tokens.stEth; + setBlockNumber(17665622); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + + rEth = (await ethers.getContractAt( + "StandardTokenMock", + addresses.tokens.rETH, + )) as StandardTokenMock; + + setToken = (await ethers.getContractAt( + "StandardTokenMock", + addresses.tokens.icReth, + )) as StandardTokenMock; + + + weth = (await ethers.getContractAt("IWETH", addresses.tokens.weth)) as IWETH; + }); + + it("can get lending pool from address provider", async () => { + const addressProvider = await ethers.getContractAt( + "IPoolAddressesProvider", + addresses.lending.aaveV3.addressProvider, + ); + const lendingPool = await addressProvider.getPool(); + expect(lendingPool).to.eq(addresses.lending.aaveV3.lendingPool); + }); + + context("When exchange issuance is deployed", () => { + let flashMintLeveraged: FlashMintLeveraged; + before(async () => { + flashMintLeveraged = await deployer.extensions.deployFlashMintLeveraged( + addresses.tokens.weth, + addresses.dexes.uniV2.router, + addresses.dexes.sushiswap.router, + addresses.dexes.uniV3.router, + addresses.dexes.uniV3.quoter, + addresses.setFork.controller, + addresses.setFork.debtIssuanceModuleV2, + addresses.setFork.aaveV3LeverageModule, + addresses.lending.aaveV3.lendingPool, + addresses.dexes.curve.addressProvider, + addresses.dexes.curve.calculator, + addresses.dexes.balancerv2.vault + ); + }); + + it("weth address is set correctly", async () => { + const returnedAddresses = await flashMintLeveraged.addresses(); + expect(returnedAddresses.weth).to.eq(utils.getAddress(addresses.tokens.weth)); + }); + + it("sushi router address is set correctly", async () => { + const returnedAddresses = await flashMintLeveraged.addresses(); + expect(returnedAddresses.sushiRouter).to.eq( + utils.getAddress(addresses.dexes.sushiswap.router), + ); + }); + + it("uniV2 router address is set correctly", async () => { + const returnedAddresses = await flashMintLeveraged.addresses(); + expect(returnedAddresses.quickRouter).to.eq( + utils.getAddress(addresses.dexes.uniV2.router), + ); + }); + + it("uniV3 router address is set correctly", async () => { + const returnedAddresses = await flashMintLeveraged.addresses(); + expect(returnedAddresses.uniV3Router).to.eq( + utils.getAddress(addresses.dexes.uniV3.router), + ); + }); + + it("controller address is set correctly", async () => { + expect(await flashMintLeveraged.setController()).to.eq( + utils.getAddress(addresses.setFork.controller), + ); + }); + + it("debt issuance module address is set correctly", async () => { + expect(await flashMintLeveraged.debtIssuanceModule()).to.eq( + utils.getAddress(addresses.setFork.debtIssuanceModuleV2), + ); + }); + + describe("When setToken is approved", () => { + let collateralAToken: StandardTokenMock; + let debtToken: StandardTokenMock; + let collateralATokenAddress: Address; + let collateralTokenAddress: Address; + let debtTokenAddress: Address; + before(async () => { + const arETHWhale = "0x4D17676309cb16fA991E6AE43181d08203b781F8"; + const rEthWhale = "0x7d6149aD9A573A6E2Ca6eBf7D4897c1B766841B4"; + const operator = "0x6904110f17feD2162a11B5FA66B188d801443Ea4"; + const whaleSigner = await impersonateAccount(arETHWhale); + + const rEthWhaleSigner = await impersonateAccount(rEthWhale); + + const arETH = IERC20__factory.connect(addresses.tokens.aEthrETH, whaleSigner); + + const rETH = IERC20__factory.connect(addresses.tokens.rETH, rEthWhaleSigner); + await rETH.transfer(owner.address, ether(100)); + await arETH.transfer(owner.address, ether(100)); + + await arETH.connect(owner.wallet).approve(addresses.setFork.debtIssuanceModuleV2, ether(10)); + await rETH.connect(owner.wallet).approve(flashMintLeveraged.address, ether(100)); + const debtIssuanceModule = await ethers.getContractAt( + "IDebtIssuanceModule", addresses.setFork.debtIssuanceModuleV2, owner.wallet) as IDebtIssuanceModule; + + + const issueTx = await debtIssuanceModule.issue(setToken.address, ether(10), owner.address); + + await issueTx.wait(); + + const operatorSigner = await impersonateAccount(operator); + + const aaveV3LeverageStrategyExtension = AaveV3LeverageStrategyExtension__factory.connect( + addresses.setFork.aaveV3LeverageStrategyExtension, operatorSigner); + const engageTx = await aaveV3LeverageStrategyExtension.engage("BalancerV2ExchangeAdapter"); + await engageTx.wait(); + + + await flashMintLeveraged.approveSetToken(setToken.address); + + const leveragedTokenData = await flashMintLeveraged.getLeveragedTokenData( + setToken.address, + ether(1), + true, + ); + + collateralATokenAddress = leveragedTokenData.collateralAToken; + collateralTokenAddress = leveragedTokenData.collateralToken; + debtTokenAddress = leveragedTokenData.debtToken; + + collateralAToken = (await ethers.getContractAt( + "StandardTokenMock", + collateralATokenAddress, + )) as StandardTokenMock; + debtToken = (await ethers.getContractAt( + "StandardTokenMock", + debtTokenAddress, + )) as StandardTokenMock; + + + }); + + it("should adjust collateral a token allowance correctly", async () => { + expect( + await collateralAToken.allowance( + flashMintLeveraged.address, + addresses.setFork.debtIssuanceModuleV2, + ), + ).to.equal(MAX_UINT_256); + }); + it("should adjust debt token allowance correctly", async () => { + expect( + await debtToken.allowance(flashMintLeveraged.address, addresses.setFork.debtIssuanceModuleV2), + ).to.equal(MAX_UINT_256); + }); + + ["collateralToken", "WETH", "ETH"].forEach(inputTokenName => { + describe(`When input/output token is ${inputTokenName}`, () => { + let subjectSetAmount: BigNumber; + let amountIn: BigNumber; + beforeEach(async () => { + amountIn = ether(2); + subjectSetAmount = ether(1); + }); + + describe( + inputTokenName === "ETH" ? "issueExactSetFromETH" : "#issueExactSetFromERC20", + () => { + let swapDataDebtToCollateral: SwapData; + let swapDataInputToken: SwapData; + + + let inputToken: StandardTokenMock | IWETH; + + let subjectSetToken: Address; + let subjectMaxAmountIn: BigNumber; + let subjectInputToken: Address; + + beforeEach(async () => { + swapDataDebtToCollateral = { + path: [addresses.tokens.weth, addresses.tokens.rETH], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + + + swapDataInputToken = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + exchange: Exchange.None, + }; + + + + if (inputTokenName === "collateralToken") { + inputToken = rEth; + } else { + swapDataInputToken = swapDataDebtToCollateral; + + if (inputTokenName === "WETH") { + inputToken = weth; + await weth.deposit({ value: amountIn }); + } + } + + let inputTokenBalance: BigNumber; + if (inputTokenName === "ETH") { + subjectMaxAmountIn = amountIn; + } else { + inputTokenBalance = await inputToken.balanceOf(owner.address); + subjectMaxAmountIn = inputTokenBalance; + await inputToken.approve(flashMintLeveraged.address, subjectMaxAmountIn); + subjectInputToken = inputToken.address; + } + subjectSetToken = setToken.address; + }); + + async function subject() { + if (inputTokenName === "ETH") { + return flashMintLeveraged.issueExactSetFromETH( + subjectSetToken, + subjectSetAmount, + swapDataDebtToCollateral, + swapDataInputToken , + { value: subjectMaxAmountIn }, + ); + } + return flashMintLeveraged.issueExactSetFromERC20( + subjectSetToken, + subjectSetAmount, + subjectInputToken, + subjectMaxAmountIn, + swapDataDebtToCollateral, + swapDataInputToken , + ); + } + + async function subjectQuote() { + return flashMintLeveraged.callStatic.getIssueExactSet( + subjectSetToken, + subjectSetAmount, + swapDataDebtToCollateral, + swapDataInputToken , + ); + } + + + it("should issue the correct amount of tokens", async () => { + const setBalancebefore = await setToken.balanceOf(owner.address); + await subject(); + const setBalanceAfter = await setToken.balanceOf(owner.address); + const setObtained = setBalanceAfter.sub(setBalancebefore); + expect(setObtained).to.eq(subjectSetAmount); + }); + + it("should spend less than specified max amount", async () => { + const inputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + expect(inputSpent.gt(0)).to.be.true; + expect(inputSpent.lte(subjectMaxAmountIn)).to.be.true; + }); + + it("should quote the correct input amount", async () => { + const inputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + + const quotedInputAmount = await subjectQuote(); + + expect(quotedInputAmount).to.gt(preciseMul(inputSpent, ether(0.99))); + expect(quotedInputAmount).to.lt(preciseMul(inputSpent, ether(1.01))); + }); + }, + ); + + describe( + inputTokenName === "ETH" ? "redeemExactSetForETH" : "#redeemExactSetForERC20", + () => { + let swapDataCollateralToDebt: SwapData; + let swapDataOutputToken: SwapData; + + let outputToken: StandardTokenMock | IWETH; + + let subjectSetToken: Address; + let subjectMinAmountOut: BigNumber; + let subjectOutputToken: Address; + + async function subject() { + if (inputTokenName === "ETH") { + return flashMintLeveraged.redeemExactSetForETH( + subjectSetToken, + subjectSetAmount, + subjectMinAmountOut, + swapDataCollateralToDebt, + swapDataOutputToken + ); + } + return flashMintLeveraged.redeemExactSetForERC20( + subjectSetToken, + subjectSetAmount, + subjectOutputToken, + subjectMinAmountOut, + swapDataCollateralToDebt, + swapDataOutputToken + ); + } + + async function subjectQuote(): Promise { + return flashMintLeveraged.callStatic.getRedeemExactSet( + subjectSetToken, + subjectSetAmount, + swapDataCollateralToDebt, swapDataOutputToken + ); + } + + beforeEach(async () => { + swapDataCollateralToDebt = { + path: [collateralTokenAddress, addresses.tokens.weth], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + + if (inputTokenName === "collateralToken") { + outputToken = rEth; + swapDataOutputToken = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + exchange: Exchange.None, + }; + } else { + swapDataOutputToken = swapDataCollateralToDebt; + + if (inputTokenName === "WETH") { + outputToken = weth; + await weth.deposit({ value: amountIn }); + } + } + + subjectMinAmountOut = subjectSetAmount.div(2); + subjectSetToken = setToken.address; + await setToken.approve(flashMintLeveraged.address, subjectSetAmount); + + if (inputTokenName !== "ETH") { + subjectOutputToken = outputToken.address; + } + }); + + it("should redeem the correct amount of tokens", async () => { + const setBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setBalanceAfter = await setToken.balanceOf(owner.address); + const setRedeemed = setBalanceBefore.sub(setBalanceAfter); + expect(setRedeemed).to.eq(subjectSetAmount); + }); + + it("should return at least the specified minimum of output tokens", async () => { + const outputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + await subject(); + const outputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + const outputObtained = outputBalanceAfter.sub(outputBalanceBefore); + expect(outputObtained.gte(subjectMinAmountOut)).to.be.true; + }); + + it("should quote the correct output amount", async () => { + const outputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + await subject(); + const outputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await outputToken.balanceOf(owner.address); + const outputObtained = outputBalanceAfter.sub(outputBalanceBefore); + + const outputAmountQuote = await subjectQuote(); + expect(outputAmountQuote).to.gt(preciseMul(outputObtained, ether(0.99))); + expect(outputAmountQuote).to.lt(preciseMul(outputObtained, ether(1.01))); + }); + }, + ); + }); + }); + }); + }); + }); +} diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 8b1a665d..4ae81600 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -11,6 +11,7 @@ export { DEXAdapter } from "../../typechain/DEXAdapter"; export { ExchangeIssuance } from "../../typechain/ExchangeIssuance"; export { ExchangeIssuanceV2 } from "../../typechain/ExchangeIssuanceV2"; export { ExchangeIssuanceLeveraged } from "../../typechain/ExchangeIssuanceLeveraged"; +export { FlashMintLeveraged } from "../../typechain/FlashMintLeveraged"; export { FlashMintLeveragedForCompound } from "../../typechain/FlashMintLeveragedForCompound"; export { ExchangeIssuanceZeroEx } from "../../typechain/ExchangeIssuanceZeroEx"; export { FlashMintNotional } from "../../typechain/FlashMintNotional"; diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 1df65a6a..aa0f7539 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -38,6 +38,7 @@ import { DEXAdapter__factory } from "../../typechain/factories/DEXAdapter__facto import { ExchangeIssuance__factory } from "../../typechain/factories/ExchangeIssuance__factory"; import { ExchangeIssuanceV2__factory } from "../../typechain/factories/ExchangeIssuanceV2__factory"; import { ExchangeIssuanceLeveraged__factory } from "../../typechain/factories/ExchangeIssuanceLeveraged__factory"; +import { FlashMintLeveraged__factory } from "../../typechain/factories/FlashMintLeveraged__factory"; import { FlashMintNotional__factory } from "../../typechain/factories/FlashMintNotional__factory"; import { FlashMintLeveragedForCompound__factory } from "../../typechain/factories/FlashMintLeveragedForCompound__factory"; import { FlashMintWrapped } from "../../typechain/FlashMintWrapped"; @@ -215,6 +216,52 @@ export default class DeployExtensions { ); } + public async deployFlashMintLeveraged( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + setControllerAddress: Address, + basicIssuanceModuleAddress: Address, + aaveLeveragedModuleAddress: Address, + aaveAddressProviderAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + BalancerV2VaultAddress: Address, + ) { + const dexAdapter = await this.deployDEXAdapter(); + + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapter.sol:DEXAdapter", + ); + + return await new FlashMintLeveraged__factory( + // @ts-ignore + { + [linkId]: dexAdapter.address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }, + setControllerAddress, + basicIssuanceModuleAddress, + aaveLeveragedModuleAddress, + aaveAddressProviderAddress, + BalancerV2VaultAddress + ); + } + + public async deployExchangeIssuanceLeveragedForCompound( wethAddress: Address, quickRouterAddress: Address, diff --git a/utils/test/testingUtils.ts b/utils/test/testingUtils.ts index bc9b6d52..4c8e3585 100644 --- a/utils/test/testingUtils.ts +++ b/utils/test/testingUtils.ts @@ -112,7 +112,7 @@ export async function impersonateAccount(address: string): Promise { method: "hardhat_impersonateAccount", params: [address], }); - return await ethers.getSigner(address); + return await ethers.provider.getSigner(address); } export function setBlockNumber(blockNumber: number) {