From e0cf00391f2cd5b656866f7e4c6ecfa306595b47 Mon Sep 17 00:00:00 2001 From: christn Date: Sat, 1 Jun 2024 14:56:09 +0800 Subject: [PATCH] feat: HyETH Flashmint contract (#173) * First version of FlashMintHyETH * Add tests * First test succesfully issuing hyETH with only instadapp component * Succesfully redeeming * Start working on pendle integration * More pendle integration * Swap suceeding * Succesfully issuing from pendle * redemption from pendle working * Add rsETH pendle position to tests * Add rswETH position to tests * Add Across WETH LP token * Contract code cleanup * Styling and docs * Factor out _depositIntoComponent * add issueExactSetFromERC20 method * add tests issuing from erc20 * factor out _swapFromEthToToken * more refactoring * Undo changes to prettierrc * Add docstring * Add array of swapdata for non-standard components * Adjust tests * Adjust swap logic for non standard components * Run FlashMintHyETH tests in CI (#174) * shift .only to latest contract * remove TODO comments --------- Co-authored-by: pblivin0x <84149824+pblivin0x@users.noreply.github.com> Co-authored-by: Pranav Bhardwaj --- contracts/exchangeIssuance/DEXAdapterV2.sol | 802 ++++++++++++++++++ contracts/exchangeIssuance/FlashMintHyETH.sol | 640 ++++++++++++++ contracts/interfaces/IERC4626.sol | 13 + .../interfaces/external/IAcrossHubPoolV2.sol | 97 +++ .../interfaces/external/IPendleMarketV3.sol | 86 ++ .../external/IPendlePrincipalToken.sol | 39 + .../external/IPendleStandardizedYield.sol | 73 ++ contracts/interfaces/external/IStETH.sol | 6 + package.json | 4 +- test/integration/ethereum/addresses.ts | 16 + .../ethereum/flashMintHyETH.spec.ts | 433 ++++++++++ ...imisticAuctionRebalanceExtenisonV1.spec.ts | 2 +- utils/config.ts | 2 +- utils/contracts/index.ts | 1 + utils/deploys/deployExtensions.ts | 51 ++ yarn.lock | 31 + 16 files changed, 2293 insertions(+), 3 deletions(-) create mode 100644 contracts/exchangeIssuance/DEXAdapterV2.sol create mode 100644 contracts/exchangeIssuance/FlashMintHyETH.sol create mode 100644 contracts/interfaces/IERC4626.sol create mode 100644 contracts/interfaces/external/IAcrossHubPoolV2.sol create mode 100644 contracts/interfaces/external/IPendleMarketV3.sol create mode 100644 contracts/interfaces/external/IPendlePrincipalToken.sol create mode 100644 contracts/interfaces/external/IPendleStandardizedYield.sol create mode 100644 contracts/interfaces/external/IStETH.sol create mode 100644 test/integration/ethereum/flashMintHyETH.spec.ts diff --git a/contracts/exchangeIssuance/DEXAdapterV2.sol b/contracts/exchangeIssuance/DEXAdapterV2.sol new file mode 100644 index 00000000..894a61f6 --- /dev/null +++ b/contracts/exchangeIssuance/DEXAdapterV2.sol @@ -0,0 +1,802 @@ +/* + 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 { IUniswapV2Router02 } from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ICurveCalculator } from "../interfaces/external/ICurveCalculator.sol"; +import { ICurveAddressProvider } from "../interfaces/external/ICurveAddressProvider.sol"; +import { ICurvePoolRegistry } from "../interfaces/external/ICurvePoolRegistry.sol"; +import { ICurvePool } from "../interfaces/external/ICurvePool.sol"; +import { ISwapRouter02 } from "../interfaces/external/ISwapRouter02.sol"; +import { IQuoter } from "../interfaces/IQuoter.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + + +/** + * @title DEXAdapterV2 + * @author Index Coop + * + * Same as DEXAdapter but without automatic WETH deposit / withdraw + */ +library DEXAdapterV2 { + using SafeERC20 for IERC20; + using PreciseUnitMath for uint256; + using SafeMath for uint256; + + /* ============ Constants ============= */ + + uint256 constant private MAX_UINT256 = type(uint256).max; + address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 public constant ROUNDING_ERROR_MARGIN = 2; + + /* ============ Enums ============ */ + + enum Exchange { None, Quickswap, Sushiswap, UniV3, Curve } + + /* ============ Structs ============ */ + + struct Addresses { + address quickRouter; + address sushiRouter; + address uniV3Router; + address uniV3Quoter; + address curveAddressProvider; + address curveCalculator; + // Wrapped native token (WMATIC on polygon) + address weth; + } + + struct SwapData { + address[] path; + uint24[] fees; + address pool; + Exchange exchange; + } + + struct CurvePoolData { + int128 nCoins; + uint256[8] balances; + uint256 A; + uint256 fee; + uint256[8] rates; + uint256[8] decimals; + } + + /** + * Swap exact tokens for another token on a given DEX. + * + * @param _addresses Struct containing relevant smart contract addresses. + * @param _amountIn The amount of input token to be spent + * @param _minAmountOut Minimum amount of output token to receive + * @param _swapData Swap data containing the path and fee levels (latter only used for uniV3) + * + * @return amountOut The amount of output tokens + */ + function swapExactTokensForTokens( + Addresses memory _addresses, + uint256 _amountIn, + uint256 _minAmountOut, + SwapData memory _swapData + ) + external + returns (uint256) + { + if (_swapData.path.length == 0 || _swapData.path[0] == _swapData.path[_swapData.path.length -1]) { + return _amountIn; + } + + if(_swapData.exchange == Exchange.Curve){ + return _swapExactTokensForTokensCurve( + _swapData.path, + _swapData.pool, + _amountIn, + _minAmountOut, + _addresses + ); + } + if(_swapData.exchange== Exchange.UniV3){ + return _swapExactTokensForTokensUniV3( + _swapData.path, + _swapData.fees, + _amountIn, + _minAmountOut, + ISwapRouter02(_addresses.uniV3Router) + ); + } else { + return _swapExactTokensForTokensUniV2( + _swapData.path, + _amountIn, + _minAmountOut, + _getRouter(_swapData.exchange, _addresses) + ); + } + } + + + /** + * Swap tokens for exact amount of output tokens on a given DEX. + * + * @param _addresses Struct containing relevant smart contract addresses. + * @param _amountOut The amount of output token required + * @param _maxAmountIn Maximum amount of input token to be spent + * @param _swapData Swap data containing the path and fee levels (latter only used for uniV3) + * + * @return amountIn The amount of input tokens spent + */ + function swapTokensForExactTokens( + Addresses memory _addresses, + uint256 _amountOut, + uint256 _maxAmountIn, + SwapData memory _swapData + ) + external + returns (uint256 amountIn) + { + if (_swapData.path.length == 0 || _swapData.path[0] == _swapData.path[_swapData.path.length -1]) { + return _amountOut; + } + + if(_swapData.exchange == Exchange.Curve){ + return _swapTokensForExactTokensCurve( + _swapData.path, + _swapData.pool, + _amountOut, + _maxAmountIn, + _addresses + ); + } + if(_swapData.exchange == Exchange.UniV3){ + return _swapTokensForExactTokensUniV3( + _swapData.path, + _swapData.fees, + _amountOut, + _maxAmountIn, + ISwapRouter02(_addresses.uniV3Router) + ); + } else { + return _swapTokensForExactTokensUniV2( + _swapData.path, + _amountOut, + _maxAmountIn, + _getRouter(_swapData.exchange, _addresses) + ); + } + } + + /** + * Gets the output amount of a token swap. + * + * @param _swapData the swap parameters + * @param _addresses Struct containing relevant smart contract addresses. + * @param _amountIn the input amount of the trade + * + * @return the output amount of the swap + */ + function getAmountOut( + Addresses memory _addresses, + SwapData memory _swapData, + uint256 _amountIn + ) + external + returns (uint256) + { + if (_swapData.path.length == 0 || _swapData.path[0] == _swapData.path[_swapData.path.length-1]) { + return _amountIn; + } + + if (_swapData.exchange == Exchange.UniV3) { + return _getAmountOutUniV3(_swapData, _addresses.uniV3Quoter, _amountIn); + } else if (_swapData.exchange == Exchange.Curve) { + (int128 i, int128 j) = _getCoinIndices( + _swapData.pool, + _swapData.path[0], + _swapData.path[1], + ICurveAddressProvider(_addresses.curveAddressProvider) + ); + return _getAmountOutCurve(_swapData.pool, i, j, _amountIn, _addresses); + } else { + return _getAmountOutUniV2( + _swapData, + _getRouter(_swapData.exchange, _addresses), + _amountIn + ); + } + } + + /** + * Gets the input amount of a fixed output swap. + * + * @param _swapData the swap parameters + * @param _addresses Struct containing relevant smart contract addresses. + * @param _amountOut the output amount of the swap + * + * @return the input amount of the swap + */ + function getAmountIn( + Addresses memory _addresses, + SwapData memory _swapData, + uint256 _amountOut + ) + external + returns (uint256) + { + if (_swapData.path.length == 0 || _swapData.path[0] == _swapData.path[_swapData.path.length-1]) { + return _amountOut; + } + + if (_swapData.exchange == Exchange.UniV3) { + return _getAmountInUniV3(_swapData, _addresses.uniV3Quoter, _amountOut); + } else if (_swapData.exchange == Exchange.Curve) { + (int128 i, int128 j) = _getCoinIndices( + _swapData.pool, + _swapData.path[0], + _swapData.path[1], + ICurveAddressProvider(_addresses.curveAddressProvider) + ); + return _getAmountInCurve(_swapData.pool, i, j, _amountOut, _addresses); + } else { + return _getAmountInUniV2( + _swapData, + _getRouter(_swapData.exchange, _addresses), + _amountOut + ); + } + } + + /** + * 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); + } + } + + /* ============ Private Methods ============ */ + + /** + * Execute exact output swap via a UniV2 based DEX. (such as sushiswap); + * + * @param _path List of token address to swap via. + * @param _amountOut The amount of output token required + * @param _maxAmountIn Maximum amount of input token to be spent + * @param _router Address of the uniV2 router to use + * + * @return amountIn The amount of input tokens spent + */ + function _swapTokensForExactTokensUniV2( + address[] memory _path, + uint256 _amountOut, + uint256 _maxAmountIn, + IUniswapV2Router02 _router + ) + private + returns (uint256) + { + _safeApprove(IERC20(_path[0]), address(_router), _maxAmountIn); + return _router.swapTokensForExactTokens(_amountOut, _maxAmountIn, _path, address(this), block.timestamp)[0]; + } + + /** + * Execute exact output swap via UniswapV3 + * + * @param _path List of token address to swap via. (In the order as + * expected by uniV2, the first element being the input toen) + * @param _fees List of fee levels identifying the pools to swap via. + * (_fees[0] refers to pool between _path[0] and _path[1]) + * @param _amountOut The amount of output token required + * @param _maxAmountIn Maximum amount of input token to be spent + * @param _uniV3Router Address of the uniswapV3 router + * + * @return amountIn The amount of input tokens spent + */ + function _swapTokensForExactTokensUniV3( + address[] memory _path, + uint24[] memory _fees, + uint256 _amountOut, + uint256 _maxAmountIn, + ISwapRouter02 _uniV3Router + ) + private + returns(uint256) + { + + require(_path.length == _fees.length + 1, "ExchangeIssuance: PATHS_FEES_MISMATCH"); + _safeApprove(IERC20(_path[0]), address(_uniV3Router), _maxAmountIn); + if(_path.length == 2){ + ISwapRouter02.ExactOutputSingleParams memory params = + ISwapRouter02.ExactOutputSingleParams({ + tokenIn: _path[0], + tokenOut: _path[1], + fee: _fees[0], + recipient: address(this), + amountOut: _amountOut, + amountInMaximum: _maxAmountIn, + sqrtPriceLimitX96: 0 + }); + return _uniV3Router.exactOutputSingle(params); + } else { + bytes memory pathV3 = _encodePathV3(_path, _fees, true); + ISwapRouter02.ExactOutputParams memory params = + ISwapRouter02.ExactOutputParams({ + path: pathV3, + recipient: address(this), + amountOut: _amountOut, + amountInMaximum: _maxAmountIn + }); + return _uniV3Router.exactOutput(params); + } + } + + /** + * Execute exact input swap via Curve + * + * @param _path Path (has to be of length 2) + * @param _pool Address of curve pool to use + * @param _amountIn The amount of input token to be spent + * @param _minAmountOut Minimum amount of output token to receive + * @param _addresses Struct containing relevant smart contract addresses. + * + * @return amountOut The amount of output token obtained + */ + function _swapExactTokensForTokensCurve( + address[] memory _path, + address _pool, + uint256 _amountIn, + uint256 _minAmountOut, + Addresses memory _addresses + ) + private + returns (uint256 amountOut) + { + require(_path.length == 2, "ExchangeIssuance: CURVE_WRONG_PATH_LENGTH"); + (int128 i, int128 j) = _getCoinIndices(_pool, _path[0], _path[1], ICurveAddressProvider(_addresses.curveAddressProvider)); + + amountOut = _exchangeCurve(i, j, _pool, _amountIn, _minAmountOut, _path[0]); + + } + + /** + * Execute exact output swap via Curve + * + * @param _path Path (has to be of length 2) + * @param _pool Address of curve pool to use + * @param _amountOut The amount of output token required + * @param _maxAmountIn Maximum amount of input token to be spent + * + * @return amountOut The amount of output token obtained + */ + function _swapTokensForExactTokensCurve( + address[] memory _path, + address _pool, + uint256 _amountOut, + uint256 _maxAmountIn, + Addresses memory _addresses + ) + private + returns (uint256) + { + require(_path.length == 2, "ExchangeIssuance: CURVE_WRONG_PATH_LENGTH"); + (int128 i, int128 j) = _getCoinIndices(_pool, _path[0], _path[1], ICurveAddressProvider(_addresses.curveAddressProvider)); + + + uint256 returnedAmountOut = _exchangeCurve(i, j, _pool, _maxAmountIn, _amountOut, _path[0]); + require(_amountOut <= returnedAmountOut, "ExchangeIssuance: CURVE_UNDERBOUGHT"); + + uint256 swappedBackAmountIn; + if(returnedAmountOut > _amountOut){ + swappedBackAmountIn = _exchangeCurve(j, i, _pool, returnedAmountOut.sub(_amountOut), 0, _path[1]); + } + + return _maxAmountIn.sub(swappedBackAmountIn); + } + + function _exchangeCurve( + int128 _i, + int128 _j, + address _pool, + uint256 _amountIn, + uint256 _minAmountOut, + address _from + ) + private + returns (uint256 amountOut) + { + ICurvePool pool = ICurvePool(_pool); + if(_from == ETH_ADDRESS){ + amountOut = pool.exchange{value: _amountIn}( + _i, + _j, + _amountIn, + _minAmountOut + ); + } + else { + IERC20(_from).approve(_pool, _amountIn); + amountOut = pool.exchange( + _i, + _j, + _amountIn, + _minAmountOut + ); + } + } + + /** + * Calculate required input amount to get a given output amount via Curve swap + * + * @param _i Index of input token as per the ordering of the pools tokens + * @param _j Index of output token as per the ordering of the pools tokens + * @param _pool Address of curve pool to use + * @param _amountOut The amount of output token to be received + * @param _addresses Struct containing relevant smart contract addresses. + * + * @return amountOut The amount of output token obtained + */ + function _getAmountInCurve( + address _pool, + int128 _i, + int128 _j, + uint256 _amountOut, + Addresses memory _addresses + ) + private + view + returns (uint256) + { + CurvePoolData memory poolData = _getCurvePoolData(_pool, ICurveAddressProvider(_addresses.curveAddressProvider)); + + return ICurveCalculator(_addresses.curveCalculator).get_dx( + poolData.nCoins, + poolData.balances, + poolData.A, + poolData.fee, + poolData.rates, + poolData.decimals, + false, + _i, + _j, + _amountOut + ) + ROUNDING_ERROR_MARGIN; + } + + /** + * Calculate output amount of a Curve swap + * + * @param _i Index of input token as per the ordering of the pools tokens + * @param _j Index of output token as per the ordering of the pools tokens + * @param _pool Address of curve pool to use + * @param _amountIn The amount of output token to be received + * @param _addresses Struct containing relevant smart contract addresses. + * + * @return amountOut The amount of output token obtained + */ + function _getAmountOutCurve( + address _pool, + int128 _i, + int128 _j, + uint256 _amountIn, + Addresses memory _addresses + ) + private + view + returns (uint256) + { + return ICurvePool(_pool).get_dy(_i, _j, _amountIn); + } + + /** + * Get metadata on curve pool required to calculate input amount from output amount + * + * @param _pool Address of curve pool to use + * @param _curveAddressProvider Address of curve address provider + * + * @return Struct containing all required data to perform getAmountInCurve calculation + */ + function _getCurvePoolData( + address _pool, + ICurveAddressProvider _curveAddressProvider + ) private view returns(CurvePoolData memory) + { + ICurvePoolRegistry registry = ICurvePoolRegistry(_curveAddressProvider.get_registry()); + + return CurvePoolData( + int128(registry.get_n_coins(_pool)[0]), + registry.get_balances(_pool), + registry.get_A(_pool), + registry.get_fees(_pool)[0], + registry.get_rates(_pool), + registry.get_decimals(_pool) + ); + } + + /** + * Get token indices for given pool + * NOTE: This was necessary sine the get_coin_indices function of the CurvePoolRegistry did not work for StEth/ETH pool + * + * @param _pool Address of curve pool to use + * @param _from Address of input token + * @param _to Address of output token + * @param _curveAddressProvider Address of curve address provider + * + * @return i Index of input token + * @return j Index of output token + */ + function _getCoinIndices( + address _pool, + address _from, + address _to, + ICurveAddressProvider _curveAddressProvider + ) + private + view + returns (int128 i, int128 j) + { + ICurvePoolRegistry registry = ICurvePoolRegistry(_curveAddressProvider.get_registry()); + + // Set to out of range index to signal the coin is not found yet + i = 9; + j = 9; + address[8] memory poolCoins = registry.get_coins(_pool); + + for(uint256 k = 0; k < 8; k++){ + if(poolCoins[k] == _from){ + i = int128(k); + } + else if(poolCoins[k] == _to){ + j = int128(k); + } + // ZeroAddress signals end of list + if(poolCoins[k] == address(0) || (i != 9 && j != 9)){ + break; + } + } + + require(i != 9, "ExchangeIssuance: CURVE_FROM_NOT_FOUND"); + require(j != 9, "ExchangeIssuance: CURVE_TO_NOT_FOUND"); + + return (i, j); + } + + /** + * Execute exact input swap via UniswapV3 + * + * @param _path List of token address to swap via. + * @param _fees List of fee levels identifying the pools to swap via. + * (_fees[0] refers to pool between _path[0] and _path[1]) + * @param _amountIn The amount of input token to be spent + * @param _minAmountOut Minimum amount of output token to receive + * @param _uniV3Router Address of the uniswapV3 router + * + * @return amountOut The amount of output token obtained + */ + function _swapExactTokensForTokensUniV3( + address[] memory _path, + uint24[] memory _fees, + uint256 _amountIn, + uint256 _minAmountOut, + ISwapRouter02 _uniV3Router + ) + private + returns (uint256) + { + require(_path.length == _fees.length + 1, "ExchangeIssuance: PATHS_FEES_MISMATCH"); + _safeApprove(IERC20(_path[0]), address(_uniV3Router), _amountIn); + if(_path.length == 2){ + ISwapRouter02.ExactInputSingleParams memory params = + ISwapRouter02.ExactInputSingleParams({ + tokenIn: _path[0], + tokenOut: _path[1], + fee: _fees[0], + recipient: address(this), + amountIn: _amountIn, + amountOutMinimum: _minAmountOut, + sqrtPriceLimitX96: 0 + }); + return _uniV3Router.exactInputSingle(params); + } else { + bytes memory pathV3 = _encodePathV3(_path, _fees, false); + ISwapRouter02.ExactInputParams memory params = + ISwapRouter02.ExactInputParams({ + path: pathV3, + recipient: address(this), + amountIn: _amountIn, + amountOutMinimum: _minAmountOut + }); + uint amountOut = _uniV3Router.exactInput(params); + return amountOut; + } + } + + /** + * Execute exact input swap via UniswapV2 + * + * @param _path List of token address to swap via. + * @param _amountIn The amount of input token to be spent + * @param _minAmountOut Minimum amount of output token to receive + * @param _router Address of uniV2 router to use + * + * @return amountOut The amount of output token obtained + */ + function _swapExactTokensForTokensUniV2( + address[] memory _path, + uint256 _amountIn, + uint256 _minAmountOut, + IUniswapV2Router02 _router + ) + private + returns (uint256) + { + _safeApprove(IERC20(_path[0]), address(_router), _amountIn); + // NOTE: The following was changed from always returning result at position [1] to returning the last element of the result array + // With this change, the actual output is correctly returned also for multi-hop swaps + // See https://github.com/IndexCoop/index-coop-smart-contracts/pull/116 + uint256[] memory result = _router.swapExactTokensForTokens(_amountIn, _minAmountOut, _path, address(this), block.timestamp); + // result = uint[] memory The input token amount and all subsequent output token amounts. + // we are usually only interested in the actual amount of the output token (so result element at the last place) + return result[result.length-1]; + } + + /** + * Gets the output amount of a token swap on Uniswap V2 + * + * @param _swapData the swap parameters + * @param _router the uniswap v2 router address + * @param _amountIn the input amount of the trade + * + * @return the output amount of the swap + */ + function _getAmountOutUniV2( + SwapData memory _swapData, + IUniswapV2Router02 _router, + uint256 _amountIn + ) + private + view + returns (uint256) + { + return _router.getAmountsOut(_amountIn, _swapData.path)[_swapData.path.length-1]; + } + + /** + * Gets the input amount of a fixed output swap on Uniswap V2. + * + * @param _swapData the swap parameters + * @param _router the uniswap v2 router address + * @param _amountOut the output amount of the swap + * + * @return the input amount of the swap + */ + function _getAmountInUniV2( + SwapData memory _swapData, + IUniswapV2Router02 _router, + uint256 _amountOut + ) + private + view + returns (uint256) + { + return _router.getAmountsIn(_amountOut, _swapData.path)[0]; + } + + /** + * Gets the output amount of a token swap on Uniswap V3. + * + * @param _swapData the swap parameters + * @param _quoter the uniswap v3 quoter + * @param _amountIn the input amount of the trade + * + * @return the output amount of the swap + */ + + function _getAmountOutUniV3( + SwapData memory _swapData, + address _quoter, + uint256 _amountIn + ) + private + returns (uint256) + { + bytes memory path = _encodePathV3(_swapData.path, _swapData.fees, false); + return IQuoter(_quoter).quoteExactInput(path, _amountIn); + } + + /** + * Gets the input amount of a fixed output swap on Uniswap V3. + * + * @param _swapData the swap parameters + * @param _quoter uniswap v3 quoter + * @param _amountOut the output amount of the swap + * + * @return the input amount of the swap + */ + function _getAmountInUniV3( + SwapData memory _swapData, + address _quoter, + uint256 _amountOut + ) + private + returns (uint256) + { + bytes memory path = _encodePathV3(_swapData.path, _swapData.fees, true); + return IQuoter(_quoter).quoteExactOutput(path, _amountOut); + } + + /** + * Encode path / fees to bytes in the format expected by UniV3 router + * + * @param _path List of token address to swap via (starting with input token) + * @param _fees List of fee levels identifying the pools to swap via. + * (_fees[0] refers to pool between _path[0] and _path[1]) + * @param _reverseOrder Boolean indicating if path needs to be reversed to start with output token. + * (which is the case for exact output swap) + * + * @return encodedPath Encoded path to be forwared to uniV3 router + */ + function _encodePathV3( + address[] memory _path, + uint24[] memory _fees, + bool _reverseOrder + ) + private + pure + returns(bytes memory encodedPath) + { + if(_reverseOrder){ + encodedPath = abi.encodePacked(_path[_path.length-1]); + for(uint i = 0; i < _fees.length; i++){ + uint index = _fees.length - i - 1; + encodedPath = abi.encodePacked(encodedPath, _fees[index], _path[index]); + } + } else { + encodedPath = abi.encodePacked(_path[0]); + for(uint i = 0; i < _fees.length; i++){ + encodedPath = abi.encodePacked(encodedPath, _fees[i], _path[i+1]); + } + } + } + + function _getRouter( + Exchange _exchange, + Addresses memory _addresses + ) + private + pure + returns (IUniswapV2Router02) + { + return IUniswapV2Router02( + (_exchange == Exchange.Quickswap) ? _addresses.quickRouter : _addresses.sushiRouter + ); + } +} diff --git a/contracts/exchangeIssuance/FlashMintHyETH.sol b/contracts/exchangeIssuance/FlashMintHyETH.sol new file mode 100644 index 00000000..8d5366f0 --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintHyETH.sol @@ -0,0 +1,640 @@ +/* + Copyright 2024 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 { 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 { IERC4626 } from "../interfaces/IERC4626.sol"; +import { IStETH } from "../interfaces/external/IStETH.sol"; +import { IAcrossHubPoolV2 } from "../interfaces/external/IAcrossHubPoolV2.sol"; +import { IPendlePrincipalToken } from "../interfaces/external/IPendlePrincipalToken.sol"; +import { IPendleMarketV3 } from "../interfaces/external/IPendleMarketV3.sol"; +import { IPendleStandardizedYield } from "../interfaces/external/IPendleStandardizedYield.sol"; +import { IController } from "../interfaces/IController.sol"; +import { IDebtIssuanceModule } from "../interfaces/IDebtIssuanceModule.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { DEXAdapterV2 } from "./DEXAdapterV2.sol"; + +/** + * @title FlashMintHyETH + */ +contract FlashMintHyETH is Ownable, ReentrancyGuard { + using DEXAdapterV2 for DEXAdapterV2.Addresses; + using Address for address payable; + using Address for address; + using SafeMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for ISetToken; + + struct PendleMarketData { + IPendlePrincipalToken pt; + IPendleStandardizedYield sy; + address underlying; + } + /* ============ Constants ============= */ + + uint256 private constant MAX_UINT256 = type(uint256).max; + uint256 public constant ROUNDING_ERROR = 10; + IERC20 public constant acrossToken = IERC20(0x28F77208728B0A45cAb24c4868334581Fe86F95B); + IAcrossHubPoolV2 public constant acrossPool = + IAcrossHubPoolV2(0xc186fA914353c44b2E33eBE05f21846F1048bEda); + /* ============ Immutables ============ */ + + IController public immutable setController; + IStETH public immutable stETH; + IDebtIssuanceModule public immutable issuanceModule; // interface is compatible with DebtIssuanceModuleV2 + mapping(IPendlePrincipalToken => IPendleMarketV3) public pendleMarkets; + mapping(IPendleMarketV3 => PendleMarketData) public pendleMarketData; + mapping(address => mapping(address => DEXAdapterV2.SwapData)) public swapData; + + /* ============ State Variables ============ */ + + DEXAdapterV2.Addresses public dexAdapter; + + /* ============ Events ============ */ + + event FlashMint( + address indexed _recipient, // The recipient address of the minted Set token + ISetToken indexed _setToken, // The minted Set token + IERC20 indexed _inputToken, // The address of the input asset(ERC20/ETH) used to mint the Set tokens + uint256 _amountInputToken, // The amount of input tokens used for minting + uint256 _amountSetIssued // The amount of Set tokens received by the recipient + ); + + event FlashRedeem( + address indexed _recipient, // The recipient address which redeemed the Set token + ISetToken indexed _setToken, // The redeemed Set token + IERC20 indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient + uint256 _amountSetRedeemed, // The amount of Set token redeemed for output tokens + uint256 _amountOutputToken // The amount of output tokens received by the recipient + ); + + /* ============ Modifiers ============ */ + + /** + * checks that _setToken is a valid listed set token on the setController + * + * @param _setToken set token to check + */ + modifier isSetToken(ISetToken _setToken) { + require(setController.isSet(address(_setToken)), "FlashMint: INVALID_SET"); + _; + } + + /** + * checks that _inputToken is the first adress in _path and _outputToken is the last address in _path + * + * @param _path Array of addresses for a DEX swap path + * @param _inputToken input token of DEX swap + * @param _outputToken output token of DEX swap + */ + modifier isValidPath( + address[] memory _path, + address _inputToken, + address _outputToken + ) { + if (_inputToken != _outputToken) { + require( + _path[0] == _inputToken || + (_inputToken == dexAdapter.weth && _path[0] == DEXAdapterV2.ETH_ADDRESS), + "FlashMint: INPUT_TOKEN_NOT_IN_PATH" + ); + require( + _path[_path.length - 1] == _outputToken || + (_outputToken == dexAdapter.weth && + _path[_path.length - 1] == DEXAdapterV2.ETH_ADDRESS), + "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH" + ); + } + _; + } + + /* ========== Constructor ========== */ + + constructor( + DEXAdapterV2.Addresses memory _dexAddresses, + IController _setController, + IDebtIssuanceModule _issuanceModule, + IStETH _stETH, + address _stEthETHPool + ) public { + dexAdapter = _dexAddresses; + setController = _setController; + issuanceModule = _issuanceModule; + stETH = _stETH; + + IERC20(address(_stETH)).approve(_stEthETHPool, MAX_UINT256); + } + + /* ============ External Functions (Publicly Accesible) ============ */ + + /** + * 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 isSetToken(_setToken) { + address[] memory _components = _setToken.getComponents(); + for (uint256 i = 0; i < _components.length; ++i) { + IERC20(_components[i]).approve(address(issuanceModule), MAX_UINT256); + } + _setToken.approve(address(issuanceModule), MAX_UINT256); + } + + + /** + * Issue exact amout of SetToken from ETH + * + * @param _setToken Address of the SetToken to issue + * @param _amountSetToken Amount of SetToken to issue + */ + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _amountSetToken, + DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + ) external payable nonReentrant returns (uint256) { + uint256 ethSpent = _issueExactSetFromEth(_setToken, _amountSetToken, _swapDataEthToComponent); + msg.sender.sendValue(msg.value.sub(ethSpent)); + return ethSpent; + } + + /** + * Issue exact amout of SetToken from ERC20 token + * + * @param _setToken Address of the SetToken to issue + * @param _amountSetToken Amount of SetToken to issue + * @param _inputToken Address of the input token + * @param _maxInputTokenAmount Maximum amount of input token to spend + * @param _swapDataInputTokenToEth Swap data from input token to ETH + * @param _swapDataEthToInputToken Swap data from ETH to input token (used to swap back the leftover eth) + */ + function issueExactSetFromERC20( + ISetToken _setToken, + uint256 _amountSetToken, + IERC20 _inputToken, + uint256 _maxInputTokenAmount, + DEXAdapterV2.SwapData memory _swapDataInputTokenToEth, + DEXAdapterV2.SwapData memory _swapDataEthToInputToken, + DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + ) external payable nonReentrant returns (uint256) { + _inputToken.safeTransferFrom(msg.sender, address(this), _maxInputTokenAmount); + + uint256 ethAmount = _swapFromTokenToEth(_inputToken, _maxInputTokenAmount, _swapDataInputTokenToEth); + ethAmount = ethAmount.sub(_issueExactSetFromEth(_setToken, _amountSetToken, _swapDataEthToComponent)); + + uint256 inputTokenLeft = _swapFromEthToToken(_inputToken, ethAmount, _swapDataEthToInputToken); + + _inputToken.safeTransfer(msg.sender, inputTokenLeft); + return _maxInputTokenAmount.sub(inputTokenLeft); + } + + /** + * Redeem exact amount of SetToken for ETH + * + * @param _setToken Address of the SetToken to redeem + * @param _amountSetToken Amount of SetToken to redeem + * @param _minETHOut Minimum amount of ETH to receive (tx will revert if actual amount is less) + * @param _swapDataComponentToEth Swap data from component to ETH (for non standard components) + */ + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _amountSetToken, + uint256 _minETHOut, + DEXAdapterV2.SwapData[] memory _swapDataComponentToEth + ) external payable nonReentrant returns (uint256) { + uint256 ethObtained = _redeemExactSetForETH(_setToken, _amountSetToken, _minETHOut, _swapDataComponentToEth); + require(ethObtained >= _minETHOut, "FlashMint: INSUFFICIENT_OUTPUT"); + msg.sender.sendValue(ethObtained); + return ethObtained; + } + + /** + * Redeem exact amount of SetToken for ERC20 + * + * @param _setToken Address of the SetToken to redeem + * @param _amountSetToken Amount of SetToken to redeem + * @param _outputToken Address of the output token + * @param _minOutputTokenAmount Minimum amount of output token to receive (tx will revert if actual amount is less) + * @param _swapDataEthToOutputToken Swap data from ETH to output token + * @param _swapDataComponentToEth Swap data from component to ETH (for non standard components) + */ + function redeemExactSetForERC20( + ISetToken _setToken, + uint256 _amountSetToken, + IERC20 _outputToken, + uint256 _minOutputTokenAmount, + DEXAdapterV2.SwapData memory _swapDataEthToOutputToken, + DEXAdapterV2.SwapData[] memory _swapDataComponentToEth + ) external payable nonReentrant returns (uint256) { + uint256 ethObtained = _redeemExactSetForETH(_setToken, _amountSetToken, 0, _swapDataComponentToEth); + uint256 outputTokenAmount = _swapFromEthToToken(_outputToken, ethObtained, _swapDataEthToOutputToken); + require(outputTokenAmount >= _minOutputTokenAmount, "FlashMint: INSUFFICIENT_OUTPUT"); + _outputToken.safeTransfer(msg.sender, outputTokenAmount); + return outputTokenAmount; + } + + + receive() external payable {} + + /* ============ External Functions (Access controlled) ============ */ + + /** + * Approve spender to spend specific token on behalf of this contract + * + * @param _token Address of the token to approve + * @param _spender Address of the spender + * @param _allowance Amount to approve + */ + function approveToken(IERC20 _token, address _spender, uint256 _allowance) external onlyOwner { + _token.approve(_spender, _allowance); + } + + /** + * Withdraw slippage to selected address + * + * @param _tokens Addresses of tokens to withdraw, specifiy ETH_ADDRESS to withdraw ETH + * @param _to Address to send the tokens to + */ + function withdrawTokens( + IERC20[] calldata _tokens, + address payable _to + ) external payable onlyOwner { + for (uint256 i = 0; i < _tokens.length; i++) { + if (address(_tokens[i]) == DEXAdapterV2.ETH_ADDRESS) { + _to.sendValue(address(this).balance); + } else { + _tokens[i].safeTransfer(_to, _tokens[i].balanceOf(address(this))); + } + } + } + + + /** + * Set swap data for specific token pair + * + * @param _inputToken Address of the input token + * @param _outputToken Address of the output token + * @param _swapData Swap data for the token pair describing DEX / route + */ + function setSwapData( + address _inputToken, + address _outputToken, + DEXAdapterV2.SwapData memory _swapData + ) external onlyOwner { + swapData[_inputToken][_outputToken] = _swapData; + } + + /** + * Set Pendle Market to use for specific pt including relevant metadata + * + * @param _pt Address of the Pendle Principal Token + * @param _sy Address of the corresponding Standardized Yield Token + * @param _underlying Address of the underlying token to redeem to + * @param _market Address of the Pendle Market to use for swapping between pt and sy + */ + function setPendleMarket( + IPendlePrincipalToken _pt, + IPendleStandardizedYield _sy, + address _underlying, + IPendleMarketV3 _market + ) external onlyOwner { + pendleMarkets[_pt] = _market; + pendleMarketData[_market] = PendleMarketData({ pt: _pt, sy: _sy, underlying: _underlying }); + } + + /** + * Callback method that is called by Pendle Market during the swap to request input token + * + * @param _ptToAccount Swap balance of pt token (negative -> swapping pt to sy) + * @param _syToAccount Swap balance of sy token (negative -> swapping sy to pt) + * @param _data Arbitrary data passed by Pendle Market (not used) + */ + function swapCallback(int256 _ptToAccount, int256 _syToAccount, bytes calldata _data) external { + PendleMarketData storage marketData = pendleMarketData[IPendleMarketV3(msg.sender)]; + require(address(marketData.sy) != address(0), "ISC"); + if (_ptToAccount < 0) { + uint256 ptAmount = uint256(-_ptToAccount); + marketData.pt.transfer(msg.sender, ptAmount); + } else if (_syToAccount < 0) { + uint256 syAmount = uint256(-_syToAccount); + uint256 ethAmount = syAmount.mul(marketData.sy.exchangeRate()).div(1e18); + marketData.sy.deposit{ value: ethAmount }(msg.sender, address(0), ethAmount, 0); + } else { + revert("Invalid callback"); + } + } + + + /* ============ Internal ============ */ + + /** + * @dev Issue exact amount of SetToken from ETH + * + */ + function _issueExactSetFromEth( + ISetToken _setToken, + uint256 _amountSetToken, + DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + ) internal returns (uint256) { + (address[] memory components, uint256[] memory positions, ) = IDebtIssuanceModule( + issuanceModule + ).getRequiredComponentIssuanceUnits(_setToken, _amountSetToken); + uint256 ethBalanceBefore = address(this).balance; + for (uint256 i = 0; i < components.length; i++) { + _depositIntoComponent(components[i], positions[i], _swapDataEthToComponent[i]); + } + issuanceModule.issue(_setToken, _amountSetToken, msg.sender); + return ethBalanceBefore.sub(address(this).balance); + } + + /** + * @dev Redeem exact amount of SetToken for ETH + * + */ + function _redeemExactSetForETH( + ISetToken _setToken, + uint256 _amountSetToken, + uint256 _minETHOut, + DEXAdapterV2.SwapData[] memory _swapDataComponentToEth + ) internal returns (uint256) { + uint256 ethBalanceBefore = address(this).balance; + + _setToken.safeTransferFrom(msg.sender, address(this), _amountSetToken); + issuanceModule.redeem(_setToken, _amountSetToken, address(this)); + (address[] memory components, uint256[] memory positions, ) = IDebtIssuanceModule( + issuanceModule + ).getRequiredComponentRedemptionUnits(_setToken, _amountSetToken); + + for (uint256 i = 0; i < components.length; i++) { + _withdrawFromComponent(components[i], positions[i], _swapDataComponentToEth[i]); + } + + return address(this).balance.sub(ethBalanceBefore); + } + + /** + * @dev Deposit ETH into given component + * + */ + function _depositIntoComponent( + address _component, + uint256 _amount, + DEXAdapterV2.SwapData memory _swapData + ) internal { + if(_swapData.exchange != DEXAdapterV2.Exchange.None) { + require(_swapData.path.length > 1, "zero length swap path"); + require(_swapData.path[0] == DEXAdapterV2.ETH_ADDRESS || _swapData.path[0] == dexAdapter.weth, "Invalid input token"); + require(_swapData.path[_swapData.path.length - 1] == _component, "Invalid output token"); + if(_swapData.path[0] == dexAdapter.weth) { + uint256 balanceBefore = IWETH(dexAdapter.weth).balanceOf(address(this)); + IWETH(dexAdapter.weth).deposit{value: address(this).balance}(); + dexAdapter.swapTokensForExactTokens(_amount, IWETH(dexAdapter.weth).balanceOf(address(this)), _swapData); + IWETH(dexAdapter.weth).withdraw(IWETH(dexAdapter.weth).balanceOf(address(this)).sub(balanceBefore)); + } + else { + dexAdapter.swapTokensForExactTokens(_amount, address(this).balance, _swapData); + } + return; + } + if (_isInstadapp(_component)) { + _depositIntoInstadapp(IERC4626(_component), _amount); + return; + } + IPendleStandardizedYield syToken = _getSyToken(IPendlePrincipalToken(_component)); + if (syToken != IPendleStandardizedYield(address(0))) { + _depositIntoPendle(IPendlePrincipalToken(_component), _amount, syToken); + return; + } + if (IERC20(_component) == acrossToken) { + _depositIntoAcross(_amount); + return; + } + if (_component == dexAdapter.weth) { + IWETH(dexAdapter.weth).deposit{ value: _amount }(); + return; + } + revert("Missing Swapdata for non-standard component"); + } + + /** + * @dev Withdraw ETH from given component + * + */ + function _withdrawFromComponent( + address _component, + uint256 _amount, + DEXAdapterV2.SwapData memory _swapData + ) internal { + if(_swapData.exchange != DEXAdapterV2.Exchange.None) { + require(_swapData.path.length > 1, "zero length swap path"); + require(_swapData.path[0] == _component, "Invalid input token"); + require(_swapData.path[_swapData.path.length - 1] == DEXAdapterV2.ETH_ADDRESS || _swapData.path[_swapData.path.length - 1] == dexAdapter.weth, "Invalid output token"); + uint256 ethReceived = dexAdapter.swapExactTokensForTokens(_amount, 0, _swapData); + if(_swapData.path[_swapData.path.length - 1] == dexAdapter.weth) { + IWETH(dexAdapter.weth).withdraw(ethReceived); + } + return; + } + if (_isInstadapp(_component)) { + _withdrawFromInstadapp(IERC4626(_component), _amount); + return; + } + IPendleMarketV3 market = pendleMarkets[IPendlePrincipalToken(_component)]; + if (market != IPendleMarketV3(address(0))) { + _withdrawFromPendle(IPendlePrincipalToken(_component), _amount, market); + return; + } + if (IERC20(_component) == acrossToken) { + _withdrawFromAcross(_amount); + return; + } + if (_component == dexAdapter.weth) { + IWETH(dexAdapter.weth).withdraw(_amount); + return; + } + revert("Missing Swapdata for non-standard component"); + } + + /** + * @dev Deposit eth into steth and then into instadapp vault + * + */ + function _depositIntoInstadapp(IERC4626 _vault, uint256 _amount) internal { + uint256 stETHAmount = _vault.previewMint(_amount); + _depositIntoLido(stETHAmount); + _vault.mint(_amount, address(this)); + } + + /** + * @dev Deposit eth into steth + * + */ + function _depositIntoLido(uint256 _amount) internal { + stETH.submit{ value: _amount }(address(0)); + } + + /** + * @dev Withdraw steth from instadapp vault and then swap to eth + * @dev Requries the respective swap data (stETH -> ETH) to be set + * + */ + function _withdrawFromInstadapp(IERC4626 _vault, uint256 _amount) internal { + uint256 stETHAmount = _vault.redeem(_amount, address(this), address(this)); + _swapExactTokensForTokens(stETHAmount, address(stETH), address(0)); + } + + /** + * @dev Check if given component is the Instadapp vault + * + */ + function _isInstadapp(address _token) internal pure returns (bool) { + return _token == 0xA0D3707c569ff8C87FA923d3823eC5D81c98Be78; + } + + /** + * @dev Get Sy token for given pt token + * @dev Also functions as check if given component is a Pendle Principal Token + * + */ + function _getSyToken( + IPendlePrincipalToken _pt + ) internal view returns (IPendleStandardizedYield) { + return pendleMarketData[pendleMarkets[_pt]].sy; + } + + /** + * @dev Initiate deposit into pendle by swapping pt for sy + * @dev Deposit from eth to sy is done in swapCallback + */ + function _depositIntoPendle( + IPendlePrincipalToken _pt, + uint256 _ptAmount, + IPendleStandardizedYield _sy + ) internal { + // Adding random bytes here since PendleMarket will not call back if data is empty + IPendleMarketV3(pendleMarkets[_pt]).swapSyForExactPt(address(this), _ptAmount, bytes("a")); + } + + /** + * @dev Obtain across lp tokens by adding eth liquidity into the across pool + */ + function _depositIntoAcross(uint256 _acrossLpAmount) internal { + uint256 ethAmount = acrossPool + .exchangeRateCurrent(dexAdapter.weth) + .mul(_acrossLpAmount) + .div(1e18) + .add(ROUNDING_ERROR); + acrossPool.addLiquidity{ value: ethAmount }(dexAdapter.weth, ethAmount); + } + + /** + * @dev Withdraw eth by removing liquidity from across pool + */ + function _withdrawFromAcross(uint256 _acrossLpAmount) internal { + acrossPool.removeLiquidity(dexAdapter.weth, _acrossLpAmount, true); + } + + /** + * @dev Withdraw from Pendle by swapping pt for sy, redeeming sy for underlying and swapping underlying to eth + */ + function _withdrawFromPendle( + IPendlePrincipalToken _pt, + uint256 _ptAmount, + IPendleMarketV3 _pendleMarket + ) internal { + // Adding random bytes here since PendleMarket will not call back if data is empty + (uint256 syAmount, ) = _pendleMarket.swapExactPtForSy(address(this), _ptAmount, bytes("a")); + PendleMarketData storage data = pendleMarketData[_pendleMarket]; + uint256 amountUnderlying = data.sy.redeem( + address(this), + syAmount, + data.underlying, + 0, + false + ); + _swapExactTokensForTokens(amountUnderlying, data.underlying, address(0)); + IWETH(dexAdapter.weth).withdraw(IERC20(dexAdapter.weth).balanceOf(address(this))); + } + + /** + * @dev Swap exact amount of input token for output token using configured swap data + */ + function _swapExactTokensForTokens( + uint256 _amountIn, + address _inputToken, + address _outputToken + ) internal returns (uint256) { + dexAdapter.swapExactTokensForTokens(_amountIn, 0, swapData[_inputToken][_outputToken]); + } + + /** + * @dev Convert ETH to specified token, either swapping or simply depositing if outputToken is WETH + */ + function _swapFromEthToToken( + IERC20 _outputToken, + uint256 _ethAmount, + DEXAdapterV2.SwapData memory _swapDataEthToOutputToken + ) internal returns(uint256 outputTokenAmount) { + if(address(_outputToken) == address(dexAdapter.weth)) { + outputTokenAmount = _ethAmount; + IWETH(dexAdapter.weth).deposit{value: _ethAmount}(); + } else { + if(_swapDataEthToOutputToken.path[0] == dexAdapter.weth) { + IWETH(dexAdapter.weth).deposit{value: _ethAmount}(); + } + outputTokenAmount = dexAdapter.swapExactTokensForTokens( + _ethAmount, + 0, + _swapDataEthToOutputToken + ); + } + } + + /** + * @dev Convert specified token to ETH, either swapping or simply withdrawing if inputToken is WETH + */ + function _swapFromTokenToEth( + IERC20 _inputToken, + uint256 _maxInputTokenAmount, + DEXAdapterV2.SwapData memory _swapDataInputTokenToEth + ) internal returns (uint256 ethAmount) { + if(address(_inputToken) == dexAdapter.weth) { + ethAmount = _maxInputTokenAmount; + IWETH(dexAdapter.weth).withdraw(ethAmount); + } else { + ethAmount = dexAdapter.swapExactTokensForTokens( + _maxInputTokenAmount, + 0, + _swapDataInputTokenToEth + ); + if(_swapDataInputTokenToEth.path[_swapDataInputTokenToEth.path.length - 1] == dexAdapter.weth) { + IWETH(dexAdapter.weth).withdraw(ethAmount); + } + } + } + + +} diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol new file mode 100644 index 00000000..ad9a71aa --- /dev/null +++ b/contracts/interfaces/IERC4626.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; + +interface IERC4626 { + function deposit(uint256 assets_, address receiver_) external returns (uint256 shares_); + function mint(uint256 shares_, address receiver_) external returns (uint256 assets_); + function redeem(uint256 shares_, address receiver_, address owner_) external returns (uint256 assetsAfterFee_); + function withdraw(uint256 assets_, address receiver_, address owner_) external returns (uint256 shares_); + function previewDeposit(uint256 assets) external view returns (uint256); + function previewMint(uint256 shares) external view returns (uint256); + function previewRedeem(uint256 shares) external view returns (uint256); + function previewWithdraw(uint256 assets) external view returns (uint256); +} diff --git a/contracts/interfaces/external/IAcrossHubPoolV2.sol b/contracts/interfaces/external/IAcrossHubPoolV2.sol new file mode 100644 index 00000000..ebfa0ab5 --- /dev/null +++ b/contracts/interfaces/external/IAcrossHubPoolV2.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +interface IAcrossHubPoolV2 { + function addLiquidity(address l1Token, uint256 l1TokenAmount) external payable; + function bondAmount() external view returns (uint256); + function bondToken() external view returns (address); + function claimProtocolFeesCaptured(address l1Token) external; + function crossChainContracts(uint256) external view returns (address adapter, address spokePool); + function disableL1TokenForLiquidityProvision(address l1Token) external; + function disputeRootBundle() external; + function emergencyDeleteProposal() external; + function enableL1TokenForLiquidityProvision(address l1Token) external; + function exchangeRateCurrent(address l1Token) external returns (uint256); + function executeRootBundle( + uint256 chainId, + uint256 groupIndex, + uint256[] memory bundleLpFees, + int256[] memory netSendAmounts, + int256[] memory runningBalances, + uint8 leafId, + address[] memory l1Tokens, + bytes32[] memory proof + ) external; + function finder() external view returns (address); + function getCurrentTime() external view returns (uint256); + function haircutReserves(address l1Token, int256 haircutAmount) external; + function identifier() external view returns (bytes32); + function liquidityUtilizationCurrent(address l1Token) external returns (uint256); + function liquidityUtilizationPostRelay(address l1Token, uint256 relayedAmount) external returns (uint256); + function liveness() external view returns (uint32); + function loadEthForL2Calls() external payable; + function lpFeeRatePerSecond() external view returns (uint256); + function lpTokenFactory() external view returns (address); + function multicall(bytes[] memory data) external payable returns (bytes[] memory results); + function owner() external view returns (address); + function paused() external view returns (bool); + function poolRebalanceRoute(uint256 destinationChainId, address l1Token) + external + view + returns (address destinationToken); + function pooledTokens(address) + external + view + returns ( + address lpToken, + bool isEnabled, + uint32 lastLpFeeUpdate, + int256 utilizedReserves, + uint256 liquidReserves, + uint256 undistributedLpFees + ); + function proposeRootBundle( + uint256[] memory bundleEvaluationBlockNumbers, + uint8 poolRebalanceLeafCount, + bytes32 poolRebalanceRoot, + bytes32 relayerRefundRoot, + bytes32 slowRelayRoot + ) external; + function protocolFeeCaptureAddress() external view returns (address); + function protocolFeeCapturePct() external view returns (uint256); + function relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) external; + function removeLiquidity(address l1Token, uint256 lpTokenAmount, bool sendEth) external; + function renounceOwnership() external; + function rootBundleProposal() + external + view + returns ( + bytes32 poolRebalanceRoot, + bytes32 relayerRefundRoot, + bytes32 slowRelayRoot, + uint256 claimedBitMap, + address proposer, + uint8 unclaimedPoolRebalanceLeafCount, + uint32 challengePeriodEndTimestamp + ); + function setBond(address newBondToken, uint256 newBondAmount) external; + function setCrossChainContracts(uint256 l2ChainId, address adapter, address spokePool) external; + function setCurrentTime(uint256 time) external; + function setDepositRoute( + uint256 originChainId, + uint256 destinationChainId, + address originToken, + bool depositsEnabled + ) external; + function setIdentifier(bytes32 newIdentifier) external; + function setLiveness(uint32 newLiveness) external; + function setPaused(bool pause) external; + function setPoolRebalanceRoute(uint256 destinationChainId, address l1Token, address destinationToken) external; + function setProtocolFeeCapture(address newProtocolFeeCaptureAddress, uint256 newProtocolFeeCapturePct) external; + function sync(address l1Token) external; + function timerAddress() external view returns (address); + function transferOwnership(address newOwner) external; + function unclaimedAccumulatedProtocolFees(address) external view returns (uint256); + function weth() external view returns (address); +} diff --git a/contracts/interfaces/external/IPendleMarketV3.sol b/contracts/interfaces/external/IPendleMarketV3.sol new file mode 100644 index 00000000..67ccc27a --- /dev/null +++ b/contracts/interfaces/external/IPendleMarketV3.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +interface IPendleMarketV3 { + struct MarketState { + int256 totalPt; + int256 totalSy; + int256 totalLp; + address treasury; + int256 scalarRoot; + uint256 expiry; + uint256 lnFeeRateRoot; + uint256 reserveFeePercent; + uint256 lastLnImpliedRate; + } + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function _storage() + external + view + returns ( + int128 totalPt, + int128 totalSy, + uint96 lastLnImpliedRate, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext + ); + function activeBalance(address) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function burn(address receiverSy, address receiverPt, uint256 netLpToBurn) + external + returns (uint256 netSyOut, uint256 netPtOut); + function decimals() external view returns (uint8); + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + function expiry() external view returns (uint256); + function factory() external view returns (address); + function getNonOverrideLnFeeRateRoot() external view returns (uint80); + function getRewardTokens() external view returns (address[] memory); + function increaseObservationsCardinalityNext(uint16 cardinalityNext) external; + function isExpired() external view returns (bool); + function lastRewardBlock() external view returns (uint256); + function mint(address receiver, uint256 netSyDesired, uint256 netPtDesired) + external + returns (uint256 netLpOut, uint256 netSyUsed, uint256 netPtUsed); + function name() external view returns (string memory); + function nonces(address owner) external view returns (uint256); + function observations(uint256) + external + view + returns (uint32 blockTimestamp, uint216 lnImpliedRateCumulative, bool initialized); + function observe(uint32[] memory secondsAgos) external view returns (uint216[] memory lnImpliedRateCumulative); + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + function readState(address router) external view returns (MarketState memory market); + function readTokens() external view returns (address _SY, address _PT, address _YT); + function redeemRewards(address user) external returns (uint256[] memory); + function rewardState(address) external view returns (uint128 index, uint128 lastBalance); + function skim() external; + function swapExactPtForSy(address receiver, uint256 exactPtIn, bytes memory data) + external + returns (uint256 netSyOut, uint256 netSyFee); + function swapSyForExactPt(address receiver, uint256 exactPtOut, bytes memory data) + external + returns (uint256 netSyIn, uint256 netSyFee); + function symbol() external view returns (string memory); + function totalActiveSupply() external view returns (uint256); + function totalSupply() external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function userReward(address, address) external view returns (uint128 index, uint128 accrued); +} diff --git a/contracts/interfaces/external/IPendlePrincipalToken.sol b/contracts/interfaces/external/IPendlePrincipalToken.sol new file mode 100644 index 00000000..b7d3d491 --- /dev/null +++ b/contracts/interfaces/external/IPendlePrincipalToken.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +interface IPendlePrincipalToken { + function DOMAIN_SEPARATOR() external view returns (bytes32); + function SY() external view returns (address); + function YT() external view returns (address); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function burnByYT(address user, uint256 amount) external; + function decimals() external view returns (uint8); + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + function expiry() external view returns (uint256); + function factory() external view returns (address); + function initialize(address _YT) external; + function isExpired() external view returns (bool); + function mintByYT(address user, uint256 amount) external; + function name() external view returns (string memory); + function nonces(address owner) external view returns (uint256); + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + function symbol() external view returns (string memory); + function totalSupply() external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/contracts/interfaces/external/IPendleStandardizedYield.sol b/contracts/interfaces/external/IPendleStandardizedYield.sol new file mode 100644 index 00000000..dfa01fd6 --- /dev/null +++ b/contracts/interfaces/external/IPendleStandardizedYield.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +interface IPendleStandardizedYield { + function DOMAIN_SEPARATOR() external view returns (bytes32); + function accruedRewards(address) external view returns (uint256[] memory rewardAmounts); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function assetInfo() external view returns (uint8, address, uint8); + function balanceOf(address account) external view returns (uint256); + function claimOwnership() external; + function claimRewards(address) external returns (uint256[] memory rewardAmounts); + function decimals() external view returns (uint8); + function deposit(address receiver, address tokenIn, uint256 amountTokenToDeposit, uint256 minSharesOut) + external + payable + returns (uint256 amountSharesOut); + function eETH() external view returns (address); + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); + function exchangeRate() external view returns (uint256); + function getRewardTokens() external view returns (address[] memory rewardTokens); + function getTokensIn() external view returns (address[] memory res); + function getTokensOut() external view returns (address[] memory res); + function isValidTokenIn(address token) external view returns (bool); + function isValidTokenOut(address token) external view returns (bool); + function liquidityPool() external view returns (address); + function name() external view returns (string memory); + function nonces(address owner) external view returns (uint256); + function owner() external view returns (address); + function pause() external; + function paused() external view returns (bool); + function pendingOwner() external view returns (address); + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + function previewDeposit(address tokenIn, uint256 amountTokenToDeposit) + external + view + returns (uint256 amountSharesOut); + function previewRedeem(address tokenOut, uint256 amountSharesToRedeem) + external + view + returns (uint256 amountTokenOut); + function redeem( + address receiver, + uint256 amountSharesToRedeem, + address tokenOut, + uint256 minTokenOut, + bool burnFromInternalBalance + ) external returns (uint256 amountTokenOut); + function referee() external view returns (address); + function rewardIndexesCurrent() external returns (uint256[] memory indexes); + function rewardIndexesStored() external view returns (uint256[] memory indexes); + function symbol() external view returns (string memory); + function totalSupply() external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function transferOwnership(address newOwner, bool direct, bool renounce) external; + function unpause() external; + function weETH() external view returns (address); + function yieldToken() external view returns (address); +} diff --git a/contracts/interfaces/external/IStETH.sol b/contracts/interfaces/external/IStETH.sol new file mode 100644 index 00000000..460f868d --- /dev/null +++ b/contracts/interfaces/external/IStETH.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; + +interface IStETH { + function submit(address _referral) external payable returns (uint256); +} diff --git a/package.json b/package.json index 04551b37..743826d8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build:typechain": "yarn patch-hardhat-typechain && yarn typechain && yarn fix-typechain && yarn transpile-dist", "chain": "npx hardhat node --no-deploy", "chain:fork": "FORK=true yarn run chain", - "chain:fork:ethereum": "yarn run chain", + "chain:fork:ethereum": "yarn run chain:fork", "chain:fork:polygon": "NETWORK=polygon yarn run chain:fork", "chain:fork:optimism": "NETWORK=optimism yarn run chain:fork", "chain:fork:arbitrum": "NETWORK=arbitrum yarn run chain:fork", @@ -87,6 +87,8 @@ "husky": "^4.2.5", "lint-staged": "^10.2.11", "lodash": "^4.17.4", + "prettier": "^3.2.5", + "prettier-plugin-solidity": "^1.3.1", "semantic-release": "^19.0.2", "solc": "^0.6.10", "solhint": "^3.1.0", diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index a198db19..1683ce92 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -25,6 +25,14 @@ export const PRODUCTION_ADDRESSES = { wbtc: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", swETH: "0xf951E335afb289353dc249e82926178EaC7DEd78", ETHx: "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", + instadappEthV2: "0xa0d3707c569ff8c87fa923d3823ec5d81c98be78", + pendleEEth0624: "0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966", + pendleRsEth0624: "0xB05cABCd99cf9a73b19805edefC5f67CA5d1895E", + pendleRswEth0624: "0x5cb12D56F5346a016DBBA8CA90635d82e6D1bcEa", + weEth: "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + rsEth: "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + rswEth: "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0", + acrossWethLP: "0x28F77208728B0A45cAb24c4868334581Fe86F95B", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", @@ -56,6 +64,13 @@ export const PRODUCTION_ADDRESSES = { balancerv2: { vault: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", }, + pendle: { + markets: { + eEth0624: "0xF32e58F92e60f4b0A37A69b95d642A471365EAe8", + rsEth0624: "0x4f43c77872db6ba177c270986cd30c3381af37ee", + rswEth0624: "0xa9355a5d306c67027c54de0e5a72df76befa5694", + }, + }, }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", @@ -76,6 +91,7 @@ export const PRODUCTION_ADDRESSES = { aaveV3LeverageModule: "0x71E932715F5987077ADC5A7aA245f38841E0DcBe", constantPriceAdapter: "0x13c33656570092555Bf27Bdf53Ce24482B85D992", linearPriceAdapter: "0x237F7BBe0b358415bE84AB6d279D4338C0d026bB", + setTokenCreator: "0x2758BF6Af0EC63f1710d3d7890e1C263a247B75E", }, lending: { aave: { diff --git a/test/integration/ethereum/flashMintHyETH.spec.ts b/test/integration/ethereum/flashMintHyETH.spec.ts new file mode 100644 index 00000000..04eab339 --- /dev/null +++ b/test/integration/ethereum/flashMintHyETH.spec.ts @@ -0,0 +1,433 @@ +import "module-alias/register"; +import { Account, Address } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { getAccounts, getWaffleExpect } from "@utils/index"; +import { setBlockNumber } from "@utils/test/testingUtils"; +import { ProtocolUtils } from "@utils/common"; +import { ethers } from "hardhat"; +import { utils } from "ethers"; +import { + IDebtIssuanceModule, + IDebtIssuanceModule__factory, + SetToken, + SetToken__factory, + SetTokenCreator, + SetTokenCreator__factory, + FlashMintHyETH, + IPendlePrincipalToken__factory, + IERC20, + IWETH, + IWETH__factory, +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO, MAX_UINT_256 } from "@utils/constants"; +import { ether, usdc } from "@utils/index"; +import { impersonateAccount } from "./utils"; + +const expect = getWaffleExpect(); +const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + +enum Exchange { + None, + Sushiswap, + Quickswap, + UniV3, + Curve, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +const NO_OP_SWAP_DATA: SwapData = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + exchange: Exchange.None, +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintHyETH - Integration Test", async () => { + const addresses = PRODUCTION_ADDRESSES; + let owner: Account; + let deployer: DeployHelper; + + let setTokenCreator: SetTokenCreator; + let debtIssuanceModule: IDebtIssuanceModule; + + // const collateralTokenAddress = addresses.tokens.stEth; + setBlockNumber(19740000, true); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + setTokenCreator = SetTokenCreator__factory.connect( + addresses.setFork.setTokenCreator, + owner.wallet, + ); + debtIssuanceModule = IDebtIssuanceModule__factory.connect( + addresses.setFork.debtIssuanceModuleV2, + owner.wallet, + ); + }); + + context("When exchange issuance is deployed", () => { + let flashMintHyETH: FlashMintHyETH; + before(async () => { + flashMintHyETH = await deployer.extensions.deployFlashMintHyETH( + addresses.tokens.weth, + addresses.dexes.uniV2.router, + addresses.dexes.sushiswap.router, + addresses.dexes.uniV3.router, + addresses.dexes.uniV3.quoter, + addresses.dexes.curve.calculator, + addresses.dexes.curve.addressProvider, + addresses.setFork.controller, + addresses.setFork.debtIssuanceModuleV2, + addresses.tokens.stEth, + addresses.dexes.curve.pools.stEthEth, + ); + }); + + it("weth address is set correctly", async () => { + const returnedAddresses = await flashMintHyETH.dexAdapter(); + expect(returnedAddresses.weth).to.eq(utils.getAddress(addresses.tokens.weth)); + }); + + it("sushi router address is set correctly", async () => { + const returnedAddresses = await flashMintHyETH.dexAdapter(); + expect(returnedAddresses.sushiRouter).to.eq( + utils.getAddress(addresses.dexes.sushiswap.router), + ); + }); + + it("uniV2 router address is set correctly", async () => { + const returnedAddresses = await flashMintHyETH.dexAdapter(); + expect(returnedAddresses.quickRouter).to.eq(utils.getAddress(addresses.dexes.uniV2.router)); + }); + + it("uniV3 router address is set correctly", async () => { + const returnedAddresses = await flashMintHyETH.dexAdapter(); + expect(returnedAddresses.uniV3Router).to.eq(utils.getAddress(addresses.dexes.uniV3.router)); + }); + + it("controller address is set correctly", async () => { + expect(await flashMintHyETH.setController()).to.eq( + utils.getAddress(addresses.setFork.controller), + ); + }); + + it("debt issuance module address is set correctly", async () => { + expect(await flashMintHyETH.issuanceModule()).to.eq( + utils.getAddress(addresses.setFork.debtIssuanceModuleV2), + ); + }); + + context("when setToken with hyETH launch composition is deployed", () => { + let setToken: SetToken; + const components = [ + addresses.tokens.instadappEthV2, + addresses.tokens.pendleEEth0624, + addresses.tokens.pendleRsEth0624, + addresses.tokens.pendleRswEth0624, + addresses.tokens.acrossWethLP, + addresses.tokens.USDC, + ]; + const positions = [ + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + usdc(600), + ]; + + const componentSwapDataIssue = [ + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.USDC], + pool: ADDRESS_ZERO, + }, + ]; + + const componentSwapDataRedeem = [ + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + { + exchange: Exchange.UniV3, + fees: [500], + path: [ addresses.tokens.USDC, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + ]; + + const modules = [addresses.setFork.debtIssuanceModuleV2]; + const tokenName = "IndexCoop High Yield ETH"; + const tokenSymbol = "HyETH"; + + before(async () => { + const tx = await setTokenCreator.create( + components, + positions, + modules, + owner.address, + tokenName, + tokenSymbol, + ); + const retrievedSetAddress = await new ProtocolUtils( + ethers.provider, + ).getCreatedSetTokenAddress(tx.hash); + setToken = SetToken__factory.connect(retrievedSetAddress, owner.wallet); + + await debtIssuanceModule.initialize( + setToken.address, + ether(0.5), + ether(0), + ether(0), + owner.address, + ADDRESS_ZERO, + ); + + await flashMintHyETH.approveToken( + addresses.tokens.stEth, + addresses.tokens.instadappEthV2, + MAX_UINT_256, + ); + await flashMintHyETH.setSwapData(addresses.tokens.stEth, ADDRESS_ZERO, { + path: [addresses.tokens.stEth, ETH_ADDRESS], + fees: [], + pool: addresses.dexes.curve.pools.stEthEth, + exchange: 4, + }); + + const eEthPendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleEEth0624, + owner.wallet, + ); + await flashMintHyETH.approveSetToken(setToken.address); + const eEthSyToken = await eEthPendleToken.SY(); + await flashMintHyETH.approveToken( + eEthSyToken, + addresses.dexes.pendle.markets.eEth0624, + MAX_UINT_256, + ); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleEEth0624, + eEthSyToken, + addresses.tokens.weEth, + addresses.dexes.pendle.markets.eEth0624, + ); + // weETH -> weth pool: https://etherscan.io/address/0x7a415b19932c0105c82fdb6b720bb01b0cc2cae3 + await flashMintHyETH.setSwapData(addresses.tokens.weEth, ADDRESS_ZERO, { + path: [addresses.tokens.weEth, addresses.tokens.weth], + fees: [500], + pool: ADDRESS_ZERO, + exchange: 3, + }); + + const rsEthPendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleRsEth0624, + owner.wallet, + ); + await flashMintHyETH.approveSetToken(setToken.address); + const rsEthSyToken = await rsEthPendleToken.SY(); + await flashMintHyETH.approveToken( + rsEthSyToken, + addresses.dexes.pendle.markets.rsEth0624, + MAX_UINT_256, + ); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleRsEth0624, + rsEthSyToken, + addresses.tokens.rsEth, + addresses.dexes.pendle.markets.rsEth0624, + ); + // rsEth -> weth pool: https://etherscan.io/address/0x059615ebf32c946aaab3d44491f78e4f8e97e1d3 + await flashMintHyETH.setSwapData(addresses.tokens.rsEth, ADDRESS_ZERO, { + path: [addresses.tokens.rsEth, addresses.tokens.weth], + fees: [500], + pool: ADDRESS_ZERO, + exchange: 3, + }); + + const rswEthPendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleRswEth0624, + owner.wallet, + ); + await flashMintHyETH.approveSetToken(setToken.address); + const rswEthSyToken = await rswEthPendleToken.SY(); + await flashMintHyETH.approveToken( + rswEthSyToken, + addresses.dexes.pendle.markets.rswEth0624, + MAX_UINT_256, + ); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleRswEth0624, + rswEthSyToken, + addresses.tokens.rswEth, + addresses.dexes.pendle.markets.rswEth0624, + ); + // rswEth -> weth pool: https://etherscan.io/address/0xe62627326d7794e20bb7261b24985294de1579fe + await flashMintHyETH.setSwapData(addresses.tokens.rswEth, ADDRESS_ZERO, { + path: [addresses.tokens.rswEth, addresses.tokens.weth], + fees: [3000], + pool: ADDRESS_ZERO, + exchange: 3, + }); + }); + it("setToken is deployed correctly", async () => { + expect(await setToken.symbol()).to.eq(tokenSymbol); + }); + + ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { + describe(`When inputToken is ${inputTokenName}`, () => { + const ethIn = ether(1.01); + const maxAmountIn = inputTokenName == "USDC" ? usdc(3300) : ethIn; + const setTokenAmount = ether(1); + let inputToken: IERC20 | IWETH; + let swapDataInputTokenToEth: SwapData; + let swapDataEthToInputToken: SwapData; + + before(async () => { + if (inputTokenName != "eth") { + inputToken = IWETH__factory.connect(addresses.tokens[inputTokenName], owner.wallet); + inputToken.approve(flashMintHyETH.address, maxAmountIn); + } + if (inputTokenName === "weth") { + await inputToken.deposit({ value: maxAmountIn }); + swapDataInputTokenToEth = { + path: [addresses.tokens.weth, ETH_ADDRESS], + fees: [], + pool: ADDRESS_ZERO, + exchange: 0, + }; + swapDataEthToInputToken = { + path: [ETH_ADDRESS, addresses.tokens.weth], + fees: [], + pool: ADDRESS_ZERO, + exchange: 0, + }; + } + if (inputTokenName === "USDC") { + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); + swapDataInputTokenToEth = { + path: [addresses.tokens.USDC, addresses.tokens.weth], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + swapDataEthToInputToken = { + path: [addresses.tokens.weth, addresses.tokens.USDC], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + } + }); + function subject() { + if (inputTokenName === "eth") { + return flashMintHyETH.issueExactSetFromETH( + setToken.address, + setTokenAmount, + componentSwapDataIssue, + { + value: maxAmountIn, + }, + ); + } else { + return flashMintHyETH.issueExactSetFromERC20( + setToken.address, + setTokenAmount, + inputToken.address, + maxAmountIn, + swapDataInputTokenToEth, + swapDataEthToInputToken, + componentSwapDataIssue, + ); + } + } + it("Can issue set token", async () => { + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputTokenBalanceAfter = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + }); + + describe("When set token has been issued", () => { + const minAmountOut = maxAmountIn.mul(8).div(10); + beforeEach(async () => { + await flashMintHyETH.issueExactSetFromETH( + setToken.address, + setTokenAmount, + componentSwapDataIssue, + { + value: ethIn, + }, + ); + await setToken.approve(flashMintHyETH.address, setTokenAmount); + }); + + function subject() { + if (inputTokenName === "eth") { + return flashMintHyETH.redeemExactSetForETH( + setToken.address, + setTokenAmount, + minAmountOut, + componentSwapDataRedeem, + ); + } else { + return flashMintHyETH.redeemExactSetForERC20( + setToken.address, + setTokenAmount, + inputToken.address, + minAmountOut, + swapDataEthToInputToken, + componentSwapDataRedeem, + ); + } + } + + it("Can redeem set token", async () => { + const inputTokenBalanceBefore = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const inputTokenBalanceAfter = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); + }); + }); + }); + }); + }); + }); + }); +} diff --git a/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts b/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts index f444e8b4..53b2e2ae 100644 --- a/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts +++ b/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts @@ -41,7 +41,7 @@ import { ethers } from "hardhat"; const expect = getWaffleExpect(); if (process.env.INTEGRATIONTEST) { - describe.only("OptimisticAuctionRebalanceExtensionV1 - Integration Test dsEth", () => { + describe("OptimisticAuctionRebalanceExtensionV1 - Integration Test dsEth", () => { const contractAddresses = PRODUCTION_ADDRESSES; const rules = "Rules stored on ipfs under hash: Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z"; diff --git a/utils/config.ts b/utils/config.ts index dcb392a1..67b5ca2c 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -15,7 +15,7 @@ export const arbitrumForkingConfig = { export const mainnetForkingConfig = { url: "https://eth-mainnet.alchemyapi.io/v2/" + process.env.ALCHEMY_TOKEN, - blockNumber: process.env.LATESTBLOCK ? undefined : 17895372, + blockNumber: process.env.LATESTBLOCK ? undefined : 19740000, }; export const forkingConfig = diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index c9f1cc45..8d526249 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -12,6 +12,7 @@ export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { BasicIssuanceModule } from "../../typechain/BasicIssuanceModule"; export { DEXAdapter } from "../../typechain/DEXAdapter"; +export { DEXAdapterV2 } from "../../typechain/DEXAdapterV2"; export { ExchangeIssuance } from "../../typechain/ExchangeIssuance"; export { ExchangeIssuanceV2 } from "../../typechain/ExchangeIssuanceV2"; export { ExchangeIssuanceLeveraged } from "../../typechain/ExchangeIssuanceLeveraged"; diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 35785542..9ea93e8e 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -13,6 +13,7 @@ import { AirdropExtension, AuctionRebalanceExtension, DEXAdapter, + DEXAdapterV2, ExchangeIssuance, ExchangeIssuanceV2, ExchangeIssuanceLeveraged, @@ -40,9 +41,11 @@ import { import { AirdropExtension__factory } from "../../typechain/factories/AirdropExtension__factory"; import { AuctionRebalanceExtension__factory } from "../../typechain/factories/AuctionRebalanceExtension__factory"; import { DEXAdapter__factory } from "../../typechain/factories/DEXAdapter__factory"; +import { DEXAdapterV2__factory } from "../../typechain/factories/DEXAdapterV2__factory"; 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 { FlashMintHyETH__factory } from "../../typechain/factories/FlashMintHyETH__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"; @@ -182,6 +185,10 @@ export default class DeployExtensions { return await new DEXAdapter__factory(this._deployerSigner).deploy(); } + public async deployDEXAdapterV2(): Promise { + return await new DEXAdapterV2__factory(this._deployerSigner).deploy(); + } + public async deployExchangeIssuanceLeveraged( wethAddress: Address, quickRouterAddress: Address, @@ -312,6 +319,50 @@ export default class DeployExtensions { ); } + public async deployFlashMintHyETH( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + setControllerAddress: Address, + debtIssuanceModuleAddress: Address, + stETHAddress: Address, + curveStEthEthPoolAddress: Address, + ) { + const dexAdapter = await this.deployDEXAdapterV2(); + + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", + ); + + return await new FlashMintHyETH__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, + debtIssuanceModuleAddress, + stETHAddress, + curveStEthEthPoolAddress + ); + } + + public async deployExchangeIssuanceLeveragedForCompound( wethAddress: Address, quickRouterAddress: Address, diff --git a/yarn.lock b/yarn.lock index ea0beec6..1719b205 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1529,6 +1529,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@solidity-parser/parser@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" + integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== + "@solidity-parser/parser@^0.18.0": version "0.18.0" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" @@ -9346,6 +9351,15 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier-plugin-solidity@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" + integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== + dependencies: + "@solidity-parser/parser" "^0.17.0" + semver "^7.5.4" + solidity-comments-extractor "^0.0.8" + prettier@^1.14.3: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" @@ -9356,6 +9370,11 @@ prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -10231,6 +10250,13 @@ semver@^7.3.4: dependencies: lru-cache "^6.0.0" +semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -10557,6 +10583,11 @@ solhint@^3.1.0: optionalDependencies: prettier "^1.14.3" +solidity-comments-extractor@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" + integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== + solidity-coverage@^0.8.0: version "0.8.12" resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.12.tgz#c4fa2f64eff8ada7a1387b235d6b5b0e6c6985ed"