From b4b7216c5ab22b51c2e8da88f184f73e154aa404 Mon Sep 17 00:00:00 2001 From: pblivin0x <84149824+pblivin0x@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:22:00 -0400 Subject: [PATCH] feat: `FlashMintHyEthV3` (#187) * flash mint hyeth v3 * DexAdapterV3 with BalancerV2 support --------- Co-authored-by: Edward Kim Co-authored-by: christn --- contracts/exchangeIssuance/DEXAdapterV3.sol | 1147 +++++++++++++++++ .../exchangeIssuance/FlashMintHyETHV3.sol | 698 ++++++++++ .../interfaces/external/IRsEthAdapter.sol | 11 + test/integration/ethereum/addresses.ts | 3 + .../ethereum/flashMintHyETHV3.spec.ts | 476 +++++++ .../integration/ethereum/flashMintNAV.spec.ts | 2 +- .../ethereum/flashMintWrappedRebasing.spec.ts | 2 +- ...rgetWeightWrapExtensionRebasingNav.spec.ts | 2 +- utils/deploys/deployExtensions.ts | 51 + 9 files changed, 2389 insertions(+), 3 deletions(-) create mode 100644 contracts/exchangeIssuance/DEXAdapterV3.sol create mode 100644 contracts/exchangeIssuance/FlashMintHyETHV3.sol create mode 100644 contracts/interfaces/external/IRsEthAdapter.sol create mode 100644 test/integration/ethereum/flashMintHyETHV3.spec.ts diff --git a/contracts/exchangeIssuance/DEXAdapterV3.sol b/contracts/exchangeIssuance/DEXAdapterV3.sol new file mode 100644 index 00000000..c33ce740 --- /dev/null +++ b/contracts/exchangeIssuance/DEXAdapterV3.sol @@ -0,0 +1,1147 @@ +/* + 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 { 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 { IVault } from "../interfaces/external/balancer-v2/IVault.sol"; +import { IQuoter } from "../interfaces/IQuoter.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + + +/** + * @title DEXAdapterV3 + * @author Index Coop + * + * Same as DEXAdapterV2 but adds BalancerV2 support + */ +library DEXAdapterV3 { + 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, BalancerV2 } + + /* ============ Structs ============ */ + + struct Addresses { + address quickRouter; + address sushiRouter; + address uniV3Router; + address uniV3Quoter; + address curveAddressProvider; + address curveCalculator; + address balV2Vault; + // Wrapped native token (WMATIC on polygon) + address weth; + } + + struct SwapData { + address[] path; + uint24[] fees; + address pool; // For Curve swaps + bytes32[] poolIds; // For Balancer V2 multihop swaps + 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, fees, pool, and pool IDs + * + * @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) + ); + } + if(_swapData.exchange == Exchange.BalancerV2){ + return _swapExactTokensForTokensBalancerV2( + _swapData.path, + _amountIn, + _minAmountOut, + _swapData.poolIds, + IVault(_addresses.balV2Vault) + ); + } 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, fees, pool, and pool IDs + * + * @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) + ); + } + if(_swapData.exchange == Exchange.BalancerV2){ + return _swapTokensForExactTokensBalancerV2( + _swapData.path, + _amountOut, + _maxAmountIn, + _swapData.poolIds, + IVault(_addresses.balV2Vault) + ); + } 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 if (_swapData.exchange == Exchange.BalancerV2) { + return _getAmountOutBalancerV2( + _swapData, + _addresses, + _amountIn + ); + } 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 if (_swapData.exchange == Exchange.BalancerV2) { + return _getAmountInBalancerV2( + _swapData, + _addresses, + _amountOut + ); + } 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 + ); + } + + /** + * Execute exact input swap via Balancer V2 (supports multihop swaps) + * + * @param _path List of token addresses to swap via. + * @param _amountIn The amount of input token to be spent + * @param _minAmountOut Minimum amount of output token to receive + * @param _poolIds List of pool IDs for each swap step + * @param _vault Address of the Balancer V2 Vault + * + * @return amountOut The amount of output tokens received + */ + function _swapExactTokensForTokensBalancerV2( + address[] memory _path, + uint256 _amountIn, + uint256 _minAmountOut, + bytes32[] memory _poolIds, + IVault _vault + ) + private + returns (uint256 amountOut) + { + require(_path.length >= 2, "DEXAdapterV3: BALANCER_PATH_LENGTH"); + require(_poolIds.length == _path.length - 1, "DEXAdapterV3: INVALID_POOL_IDS"); + + // Approve the Vault to spend the input token + _safeApprove(IERC20(_path[0]), address(_vault), _amountIn); + + // Build the assets array (unique tokens in the path) + address[] memory assets = _getAssets(_path); + + // Build the swaps array + IVault.BatchSwapStep[] memory swaps = new IVault.BatchSwapStep[](_path.length - 1); + + for (uint256 i = 0; i < _path.length - 1; i++) { + swaps[i] = IVault.BatchSwapStep({ + poolId: _poolIds[i], + assetInIndex: _getAssetIndex(assets, _path[i]), + assetOutIndex: _getAssetIndex(assets, _path[i + 1]), + amount: i == 0 ? _amountIn : 0, // Only specify amount for first swap + userData: "" + }); + } + + // Set up funds + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + // Set up limits + int256[] memory limits = new int256[](assets.length); + + for (uint256 i = 0; i < assets.length; i++) { + if (assets[i] == _path[0]) { + limits[i] = int256(_amountIn); + } else if (assets[i] == _path[_path.length - 1]) { + limits[i] = -int256(_minAmountOut); + } else { + limits[i] = 0; + } + } + + // Perform the batch swap + int256[] memory deltas = _vault.batchSwap( + IVault.SwapKind.GIVEN_IN, + swaps, + assets, + funds, + limits, + block.timestamp + ); + + amountOut = uint256(-deltas[_getAssetIndex(assets, _path[_path.length - 1])]); + require(amountOut >= _minAmountOut, "DEXAdapterV3: INSUFFICIENT_OUTPUT_AMOUNT"); + } + + /** + * Execute exact output swap via Balancer V2 (supports multihop swaps) + * + * @param _path List of token addresses to swap via. + * @param _amountOut The amount of output token required + * @param _maxAmountIn Maximum amount of input token to be spent + * @param _poolIds List of pool IDs for each swap step + * @param _vault Address of the Balancer V2 Vault + * + * @return amountIn The amount of input tokens spent + */ + function _swapTokensForExactTokensBalancerV2( + address[] memory _path, + uint256 _amountOut, + uint256 _maxAmountIn, + bytes32[] memory _poolIds, + IVault _vault + ) + private + returns (uint256 amountIn) + { + require(_path.length >= 2, "DEXAdapterV3: BALANCER_PATH_LENGTH"); + require(_poolIds.length == _path.length - 1, "DEXAdapterV3: INVALID_POOL_IDS"); + + // Approve the Vault to spend the input token + _safeApprove(IERC20(_path[0]), address(_vault), _maxAmountIn); + + // Build the assets array (unique tokens in the path) + address[] memory assets = _getAssets(_path); + + // Build the swaps array + IVault.BatchSwapStep[] memory swaps = new IVault.BatchSwapStep[](_path.length - 1); + + for (uint256 i = 0; i < _path.length - 1; i++) { + swaps[i] = IVault.BatchSwapStep({ + poolId: _poolIds[i], + assetInIndex: _getAssetIndex(assets, _path[i]), + assetOutIndex: _getAssetIndex(assets, _path[i + 1]), + amount: 0, // Amount is determined by the Vault + userData: "" + }); + } + + // Set up funds + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + // Set up limits + int256[] memory limits = new int256[](assets.length); + + for (uint256 i = 0; i < assets.length; i++) { + if (assets[i] == _path[0]) { + limits[i] = int256(_maxAmountIn); + } else if (assets[i] == _path[_path.length - 1]) { + limits[i] = -int256(_amountOut); + } else { + limits[i] = 0; + } + } + + // Perform the batch swap + int256[] memory deltas = _vault.batchSwap( + IVault.SwapKind.GIVEN_OUT, + swaps, + assets, + funds, + limits, + block.timestamp + ); + + amountIn = uint256(deltas[_getAssetIndex(assets, _path[0])]); + require(amountIn <= _maxAmountIn, "DEXAdapterV3: EXCESSIVE_INPUT_AMOUNT"); + } + + /** + * Gets the output amount of a token swap on Balancer V2 using queryBatchSwap. + * + * @param _swapData the swap parameters + * @param _addresses Struct containing relevant smart contract addresses + * @param _amountIn the input amount of the trade + * + * @return amountOut the output amount of the swap + */ + function _getAmountOutBalancerV2( + SwapData memory _swapData, + Addresses memory _addresses, + uint256 _amountIn + ) + private + returns (uint256 amountOut) + { + IVault _vault = IVault(_addresses.balV2Vault); + + // Build the assets array (unique tokens in the path) + address[] memory assets = _getAssets(_swapData.path); + + // Build the swaps array + IVault.BatchSwapStep[] memory swaps = new IVault.BatchSwapStep[](_swapData.path.length - 1); + + for (uint256 i = 0; i < _swapData.path.length - 1; i++) { + swaps[i] = IVault.BatchSwapStep({ + poolId: _swapData.poolIds[i], + assetInIndex: _getAssetIndex(assets, _swapData.path[i]), + assetOutIndex: _getAssetIndex(assets, _swapData.path[i + 1]), + amount: i == 0 ? _amountIn : 0, // Only specify amount for first swap + userData: "" + }); + } + + // Set up funds (not used in query) + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + // Perform the query + int256[] memory deltas = _vault.queryBatchSwap( + IVault.SwapKind.GIVEN_IN, + swaps, + assets, + funds + ); + + amountOut = uint256(-deltas[_getAssetIndex(assets, _swapData.path[_swapData.path.length - 1])]); + } + + /** + * Gets the input amount of a fixed output swap on Balancer V2 using queryBatchSwap. + * + * @param _swapData the swap parameters + * @param _addresses Struct containing relevant smart contract addresses + * @param _amountOut the output amount of the swap + * + * @return amountIn the input amount of the swap + */ + function _getAmountInBalancerV2( + SwapData memory _swapData, + Addresses memory _addresses, + uint256 _amountOut + ) + private + returns (uint256 amountIn) + { + IVault _vault = IVault(_addresses.balV2Vault); + + // Build the assets array (unique tokens in the path) + address[] memory assets = _getAssets(_swapData.path); + + // Build the swaps array + IVault.BatchSwapStep[] memory swaps = new IVault.BatchSwapStep[](_swapData.path.length - 1); + + for (uint256 i = 0; i < _swapData.path.length - 1; i++) { + swaps[i] = IVault.BatchSwapStep({ + poolId: _swapData.poolIds[i], + assetInIndex: _getAssetIndex(assets, _swapData.path[i]), + assetOutIndex: _getAssetIndex(assets, _swapData.path[i + 1]), + amount: i == swaps.length - 1 ? _amountOut : 0, // Only specify amount for last swap + userData: "" + }); + } + + // Set up funds (not used in query) + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: address(this), + fromInternalBalance: false, + recipient: payable(address(this)), + toInternalBalance: false + }); + + // Perform the query + int256[] memory deltas = _vault.queryBatchSwap( + IVault.SwapKind.GIVEN_OUT, + swaps, + assets, + funds + ); + + amountIn = uint256(deltas[_getAssetIndex(assets, _swapData.path[0])]); + } + + /** + * Helper function to get the list of unique assets from the path. + * + * @param _path List of token addresses in the swap path + * + * @return assets List of unique assets + */ + function _getAssets(address[] memory _path) private pure returns (address[] memory assets) { + uint256 assetCount = 0; + address[] memory tempAssets = new address[](_path.length); + + for (uint256 i = 0; i < _path.length; i++) { + bool alreadyAdded = false; + for (uint256 j = 0; j < assetCount; j++) { + if (tempAssets[j] == _path[i]) { + alreadyAdded = true; + break; + } + } + if (!alreadyAdded) { + tempAssets[assetCount] = _path[i]; + assetCount++; + } + } + + assets = new address[](assetCount); + for (uint256 i = 0; i < assetCount; i++) { + assets[i] = tempAssets[i]; + } + } + + /** + * Helper function to get the index of an asset in the assets array. + * + * @param assets List of assets + * @param token Token address to find + * + * @return index Index of the token in the assets array + */ + function _getAssetIndex(address[] memory assets, address token) private pure returns (uint256) { + for (uint256 i = 0; i < assets.length; i++) { + if (assets[i] == token) { + return i; + } + } + revert("DEXAdapterV3: TOKEN_NOT_IN_ASSETS"); + } +} diff --git a/contracts/exchangeIssuance/FlashMintHyETHV3.sol b/contracts/exchangeIssuance/FlashMintHyETHV3.sol new file mode 100644 index 00000000..e22d8126 --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintHyETHV3.sol @@ -0,0 +1,698 @@ +/* + 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 { IRsEthAdapter } from "../interfaces/external/IRsEthAdapter.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 { DEXAdapterV3 } from "./DEXAdapterV3.sol"; + +/** + * @title FlashMintHyETHV3 + */ +contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { + using DEXAdapterV3 for DEXAdapterV3.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; + uint256 exchangeRateFactor; + } + /* ============ 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); + IERC20 public constant agETH = IERC20(0xe1B4d34E8754600962Cd944B535180Bd758E6c2e); + IRsEthAdapter public constant rsEthAdapter = IRsEthAdapter(0xbf28C9FCb12A97441488f9C68FaA49811a98688a); + + /* ============ 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 => DEXAdapterV3.SwapData)) public swapData; + mapping(address => bool) public erc4626Components; + + /* ============ State Variables ============ */ + + DEXAdapterV3.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] == DEXAdapterV3.ETH_ADDRESS), + "FlashMint: INPUT_TOKEN_NOT_IN_PATH" + ); + require( + _path[_path.length - 1] == _outputToken || + (_outputToken == dexAdapter.weth && + _path[_path.length - 1] == DEXAdapterV3.ETH_ADDRESS), + "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH" + ); + } + _; + } + + /* ========== Constructor ========== */ + + constructor( + DEXAdapterV3.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 needs 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 amount 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, + DEXAdapterV3.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 amount 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, + DEXAdapterV3.SwapData memory _swapDataInputTokenToEth, + DEXAdapterV3.SwapData memory _swapDataEthToInputToken, + DEXAdapterV3.SwapData[] memory _swapDataEthToComponent + ) external payable nonReentrant returns (uint256) { + _inputToken.safeTransferFrom(msg.sender, address(this), _maxInputTokenAmount); + + uint256 ethAmount = _swapExactTokenForEth(_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, + DEXAdapterV3.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, + DEXAdapterV3.SwapData memory _swapDataEthToOutputToken, + DEXAdapterV3.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) ============ */ + + /** + * Control wether a component is registered as an ERC4626 token + * + * @param _component Address of the component + * @param _isERC4626 Boolean indicating if the component is an ERC4626 token + */ + function setERC4626Component( + address _component, + bool _isERC4626 + ) external onlyOwner { + erc4626Components[_component] = _isERC4626; + } + + /** + * 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, specify 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]) == DEXAdapterV3.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, + DEXAdapterV3.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 + * @param _exchangeRateFactor Factor to multiply the exchange rate when supplying to Pendle Market + */ + function setPendleMarket( + IPendlePrincipalToken _pt, + IPendleStandardizedYield _sy, + address _underlying, + IPendleMarketV3 _market, + uint256 _exchangeRateFactor + ) external onlyOwner { + pendleMarkets[_pt] = _market; + pendleMarketData[_market] = PendleMarketData({ + pt: _pt, + sy: _sy, + underlying: _underlying, + exchangeRateFactor: _exchangeRateFactor + }); + } + + /** + * 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); + + // Withdraw necessary ETH, if deposit size is enough to move the oracle, then the exchange rate will not be + // valid for computing the amount of ETH to withdraw, so increase by exchangeRateFactor + uint256 ethAmount = syAmount.mul(marketData.sy.exchangeRate()).div(1 ether); + uint256 syAmountPreview = marketData.sy.previewDeposit(address(0), ethAmount); + if (syAmountPreview < syAmount) { + ethAmount = ethAmount * marketData.exchangeRateFactor / 1 ether; + } + + // Special handling for agETH + if (marketData.underlying == address(agETH)) { + rsEthAdapter.getRSETHWithETH{value: ethAmount}(""); + uint256 agEthAmount = agETH.balanceOf(address(this)); + marketData.sy.deposit(address(this), address(agETH), agEthAmount, 0); + } else { + marketData.sy.deposit{ value: ethAmount }(address(this), address(0), ethAmount, 0); + } + marketData.sy.transfer(msg.sender, syAmount); + } else { + revert("Invalid callback"); + } + } + + /* ============ Internal ============ */ + + /** + * @dev Issue exact amount of SetToken from ETH + * + */ + function _issueExactSetFromEth( + ISetToken _setToken, + uint256 _amountSetToken, + DEXAdapterV3.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, + DEXAdapterV3.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, + DEXAdapterV3.SwapData memory _swapData + ) internal { + if(_swapData.exchange != DEXAdapterV3.Exchange.None) { + _swapEthForExactToken(_component, _amount, _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; + } + if (erc4626Components[_component]) { + uint256 assetAmount = IERC4626(_component).previewMint(_amount); + address asset = IERC4626(_component).asset(); + _swapEthForExactToken(asset, assetAmount, swapData[DEXAdapterV3.ETH_ADDRESS][asset]); + IERC4626(_component).mint(_amount, address(this)); + return; + } + revert("Missing Swapdata for non-standard component"); + } + + /** + * @dev Withdraw ETH from given component + * + */ + function _withdrawFromComponent( + address _component, + uint256 _amount, + DEXAdapterV3.SwapData memory _swapData + ) internal { + if(_swapData.exchange != DEXAdapterV3.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] == DEXAdapterV3.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; + } + if (erc4626Components[_component]) { + address asset = IERC4626(_component).asset(); + uint256 assetAmount = IERC4626(_component).redeem(_amount, address(this), address(this)); + _swapExactTokenForEth(IERC20(asset), assetAmount, swapData[asset][DEXAdapterV3.ETH_ADDRESS]); + 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 Requires 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, + DEXAdapterV3.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 _swapExactTokenForEth( + IERC20 _inputToken, + uint256 _inputTokenAmount, + DEXAdapterV3.SwapData memory _swapDataInputTokenToEth + ) internal returns (uint256 ethAmount) { + if(address(_inputToken) == dexAdapter.weth) { + ethAmount = _inputTokenAmount; + IWETH(dexAdapter.weth).withdraw(ethAmount); + } else { + ethAmount = dexAdapter.swapExactTokensForTokens( + _inputTokenAmount, + 0, + _swapDataInputTokenToEth + ); + if(_swapDataInputTokenToEth.path[_swapDataInputTokenToEth.path.length - 1] == dexAdapter.weth) { + IWETH(dexAdapter.weth).withdraw(ethAmount); + } + } + } + + function _swapEthForExactToken(address _token, uint256 _amount, DEXAdapterV3.SwapData memory _swapData) internal { + if(_token == dexAdapter.weth) { + IWETH(dexAdapter.weth).deposit{value: _amount}(); + return; + } + + require(_swapData.path.length > 1, "zero length swap path"); + require(_swapData.path[0] == DEXAdapterV3.ETH_ADDRESS || _swapData.path[0] == dexAdapter.weth, "Invalid input token"); + require(_swapData.path[_swapData.path.length - 1] == _token, "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); + } + } +} diff --git a/contracts/interfaces/external/IRsEthAdapter.sol b/contracts/interfaces/external/IRsEthAdapter.sol new file mode 100644 index 00000000..b0ec4f20 --- /dev/null +++ b/contracts/interfaces/external/IRsEthAdapter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.6.10; + +interface IRsEthAdapter { + function depositRsETH(uint256 rsETHAmount, string memory referralId) external; + function getRSETHWithERC20(address asset, uint256 depositAmount, string memory referralId) external; + function getRSETHWithETH(string memory referralId) external payable; + function lrtDepositPool() external view returns (address); + function rsETH() external view returns (address); + function vault() external view returns (address); +} diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 70b8fdf2..9481bfee 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -32,6 +32,8 @@ export const PRODUCTION_ADDRESSES = { pendleEzEth1226: "0xf7906F274c174A52d444175729E3fa98f9bde285", pendleEEth0926: "0x1c085195437738d73d75DC64bC5A3E098b7f93b1", pendleEEth1226: "0x6ee2b5E19ECBa773a352E5B21415Dc419A700d1d", + pendleAgEth1226: "0x7aa68E84bCD8d1B4C9e10B1e565DB993f68a3E09", + agEth: "0xe1B4d34E8754600962Cd944B535180Bd758E6c2e", ezEth: "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", weEth: "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", rsEth: "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", @@ -83,6 +85,7 @@ export const PRODUCTION_ADDRESSES = { ezEth1226: "0xD8F12bCDE578c653014F27379a6114F67F0e445f", eEth0926: "0xC8eDd52D0502Aa8b4D5C77361D4B3D300e8fC81c", eEth1226: "0x7d372819240D14fB477f17b964f95F33BeB4c704", + agEth1226: "0x6010676Bc2534652aD1Ef5Fa8073DcF9AD7EBFBe", }, }, dexAdapterV2: "0x88858930B3F1946A5C41a5deD7B5335431d5dE8D", diff --git a/test/integration/ethereum/flashMintHyETHV3.spec.ts b/test/integration/ethereum/flashMintHyETHV3.spec.ts new file mode 100644 index 00000000..0f1cec3e --- /dev/null +++ b/test/integration/ethereum/flashMintHyETHV3.spec.ts @@ -0,0 +1,476 @@ +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, + FlashMintHyETHV3, + 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, + BalancerV2, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + poolIds: utils.BytesLike[]; + exchange: Exchange; +}; + +const NO_OP_SWAP_DATA: SwapData = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + poolIds: [], + exchange: Exchange.None, +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintHyETHV3 - 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(20930000, false); + + 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: FlashMintHyETHV3; + before(async () => { + flashMintHyETH = await deployer.extensions.deployFlashMintHyETHV3( + 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.dexes.balancerv2.vault, + 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.pendleEzEth1226, + addresses.tokens.pendleEEth1226, + addresses.tokens.morphoRe7WETH, + addresses.tokens.pendleAgEth1226, + 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, + poolIds: [], + }, + ]; + + 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, + poolIds: [], + }, + ]; + + 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, + poolIds: [], + exchange: 4, + }); + await flashMintHyETH.setSwapData(addresses.tokens.agEth, ADDRESS_ZERO, { + exchange: Exchange.BalancerV2, + fees: [], + path: [addresses.tokens.agEth, addresses.tokens.rsEth, addresses.tokens.weth], + pool: ADDRESS_ZERO, + poolIds: [ + "0xf1bbc5d95cd5ae25af9916b8a193748572050eb00000000000000000000006bc", + "0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f", + ], + }); + + await flashMintHyETH.setERC4626Component(addresses.tokens.morphoRe7WETH, true); + await flashMintHyETH.approveToken( + addresses.tokens.weth, + addresses.tokens.morphoRe7WETH, + MAX_UINT_256, + ); + const ezEth1226PendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleEzEth1226, + owner.wallet, + ); + await flashMintHyETH.approveSetToken(setToken.address); + const ezEth1226SyToken = await ezEth1226PendleToken.SY(); + await flashMintHyETH.approveToken( + ezEth1226SyToken, + addresses.dexes.pendle.markets.ezEth1226, + MAX_UINT_256, + ); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleEzEth1226, + ezEth1226SyToken, + addresses.tokens.ezEth, + addresses.dexes.pendle.markets.ezEth1226, + ethers.utils.parseEther("1.0005"), + ); + const agEth1226PendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleAgEth1226, + owner.wallet, + ); + const agEth1226SyToken = await agEth1226PendleToken.SY(); + await flashMintHyETH.approveToken( + agEth1226SyToken, + addresses.dexes.pendle.markets.agEth1226, + MAX_UINT_256, + ); + await flashMintHyETH.approveToken(addresses.tokens.agEth, agEth1226SyToken, MAX_UINT_256); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleAgEth1226, + agEth1226SyToken, + addresses.tokens.agEth, + addresses.dexes.pendle.markets.agEth1226, + ethers.utils.parseEther("1.0005"), + ); + await flashMintHyETH.setSwapData(addresses.tokens.agEth, ADDRESS_ZERO, { + path: [addresses.tokens.agEth, addresses.tokens.rsEth, addresses.tokens.weth], + fees: [], + pool: ADDRESS_ZERO, + poolIds: [ + "0xf1bbc5d95cd5ae25af9916b8a193748572050eb00000000000000000000006bc", + "0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f", + ], + exchange: 5, + }); + // ezETH -> weth pool: https://etherscan.io/address/0xbe80225f09645f172b079394312220637c440a63#code + await flashMintHyETH.setSwapData(addresses.tokens.ezEth, ADDRESS_ZERO, { + path: [addresses.tokens.ezEth, addresses.tokens.weth], + fees: [100], + pool: ADDRESS_ZERO, + poolIds: [], + exchange: 3, + }); + + // 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, + poolIds: [], + exchange: 3, + }); + + const pendleEEth1226PendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleEEth1226, + owner.wallet, + ); + await flashMintHyETH.approveSetToken(setToken.address); + const pendleEEth1226SyToken = await pendleEEth1226PendleToken.SY(); + await flashMintHyETH.approveToken( + pendleEEth1226SyToken, + addresses.dexes.pendle.markets.eEth1226, + MAX_UINT_256, + ); + await flashMintHyETH.setPendleMarket( + addresses.tokens.pendleEEth1226, + pendleEEth1226SyToken, + addresses.tokens.weEth, + addresses.dexes.pendle.markets.eEth1226, + ethers.utils.parseEther("1.0005"), + ); + // 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, + poolIds: [], + 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.2); + const maxAmountIn = inputTokenName == "USDC" ? usdc(2700) : 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, + poolIds: [], + exchange: 0, + }; + swapDataEthToInputToken = { + path: [ETH_ADDRESS, addresses.tokens.weth], + fees: [], + pool: ADDRESS_ZERO, + poolIds: [], + 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, + poolIds: [], + exchange: Exchange.UniV3, + }; + swapDataEthToInputToken = { + path: [addresses.tokens.weth, addresses.tokens.USDC], + fees: [500], + pool: ADDRESS_ZERO, + poolIds: [], + 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/flashMintNAV.spec.ts b/test/integration/ethereum/flashMintNAV.spec.ts index f283d686..836998f3 100644 --- a/test/integration/ethereum/flashMintNAV.spec.ts +++ b/test/integration/ethereum/flashMintNAV.spec.ts @@ -78,7 +78,7 @@ const swapDataWethToUsdc = { }; if (process.env.INTEGRATIONTEST) { - describe.only("FlashMintNAV - Integration Test", async () => { + describe("FlashMintNAV - Integration Test", async () => { let owner: Account; let deployer: DeployHelper; let setV2Setup: SetFixture; diff --git a/test/integration/ethereum/flashMintWrappedRebasing.spec.ts b/test/integration/ethereum/flashMintWrappedRebasing.spec.ts index 0df6aeb4..6832de46 100644 --- a/test/integration/ethereum/flashMintWrappedRebasing.spec.ts +++ b/test/integration/ethereum/flashMintWrappedRebasing.spec.ts @@ -79,7 +79,7 @@ const whales = { }; if (process.env.INTEGRATIONTEST) { - describe.only("FlashMintWrapped - RebasingComponentModule Integration Test", async () => { + describe("FlashMintWrapped - RebasingComponentModule Integration Test", async () => { const TOKEN_TRANSFER_BUFFER = 10; const addresses = PRODUCTION_ADDRESSES; diff --git a/test/integration/ethereum/targetWeightWrapExtensionRebasingNav.spec.ts b/test/integration/ethereum/targetWeightWrapExtensionRebasingNav.spec.ts index 62a222d3..989c349e 100644 --- a/test/integration/ethereum/targetWeightWrapExtensionRebasingNav.spec.ts +++ b/test/integration/ethereum/targetWeightWrapExtensionRebasingNav.spec.ts @@ -56,7 +56,7 @@ const whales = { }; if (process.env.INTEGRATIONTEST) { - describe.only("TargetWeightWrapExtension - RebasingComponentModule Nav Issuance Integration Test", async () => { + describe("TargetWeightWrapExtension - RebasingComponentModule Nav Issuance Integration Test", async () => { const TOKEN_TRANSFER_BUFFER = 10; let owner: Account; diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 8be503da..a2d40dfe 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -46,11 +46,13 @@ import { AirdropExtension__factory } from "../../typechain/factories/AirdropExte 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 { DEXAdapterV3__factory } from "../../typechain/factories/DEXAdapterV3__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 { FlashMintHyETHV2__factory } from "../../typechain/factories/FlashMintHyETHV2__factory"; +import { FlashMintHyETHV3__factory } from "../../typechain/factories/FlashMintHyETHV3__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"; @@ -216,6 +218,10 @@ export default class DeployExtensions { return await new DEXAdapterV2__factory(this._deployerSigner).deploy(); } + public async deployDEXAdapterV3(): Promise { + return await new DEXAdapterV3__factory(this._deployerSigner).deploy(); + } + public async deployExchangeIssuanceLeveraged( wethAddress: Address, quickRouterAddress: Address, @@ -346,6 +352,51 @@ export default class DeployExtensions { ); } + public async deployFlashMintHyETHV3( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + balV2VaultAddress: Address, + setControllerAddress: Address, + debtIssuanceModuleAddress: Address, + stETHAddress: Address, + curveStEthEthPoolAddress: Address, + ) { + const dexAdapter = await this.deployDEXAdapterV3(); + + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapterV3.sol:DEXAdapterV3", + ); + + return await new FlashMintHyETHV3__factory( + // @ts-ignore + { + [linkId]: dexAdapter.address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + balV2Vault: balV2VaultAddress, + weth: wethAddress, + }, + setControllerAddress, + debtIssuanceModuleAddress, + stETHAddress, + curveStEthEthPoolAddress, + ); + } + public async deployFlashMintHyETHV2( wethAddress: Address, quickRouterAddress: Address,