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 index 749219d9..e22d8126 100644 --- a/contracts/exchangeIssuance/FlashMintHyETHV3.sol +++ b/contracts/exchangeIssuance/FlashMintHyETHV3.sol @@ -35,13 +35,13 @@ 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"; +import { DEXAdapterV3 } from "./DEXAdapterV3.sol"; /** * @title FlashMintHyETHV3 */ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { - using DEXAdapterV2 for DEXAdapterV2.Addresses; + using DEXAdapterV3 for DEXAdapterV3.Addresses; using Address for address payable; using Address for address; using SafeMath for uint256; @@ -71,12 +71,12 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { 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; + mapping(address => mapping(address => DEXAdapterV3.SwapData)) public swapData; mapping(address => bool) public erc4626Components; /* ============ State Variables ============ */ - DEXAdapterV2.Addresses public dexAdapter; + DEXAdapterV3.Addresses public dexAdapter; /* ============ Events ============ */ @@ -123,13 +123,13 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { if (_inputToken != _outputToken) { require( _path[0] == _inputToken || - (_inputToken == dexAdapter.weth && _path[0] == DEXAdapterV2.ETH_ADDRESS), + (_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] == DEXAdapterV2.ETH_ADDRESS), + _path[_path.length - 1] == DEXAdapterV3.ETH_ADDRESS), "FlashMint: OUTPUT_TOKEN_NOT_IN_PATH" ); } @@ -139,7 +139,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { /* ========== Constructor ========== */ constructor( - DEXAdapterV2.Addresses memory _dexAddresses, + DEXAdapterV3.Addresses memory _dexAddresses, IController _setController, IDebtIssuanceModule _issuanceModule, IStETH _stETH, @@ -157,7 +157,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { /** * 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 + * 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 @@ -170,9 +170,8 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { _setToken.approve(address(issuanceModule), MAX_UINT256); } - /** - * Issue exact amout of SetToken from ETH + * Issue exact amount of SetToken from ETH * * @param _setToken Address of the SetToken to issue * @param _amountSetToken Amount of SetToken to issue @@ -180,7 +179,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function issueExactSetFromETH( ISetToken _setToken, uint256 _amountSetToken, - DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + DEXAdapterV3.SwapData[] memory _swapDataEthToComponent ) external payable nonReentrant returns (uint256) { uint256 ethSpent = _issueExactSetFromEth(_setToken, _amountSetToken, _swapDataEthToComponent); msg.sender.sendValue(msg.value.sub(ethSpent)); @@ -188,7 +187,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } /** - * Issue exact amout of SetToken from ERC20 token + * Issue exact amount of SetToken from ERC20 token * * @param _setToken Address of the SetToken to issue * @param _amountSetToken Amount of SetToken to issue @@ -202,9 +201,9 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { uint256 _amountSetToken, IERC20 _inputToken, uint256 _maxInputTokenAmount, - DEXAdapterV2.SwapData memory _swapDataInputTokenToEth, - DEXAdapterV2.SwapData memory _swapDataEthToInputToken, - DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + DEXAdapterV3.SwapData memory _swapDataInputTokenToEth, + DEXAdapterV3.SwapData memory _swapDataEthToInputToken, + DEXAdapterV3.SwapData[] memory _swapDataEthToComponent ) external payable nonReentrant returns (uint256) { _inputToken.safeTransferFrom(msg.sender, address(this), _maxInputTokenAmount); @@ -223,13 +222,13 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { * @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) + * @param _swapDataComponentToEth Swap data from component to ETH (for non-standard components) */ function redeemExactSetForETH( ISetToken _setToken, uint256 _amountSetToken, uint256 _minETHOut, - DEXAdapterV2.SwapData[] memory _swapDataComponentToEth + DEXAdapterV3.SwapData[] memory _swapDataComponentToEth ) external payable nonReentrant returns (uint256) { uint256 ethObtained = _redeemExactSetForETH(_setToken, _amountSetToken, _minETHOut, _swapDataComponentToEth); require(ethObtained >= _minETHOut, "FlashMint: INSUFFICIENT_OUTPUT"); @@ -245,15 +244,15 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { * @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) + * @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 + 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); @@ -262,7 +261,6 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { return outputTokenAmount; } - receive() external payable {} /* ============ External Functions (Access controlled) ============ */ @@ -294,7 +292,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { /** * Withdraw slippage to selected address * - * @param _tokens Addresses of tokens to withdraw, specifiy ETH_ADDRESS to withdraw ETH + * @param _tokens Addresses of tokens to withdraw, specify ETH_ADDRESS to withdraw ETH * @param _to Address to send the tokens to */ function withdrawTokens( @@ -302,7 +300,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { address payable _to ) external payable onlyOwner { for (uint256 i = 0; i < _tokens.length; i++) { - if (address(_tokens[i]) == DEXAdapterV2.ETH_ADDRESS) { + if (address(_tokens[i]) == DEXAdapterV3.ETH_ADDRESS) { _to.sendValue(address(this).balance); } else { _tokens[i].safeTransfer(_to, _tokens[i].balanceOf(address(this))); @@ -310,18 +308,17 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } } - /** * Set swap data for specific token pair * - * @param _inputToken Address of the input token + * @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 + DEXAdapterV3.SwapData memory _swapData ) external onlyOwner { swapData[_inputToken][_outputToken] = _swapData; } @@ -367,7 +364,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } 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 + // 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); @@ -378,16 +375,17 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { // Special handling for agETH if (marketData.underlying == address(agETH)) { rsEthAdapter.getRSETHWithETH{value: ethAmount}(""); - marketData.sy.deposit(address(this), address(agETH), ethAmount, 0); + uint256 agEthAmount = agETH.balanceOf(address(this)); + marketData.sy.deposit(address(this), address(agETH), agEthAmount, 0); } else { - marketData.sy.deposit{ value: ethAmount }(msg.sender, address(0), ethAmount, 0); + marketData.sy.deposit{ value: ethAmount }(address(this), address(0), ethAmount, 0); } + marketData.sy.transfer(msg.sender, syAmount); } else { revert("Invalid callback"); } } - /* ============ Internal ============ */ /** @@ -397,7 +395,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function _issueExactSetFromEth( ISetToken _setToken, uint256 _amountSetToken, - DEXAdapterV2.SwapData[] memory _swapDataEthToComponent + DEXAdapterV3.SwapData[] memory _swapDataEthToComponent ) internal returns (uint256) { (address[] memory components, uint256[] memory positions, ) = IDebtIssuanceModule( issuanceModule @@ -418,7 +416,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { ISetToken _setToken, uint256 _amountSetToken, uint256 _minETHOut, - DEXAdapterV2.SwapData[] memory _swapDataComponentToEth + DEXAdapterV3.SwapData[] memory _swapDataComponentToEth ) internal returns (uint256) { uint256 ethBalanceBefore = address(this).balance; @@ -442,9 +440,9 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function _depositIntoComponent( address _component, uint256 _amount, - DEXAdapterV2.SwapData memory _swapData + DEXAdapterV3.SwapData memory _swapData ) internal { - if(_swapData.exchange != DEXAdapterV2.Exchange.None) { + if(_swapData.exchange != DEXAdapterV3.Exchange.None) { _swapEthForExactToken(_component, _amount, _swapData); return; } @@ -468,7 +466,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { if (erc4626Components[_component]) { uint256 assetAmount = IERC4626(_component).previewMint(_amount); address asset = IERC4626(_component).asset(); - _swapEthForExactToken(asset, assetAmount, swapData[DEXAdapterV2.ETH_ADDRESS][asset]); + _swapEthForExactToken(asset, assetAmount, swapData[DEXAdapterV3.ETH_ADDRESS][asset]); IERC4626(_component).mint(_amount, address(this)); return; } @@ -482,12 +480,12 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function _withdrawFromComponent( address _component, uint256 _amount, - DEXAdapterV2.SwapData memory _swapData + DEXAdapterV3.SwapData memory _swapData ) internal { - if(_swapData.exchange != DEXAdapterV2.Exchange.None) { + 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] == DEXAdapterV2.ETH_ADDRESS || _swapData.path[_swapData.path.length - 1] == dexAdapter.weth, "Invalid output 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); @@ -514,7 +512,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { if (erc4626Components[_component]) { address asset = IERC4626(_component).asset(); uint256 assetAmount = IERC4626(_component).redeem(_amount, address(this), address(this)); - _swapExactTokenForEth(IERC20(asset), assetAmount, swapData[asset][DEXAdapterV2.ETH_ADDRESS]); + _swapExactTokenForEth(IERC20(asset), assetAmount, swapData[asset][DEXAdapterV3.ETH_ADDRESS]); return; } revert("Missing Swapdata for non-standard component"); @@ -539,8 +537,8 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } /** - * @dev Withdraw steth from instadapp vault and then swap to eth - * @dev Requries the respective swap data (stETH -> ETH) to be set + * @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 { @@ -568,7 +566,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } /** - * @dev Initiate deposit into pendle by swapping pt for sy + * @dev Initiate deposit into Pendle by swapping pt for sy * @dev Deposit from eth to sy is done in swapCallback */ function _depositIntoPendle( @@ -638,7 +636,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function _swapFromEthToToken( IERC20 _outputToken, uint256 _ethAmount, - DEXAdapterV2.SwapData memory _swapDataEthToOutputToken + DEXAdapterV3.SwapData memory _swapDataEthToOutputToken ) internal returns(uint256 outputTokenAmount) { if(address(_outputToken) == address(dexAdapter.weth)) { outputTokenAmount = _ethAmount; @@ -661,7 +659,7 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { function _swapExactTokenForEth( IERC20 _inputToken, uint256 _inputTokenAmount, - DEXAdapterV2.SwapData memory _swapDataInputTokenToEth + DEXAdapterV3.SwapData memory _swapDataInputTokenToEth ) internal returns (uint256 ethAmount) { if(address(_inputToken) == dexAdapter.weth) { ethAmount = _inputTokenAmount; @@ -678,14 +676,14 @@ contract FlashMintHyETHV3 is Ownable, ReentrancyGuard { } } - function _swapEthForExactToken(address _token, uint256 _amount, DEXAdapterV2.SwapData memory _swapData) internal { + 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] == DEXAdapterV2.ETH_ADDRESS || _swapData.path[0] == dexAdapter.weth, "Invalid input token"); + 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)); diff --git a/test/integration/ethereum/flashMintHyETHV2.spec.ts b/test/integration/ethereum/flashMintHyETHV2.spec.ts index 27ab7d5d..4396ff28 100644 --- a/test/integration/ethereum/flashMintHyETHV2.spec.ts +++ b/test/integration/ethereum/flashMintHyETHV2.spec.ts @@ -50,7 +50,7 @@ const NO_OP_SWAP_DATA: SwapData = { }; if (process.env.INTEGRATIONTEST) { - describe.only("FlashMintHyETHV2 - Integration Test", async () => { + describe("FlashMintHyETHV2 - Integration Test", async () => { const addresses = PRODUCTION_ADDRESSES; let owner: Account; let deployer: DeployHelper; diff --git a/test/integration/ethereum/flashMintHyETHV3.spec.ts b/test/integration/ethereum/flashMintHyETHV3.spec.ts index 25972cce..0f1cec3e 100644 --- a/test/integration/ethereum/flashMintHyETHV3.spec.ts +++ b/test/integration/ethereum/flashMintHyETHV3.spec.ts @@ -33,12 +33,14 @@ enum Exchange { Quickswap, UniV3, Curve, + BalancerV2, } type SwapData = { path: Address[]; fees: number[]; pool: Address; + poolIds: utils.BytesLike[]; exchange: Exchange; }; @@ -46,6 +48,7 @@ const NO_OP_SWAP_DATA: SwapData = { path: [], fees: [], pool: ADDRESS_ZERO, + poolIds: [], exchange: Exchange.None, }; @@ -59,7 +62,7 @@ if (process.env.INTEGRATIONTEST) { let debtIssuanceModule: IDebtIssuanceModule; // const collateralTokenAddress = addresses.tokens.stEth; - setBlockNumber(20930000, true); + setBlockNumber(20930000, false); before(async () => { [owner] = await getAccounts(); @@ -85,6 +88,7 @@ if (process.env.INTEGRATIONTEST) { 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, @@ -133,13 +137,15 @@ if (process.env.INTEGRATIONTEST) { addresses.tokens.pendleEzEth1226, addresses.tokens.pendleEEth1226, addresses.tokens.morphoRe7WETH, + addresses.tokens.pendleAgEth1226, addresses.tokens.USDC, ]; const positions = [ - ethers.utils.parseEther("0.17"), - ethers.utils.parseEther("0.17"), - ethers.utils.parseEther("0.17"), - ethers.utils.parseEther("0.17"), + 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), ]; @@ -148,11 +154,13 @@ if (process.env.INTEGRATIONTEST) { 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: [], }, ]; @@ -161,11 +169,13 @@ if (process.env.INTEGRATIONTEST) { 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], + path: [addresses.tokens.USDC, addresses.tokens.weth], pool: ADDRESS_ZERO, + poolIds: [], }, ]; @@ -205,17 +215,25 @@ if (process.env.INTEGRATIONTEST) { 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.setERC4626Component(addresses.tokens.morphoRe7WETH, true); await flashMintHyETH.approveToken( addresses.tokens.weth, addresses.tokens.morphoRe7WETH, - MAX_UINT_256 + MAX_UINT_256, ); const ezEth1226PendleToken = IPendlePrincipalToken__factory.connect( addresses.tokens.pendleEzEth1226, @@ -235,37 +253,49 @@ if (process.env.INTEGRATIONTEST) { addresses.dexes.pendle.markets.ezEth1226, ethers.utils.parseEther("1.0005"), ); - // 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, - exchange: 3, - }); - - const pendleEEth0926PendleToken = IPendlePrincipalToken__factory.connect( - addresses.tokens.pendleEEth0926, + const agEth1226PendleToken = IPendlePrincipalToken__factory.connect( + addresses.tokens.pendleAgEth1226, owner.wallet, ); - await flashMintHyETH.approveSetToken(setToken.address); - const pendleEEth0926SyToken = await pendleEEth0926PendleToken.SY(); + const agEth1226SyToken = await agEth1226PendleToken.SY(); await flashMintHyETH.approveToken( - pendleEEth0926SyToken, - addresses.dexes.pendle.markets.eEth0926, + agEth1226SyToken, + addresses.dexes.pendle.markets.agEth1226, MAX_UINT_256, ); + await flashMintHyETH.approveToken(addresses.tokens.agEth, agEth1226SyToken, MAX_UINT_256); await flashMintHyETH.setPendleMarket( - addresses.tokens.pendleEEth0926, - pendleEEth0926SyToken, - addresses.tokens.weEth, - addresses.dexes.pendle.markets.eEth0926, + 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, }); @@ -292,6 +322,7 @@ if (process.env.INTEGRATIONTEST) { path: [addresses.tokens.weEth, addresses.tokens.weth], fees: [500], pool: ADDRESS_ZERO, + poolIds: [], exchange: 3, }); }); @@ -301,9 +332,9 @@ if (process.env.INTEGRATIONTEST) { ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { describe(`When inputToken is ${inputTokenName}`, () => { - const ethIn = ether(1001); - const maxAmountIn = inputTokenName == "USDC" ? usdc(4000000) : ethIn; - const setTokenAmount = ether(1000); + 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; @@ -319,12 +350,14 @@ if (process.env.INTEGRATIONTEST) { 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, }; } @@ -335,12 +368,14 @@ if (process.env.INTEGRATIONTEST) { 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, }; } diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index eb7d5fb0..a2d40dfe 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -46,6 +46,7 @@ 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"; @@ -217,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, @@ -355,15 +360,16 @@ export default class DeployExtensions { uniswapV3QuoterAddress: Address, curveCalculatorAddress: Address, curveAddressProviderAddress: Address, + balV2VaultAddress: Address, setControllerAddress: Address, debtIssuanceModuleAddress: Address, stETHAddress: Address, curveStEthEthPoolAddress: Address, ) { - const dexAdapter = await this.deployDEXAdapterV2(); + const dexAdapter = await this.deployDEXAdapterV3(); const linkId = convertLibraryNameToLinkId( - "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", + "contracts/exchangeIssuance/DEXAdapterV3.sol:DEXAdapterV3", ); return await new FlashMintHyETHV3__factory( @@ -381,6 +387,7 @@ export default class DeployExtensions { uniV3Quoter: uniswapV3QuoterAddress, curveAddressProvider: curveAddressProviderAddress, curveCalculator: curveCalculatorAddress, + balV2Vault: balV2VaultAddress, weth: wethAddress, }, setControllerAddress,