diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol new file mode 100644 index 00000000..16d43583 --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -0,0 +1,611 @@ +/* + 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 { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import { IBasicIssuanceModule } from "../interfaces/IBasicIssuanceModule.sol"; +import { IDebtIssuanceModule } from "../interfaces/IDebtIssuanceModule.sol"; +import { IController } from "../interfaces/IController.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; +import { DEXAdapterV2 } from "./DEXAdapterV2.sol"; + +/** + * @title FlashMintDex + * @author Index Cooperative + * @notice Part of a family of contracts that allows users to issue and redeem SetTokens with a single input/output token (ETH/ERC20). + * This contract supports SetTokens whose components have liquidity against WETH on the exchanges found in the DEXAdapterV2 library, and + * does not depend on the use of off-chain APIs for swap quotes. + * The FlashMint SDK (https://github.com/IndexCoop/flash-mint-sdk) provides a unified interface for this and other FlashMint contracts. + */ +contract FlashMintDex is Ownable, ReentrancyGuard { + using DEXAdapterV2 for DEXAdapterV2.Addresses; + using Address for address payable; + using SafeMath for uint256; + using PreciseUnitMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for ISetToken; + + /* ============ Constants ============== */ + + // Placeholder address to identify ETH where it is treated as if it was an ERC20 token + address constant public ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /* ============ State Variables ============ */ + + address public immutable WETH; + IController public immutable setController; + IController public immutable indexController; + DEXAdapterV2.Addresses public dexAdapter; + + /* ============ Structs ============ */ + struct IssueRedeemParams { + ISetToken setToken; // The address of the SetToken to be issued/redeemed + uint256 amountSetToken; // The amount of SetTokens to issue/redeem + DEXAdapterV2.SwapData[] componentSwapData; // The swap data from WETH to each component token + address issuanceModule; // The address of the issuance module to be used + bool isDebtIssuance; // A flag indicating whether the issuance module is a debt issuance module + } + + struct PaymentInfo { + IERC20 token; // The address of the input/output token for issuance/redemption + uint256 limitAmt; // Max/min amount of payment token spent/received + DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from payment token to WETH + DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to payment token + } + + /* ============ Events ============ */ + + event FlashMint( + address indexed _recipient, // The recipient address of the issued SetTokens + ISetToken indexed _setToken, // The issued SetToken + IERC20 indexed _inputToken, // The address of the input asset(ERC20/ETH) used to issue the SetTokens + uint256 _amountInputToken, // The amount of input tokens used for issuance + uint256 _amountSetIssued // The amount of SetTokens received by the recipient + ); + + event FlashRedeem( + address indexed _recipient, // The recipient adress of the output tokens obtained for redemption + ISetToken indexed _setToken, // The redeemed SetToken + IERC20 indexed _outputToken, // The address of output asset(ERC20/ETH) received by the recipient + uint256 _amountSetRedeemed, // The amount of SetTokens redeemed for output tokens + uint256 _amountOutputToken // The amount of output tokens received by the recipient + ); + + /* ============ Modifiers ============ */ + + modifier isValidModule(address _issuanceModule) { + require(setController.isModule(_issuanceModule) || indexController.isModule(_issuanceModule), "FlashMint: INVALID ISSUANCE MODULE"); + _; + } + + modifier isValidModuleAndSet(address _issuanceModule, address _setToken) { + require( + setController.isModule(_issuanceModule) && setController.isSet(_setToken) || + indexController.isModule(_issuanceModule) && indexController.isSet(_setToken), + "FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN" + ); + _; + } + + /** + * Initializes the contract with controller and DEXAdapterV2 library addresses. + * + * @param _setController Address of the legacy Set Protocol controller contract + * @param _indexController Address of the Index Coop controller contract + * @param _dexAddresses Struct containing addresses for the DEXAdapterV2 library + */ + constructor( + IController _setController, + IController _indexController, + DEXAdapterV2.Addresses memory _dexAddresses + ) + public + { + setController = _setController; + indexController = _indexController; + dexAdapter = _dexAddresses; + WETH = _dexAddresses.weth; + } + + /* ============ External Functions ============ */ + + /** + * Withdraw slippage to selected address + * + * @param _tokens Addresses of tokens to withdraw, specifiy ETH_ADDRESS to withdraw ETH + * @param _to Address to send the tokens to + */ + function withdrawTokens(IERC20[] calldata _tokens, address payable _to) external onlyOwner payable { + for(uint256 i = 0; i < _tokens.length; i++) { + if(address(_tokens[i]) == ETH_ADDRESS){ + _to.sendValue(address(this).balance); + } + else{ + _tokens[i].safeTransfer(_to, _tokens[i].balanceOf(address(this))); + } + } + } + + receive() external payable { + // required for weth.withdraw() to work properly + require(msg.sender == WETH, "FlashMint: DIRECT DEPOSITS NOT ALLOWED"); + } + + /* ============ Public Functions ============ */ + + + /** + * Runs all the necessary approval functions required for a given ERC20 token. + * This function can be called when a new token is added to a SetToken during a + * rebalance. + * + * @param _token Address of the token which needs approval + * @param _spender Address of the spender which will be approved to spend token. (Must be a whitlisted issuance module) + */ + function approveToken(IERC20 _token, address _spender) public isValidModule(_spender) { + _safeApprove(_token, _spender, type(uint256).max); + } + + /** + * Runs all the necessary approval functions required for a list of ERC20 tokens. + * + * @param _tokens Addresses of the tokens which need approval + * @param _spender Address of the spender which will be approved to spend token. (Must be a whitlisted issuance module) + */ + function approveTokens(IERC20[] calldata _tokens, address _spender) external { + for (uint256 i = 0; i < _tokens.length; i++) { + approveToken(_tokens[i], _spender); + } + } + + /** + * Runs all the necessary approval functions required before issuing + * or redeeming a SetToken. This function need to be called only once before the first time + * this smart contract is used on any particular SetToken. + * + * @param _setToken Address of the SetToken being initialized + * @param _issuanceModule Address of the issuance module which will be approved to spend component tokens. + */ + function approveSetToken(ISetToken _setToken, address _issuanceModule) external { + address[] memory components = _setToken.getComponents(); + for (uint256 i = 0; i < components.length; i++) { + approveToken(IERC20(components[i]), _issuanceModule); + } + } + + /** + * Gets the amount of input token required to issue a given quantity of set token with the provided issuance params. + * This function is not marked view, but should be static called from frontends. + * This constraint is due to the need to interact with the Uniswap V3 quoter contract + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * @param _swapDataInputTokenToWeth Swap data to trade input token for WETH. Use empty swap data if input token is ETH or WETH. + * + * @return Amount of input tokens required to perform the issuance + */ + function getIssueExactSet( + IssueRedeemParams memory _issueParams, + DEXAdapterV2.SwapData memory _swapDataInputTokenToWeth + ) + external + returns (uint256) + { + uint256 totalWethNeeded = _getWethCostsForIssue(_issueParams); + return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalWethNeeded); + } + + /** + * Gets the amount of specified payment token expected to be received after redeeming + * a given quantity of set token with the provided redemption params. + * This function is not marked view, but should be static called from frontends. + * This constraint is due to the need to interact with the Uniswap V3 quoter contract + * + * @param _redeemParams Struct containing addresses, amounts, and swap data for redemption + * @param _swapDataWethToOutputToken Swap data to trade WETH for output token. Use empty swap data if output token is ETH or WETH. + * + * @return Amount of output tokens expected after performing redemption + */ + function getRedeemExactSet( + IssueRedeemParams memory _redeemParams, + DEXAdapterV2.SwapData memory _swapDataWethToOutputToken + ) + external + returns (uint256) + { + uint256 totalWethReceived = _getWethReceivedForRedeem(_redeemParams); + return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); + } + + /** + * Issues an exact amount of SetTokens for given amount of ETH. + * Leftover ETH is returned to the caller if the amount is above _minEthRefund, + * otherwise it is kept by the contract in the form of WETH to save gas. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * @param _minEthRefund Minimum amount of unused ETH to be returned to the caller. Set to 0 to return any leftover amount. + * + * @return ethSpent Amount of ETH spent + */ + function issueExactSetFromETH(IssueRedeemParams memory _issueParams, uint256 _minEthRefund) + external + payable + isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) + nonReentrant + returns (uint256 ethSpent) + { + require(msg.value > 0, "FlashMint: NO ETH SENT"); + + IWETH(WETH).deposit{value: msg.value}(); + + uint256 ethUsedForIssuance = _issueExactSetFromWeth(_issueParams); + + uint256 leftoverETH = msg.value.sub(ethUsedForIssuance); + if (leftoverETH > _minEthRefund) { + IWETH(WETH).withdraw(leftoverETH); + payable(msg.sender).sendValue(leftoverETH); + } + ethSpent = msg.value.sub(leftoverETH); + + emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), ethSpent, _issueParams.amountSetToken); + } + + /** + * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. + * Leftover funds are swapped back to the payment token and returned to the caller if the value is above _minRefundValueInWeth, + * otherwise the leftover funds are kept by the contract in the form of WETH to save gas. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * @param _paymentInfo Struct containing input token address, max amount to spend, and swap data to trade for WETH + * @param _minRefundValueInWeth Minimum value of leftover WETH to be swapped back to input token and returned to the caller. Set to 0 to return any leftover amount. + * + * @return paymentTokenSpent Amount of input token spent + */ + function issueExactSetFromERC20(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo, uint256 _minRefundValueInWeth) + external + isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) + nonReentrant + returns (uint256 paymentTokenSpent) + { + _paymentInfo.token.safeTransferFrom(msg.sender, address(this), _paymentInfo.limitAmt); + uint256 wethReceived = _swapPaymentTokenForWeth(_paymentInfo.token, _paymentInfo.limitAmt, _paymentInfo.swapDataTokenToWeth); + + uint256 wethSpent = _issueExactSetFromWeth(_issueParams); + require(wethSpent <= wethReceived, "FlashMint: OVERSPENT WETH"); + uint256 leftoverWeth = wethReceived.sub(wethSpent); + uint256 paymentTokenReturned = 0; + + if (leftoverWeth > _minRefundValueInWeth) { + paymentTokenReturned = _swapWethForPaymentToken(leftoverWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + _paymentInfo.token.safeTransfer(msg.sender, paymentTokenReturned); + } + + paymentTokenSpent = _paymentInfo.limitAmt.sub(paymentTokenReturned); + + emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSpent, _issueParams.amountSetToken); + } + + /** + * Redeems an exact amount of SetTokens for ETH. + * The SetToken must be approved by the sender to this contract. + * + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance + * + * @return ethReceived Amount of ETH received + */ + function redeemExactSetForETH(IssueRedeemParams memory _redeemParams, uint256 _minEthReceive) + external + isValidModuleAndSet(_redeemParams.issuanceModule, address(_redeemParams.setToken)) + nonReentrant + returns (uint256 ethReceived) + { + _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + + ethReceived = _sellComponentsForWeth(_redeemParams); + require(ethReceived >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); + + IWETH(WETH).withdraw(ethReceived); + payable(msg.sender).sendValue(ethReceived); + + emit FlashRedeem(msg.sender, _redeemParams.setToken, IERC20(ETH_ADDRESS), _redeemParams.amountSetToken, ethReceived); + return ethReceived; + } + + /** + * Redeems an exact amount of SetTokens for an ERC20 token. + * The SetToken must be approved by the sender to this contract. + * + * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance + * + * @return outputTokenReceived Amount of output token received + */ + function redeemExactSetForERC20(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) + external + isValidModuleAndSet(_redeemParams.issuanceModule, address(_redeemParams.setToken)) + nonReentrant + returns (uint256 outputTokenReceived) + { + _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + + uint256 wethReceived = _sellComponentsForWeth(_redeemParams); + outputTokenReceived = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + require(outputTokenReceived >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + + _paymentInfo.token.safeTransfer(msg.sender, outputTokenReceived); + + emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputTokenReceived); + } + + /* ============ Internal Functions ============ */ + + /** + * 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 + */ + function _safeApprove(IERC20 _token, address _spender, uint256 _requiredAllowance) internal { + uint256 allowance = _token.allowance(address(this), _spender); + if (allowance < _requiredAllowance) { + _token.safeIncreaseAllowance(_spender, type(uint256).max - allowance); + } + } + + /** + * Swaps a given amount of an ERC20 token for WETH using the DEXAdapter. + * + * @param _paymentToken Address of the ERC20 payment token + * @param _paymentTokenAmount Amount of payment token to swap + * @param _swapData Swap data from input token to WETH + * + * @return amountWethOut Amount of WETH received after the swap + */ + function _swapPaymentTokenForWeth( + IERC20 _paymentToken, + uint256 _paymentTokenAmount, + DEXAdapterV2.SwapData memory _swapData + ) + internal + returns (uint256 amountWethOut) + { + if (_paymentToken == IERC20(WETH)) { + return _paymentTokenAmount; + } + + return dexAdapter.swapExactTokensForTokens( + _paymentTokenAmount, + 0, + _swapData + ); + } + + /** + * Swaps a given amount of an WETH for ERC20 using the DEXAdapter. + * + * @param _wethAmount Amount of WETH to swap for input token + * @param _paymentToken Address of the input token + * @param _swapData Swap data from WETH to input token + * + * @return amountOut Amount of ERC20 received after the swap + */ + function _swapWethForPaymentToken(uint256 _wethAmount, IERC20 _paymentToken, DEXAdapterV2.SwapData memory _swapData) + internal + returns (uint256 amountOut) + { + // If the payment token is equal to WETH we don't have to trade + if (_paymentToken == IERC20(WETH)) { + return _wethAmount; + } + + return dexAdapter.swapExactTokensForTokens( + _wethAmount, + 0, + _swapData + ); + } + + /** + * Issues an exact amount of SetTokens for given amount of WETH. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return totalWethSpent Amount of WETH used to buy components + */ + function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSpent) + { + totalWethSpent = _buyComponentsWithWeth(_issueParams); + IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); + } + + /** + * Acquires SetToken components by executing swaps whose callata is passed in _componentSwapData. + * Acquired components are then used to issue the SetTokens. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return totalWethSpent Total amount of WETH spent to buy components + */ + function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSpent) { + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); + require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + + totalWethSpent = 0; + for (uint256 i = 0; i < components.length; i++) { + require(_issueParams.setToken.getExternalPositionModules(components[i]).length == 0, "FlashMint: EXTERNAL POSITION MODULES NOT SUPPORTED"); + uint256 wethSold = dexAdapter.swapTokensForExactTokens(componentUnits[i], type(uint256).max, _issueParams.componentSwapData[i]); + totalWethSpent = totalWethSpent.add(wethSold); + } + } + + /** + * Calculates the amount of WETH required to buy all components required for issuance. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return totalWethCosts Amount of WETH needed to swap into component units required for issuance + */ + function _getWethCostsForIssue(IssueRedeemParams memory _issueParams) + internal + returns (uint256 totalWethCosts) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); + + require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + + totalWethCosts = 0; + for (uint256 i = 0; i < components.length; i++) { + if (components[i] == address(WETH)) { + totalWethCosts += componentUnits[i]; + } else { + totalWethCosts += dexAdapter.getAmountIn( + _issueParams.componentSwapData[i], + componentUnits[i] + ); + } + } + } + + /** + * Transfers given amount of set token from the sender and redeems it for underlying components. + * Obtained component tokens are sent to this contract. + * + * @param _setToken Address of the SetToken to be redeemed + * @param _amount Amount of SetToken to be redeemed + */ + function _redeem(ISetToken _setToken, uint256 _amount, address _issuanceModule) internal returns (uint256) { + _setToken.safeTransferFrom(msg.sender, address(this), _amount); + IBasicIssuanceModule(_issuanceModule).redeem(_setToken, _amount, address(this)); + } + + /** + * Sells redeemed components for WETH. + * + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance + * + * @return totalWethReceived Total amount of WETH received after liquidating all SetToken components + */ + function _sellComponentsForWeth(IssueRedeemParams memory _redeemParams) + internal + returns (uint256 totalWethReceived) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + + totalWethReceived = 0; + for (uint256 i = 0; i < components.length; i++) { + require(_redeemParams.setToken.getExternalPositionModules(components[i]).length == 0, "FlashMint: EXTERNAL POSITION MODULES NOT SUPPORTED"); + uint256 wethBought = dexAdapter.swapExactTokensForTokens(componentUnits[i], 0, _redeemParams.componentSwapData[i]); + totalWethReceived = totalWethReceived.add(wethBought); + } + } + + /** + * Calculates the amount of WETH received for selling off all components after redemption. + * + * @param _redeemParams Struct containing addresses, amounts, and swap data for redemption + * + * @return totalWethReceived Amount of WETH received after swapping all component tokens + */ + function _getWethReceivedForRedeem(IssueRedeemParams memory _redeemParams) + internal + returns (uint256 totalWethReceived) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + + require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + + totalWethReceived = 0; + for (uint256 i = 0; i < components.length; i++) { + if (components[i] == address(WETH)) { + totalWethReceived += componentUnits[i]; + } else { + totalWethReceived += dexAdapter.getAmountOut( + _redeemParams.componentSwapData[i], + componentUnits[i] + ); + } + } + } + + /** + * Returns component positions required for issuance + * + * @param _issuanceModule Address of issuance Module to use + * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * @param _setToken Set token to issue + * @param _amountSetToken Amount of set token to issue + */ + function getRequiredIssuanceComponents(address _issuanceModule, bool _isDebtIssuance, ISetToken _setToken, uint256 _amountSetToken) public view returns(address[] memory components, uint256[] memory positions) { + if(_isDebtIssuance) { + (components, positions, ) = IDebtIssuanceModule(_issuanceModule).getRequiredComponentIssuanceUnits(_setToken, _amountSetToken); + } + else { + (components, positions) = IBasicIssuanceModule(_issuanceModule).getRequiredComponentUnitsForIssue(_setToken, _amountSetToken); + } + } + + /** + * Returns component positions required for Redemption + * + * @param _issuanceModule Address of issuance Module to use + * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * @param _setToken Set token to issue + * @param _amountSetToken Amount of set token to issue + */ + function getRequiredRedemptionComponents(address _issuanceModule, bool _isDebtIssuance, ISetToken _setToken, uint256 _amountSetToken) public view returns(address[] memory components, uint256[] memory positions) { + if(_isDebtIssuance) { + (components, positions, ) = IDebtIssuanceModule(_issuanceModule).getRequiredComponentRedemptionUnits(_setToken, _amountSetToken); + } + else { + components = _setToken.getComponents(); + positions = new uint256[](components.length); + for(uint256 i = 0; i < components.length; i++) { + uint256 unit = uint256(_setToken.getDefaultPositionRealUnit(components[i])); + positions[i] = unit.preciseMul(_amountSetToken); + } + } + } +} diff --git a/contracts/interfaces/IBasicIssuanceModule.sol b/contracts/interfaces/IBasicIssuanceModule.sol index 0cd1ba07..3490cfb0 100644 --- a/contracts/interfaces/IBasicIssuanceModule.sol +++ b/contracts/interfaces/IBasicIssuanceModule.sol @@ -22,4 +22,5 @@ interface IBasicIssuanceModule { ) external view returns(address[] memory, uint256[] memory); function issue(ISetToken _setToken, uint256 _quantity, address _to) external; function redeem(ISetToken _token, uint256 _quantity, address _to) external; + function initialize(ISetToken _setToken, address _preIssueHook) external; } diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 42782117..cd6fa87d 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -38,6 +38,11 @@ export const PRODUCTION_ADDRESSES = { rswEth: "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0", acrossWethLP: "0x28F77208728B0A45cAb24c4868334581Fe86F95B", morphoRe7WETH: "0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0", + wstEth: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + sfrxEth: "0xac3E018457B222d93114458476f3E3416Abbe38F", + osEth: "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", + comp: "0xc00e94Cb662C3520282E6f5717214004A7f26888", + dpi: "0x1494CA1F11D487c2bBe4543E90080AeBa4BA3C2b", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", @@ -79,13 +84,16 @@ export const PRODUCTION_ADDRESSES = { eEth1226: "0x7d372819240D14fB477f17b964f95F33BeB4c704", }, }, + dexAdapterV2: "0x88858930B3F1946A5C41a5deD7B5335431d5dE8D", }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", + basicIssuanceModule: "0xd8EF3cACe8b4907117a45B0b125c68560532F94D", debtIssuanceModule: "0x39F024d621367C044BacE2bf0Fb15Fb3612eCB92", debtIssuanceModuleV2: "0x69a592D2129415a4A1d1b1E309C17051B7F28d57", aaveLeverageModule: "0x251Bd1D42Df1f153D86a5BA2305FaADE4D5f51DC", compoundLeverageModule: "0x8d5174eD1dd217e240fDEAa52Eb7f4540b04F419", + setTokenCreator: "0xeF72D3278dC3Eba6Dc2614965308d1435FFd748a", }, setFork: { controller: "0xD2463675a099101E36D85278494268261a66603A", diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts new file mode 100644 index 00000000..b12c56ea --- /dev/null +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -0,0 +1,889 @@ +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, BigNumber } from "ethers"; +import { + IDebtIssuanceModule, + IDebtIssuanceModule__factory, + SetToken, + SetToken__factory, + SetTokenCreator, + SetTokenCreator__factory, + FlashMintDex, + IERC20__factory, + IWETH, + IWETH__factory, + IBasicIssuanceModule, + IBasicIssuanceModule__factory, +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { ether } from "@utils/index"; +import { impersonateAccount } from "./utils"; + +const expect = getWaffleExpect(); + +enum Exchange { + None, + Sushiswap, + Quickswap, + UniV3, + Curve, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +type IssueRedeemParams = { + setToken: Address; + amountSetToken: BigNumber; + componentSwapData: SwapData[]; + issuanceModule: Address; + isDebtIssuance: boolean; +}; + +type PaymentInfo = { + token: Address; + limitAmt: BigNumber; + swapDataTokenToWeth: SwapData; + swapDataWethToToken: SwapData; +}; + +const addresses = PRODUCTION_ADDRESSES; + +const swapDataEmpty = { + exchange: Exchange.None, + fees: [], + path: [], + pool: ADDRESS_ZERO, +}; + +const swapDataUsdcToWeth = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.USDC, addresses.tokens.weth], + pool: ADDRESS_ZERO, +}; + +const swapDataWethToUsdc = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.USDC], + pool: ADDRESS_ZERO, +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintDex - Integration Test", async () => { + let owner: Account; + let deployer: DeployHelper; + let legacySetTokenCreator: SetTokenCreator; + let setTokenCreator: SetTokenCreator; + let legacyBasicIssuanceModule: IBasicIssuanceModule; + let debtIssuanceModule: IDebtIssuanceModule; + + setBlockNumber(20385208, true); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + legacySetTokenCreator = SetTokenCreator__factory.connect( + addresses.set.setTokenCreator, + owner.wallet, + ); + + setTokenCreator = SetTokenCreator__factory.connect( + addresses.setFork.setTokenCreator, + owner.wallet, + ); + + legacyBasicIssuanceModule = IBasicIssuanceModule__factory.connect( + addresses.set.basicIssuanceModule, + owner.wallet, + ); + + debtIssuanceModule = IDebtIssuanceModule__factory.connect( + addresses.setFork.debtIssuanceModuleV2, + owner.wallet, + ); + }); + + context("When FlashMintDex contract is deployed", () => { + let flashMintDex: FlashMintDex; + + before(async () => { + flashMintDex = await deployer.extensions.deployFlashMintDex( + 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.dexAdapterV2, + addresses.set.controller, + addresses.setFork.controller, + ); + }); + + it("weth address is set correctly", async () => { + const returnedAddresses = await flashMintDex.dexAdapter(); + expect(returnedAddresses.weth).to.eq(utils.getAddress(addresses.tokens.weth)); + }); + + it("sushi router address is set correctly", async () => { + const returnedAddresses = await flashMintDex.dexAdapter(); + expect(returnedAddresses.sushiRouter).to.eq( + utils.getAddress(addresses.dexes.sushiswap.router), + ); + }); + + it("uniV2 router address is set correctly", async () => { + const returnedAddresses = await flashMintDex.dexAdapter(); + expect(returnedAddresses.quickRouter).to.eq(utils.getAddress(addresses.dexes.uniV2.router)); + }); + + it("uniV3 router address is set correctly", async () => { + const returnedAddresses = await flashMintDex.dexAdapter(); + expect(returnedAddresses.uniV3Router).to.eq(utils.getAddress(addresses.dexes.uniV3.router)); + }); + + it("Set controller address is set correctly", async () => { + expect(await flashMintDex.setController()).to.eq( + utils.getAddress(addresses.set.controller), + ); + }); + + it("Index controller address is set correctly", async () => { + expect(await flashMintDex.indexController()).to.eq( + utils.getAddress(addresses.setFork.controller), + ); + }); + + it("should revert when eth is sent to the contract", async () => { + await expect( + owner.wallet.sendTransaction({ to: flashMintDex.address, value: ether(1) }) + ).to.be.revertedWith("FlashMint: DIRECT DEPOSITS NOT ALLOWED"); + }); + + context("when SetToken is deployed on legacy Set Protocol", () => { + let setToken: SetToken; + let issueParams: IssueRedeemParams; + let redeemParams: IssueRedeemParams; + const setTokenAmount = ether(100); + + const components = [ + addresses.tokens.wbtc, + addresses.tokens.weth, + addresses.tokens.dpi, + ]; + const positions = [ + BigNumber.from("84581"), + BigNumber.from("11556875581911945"), + BigNumber.from("218100363826474304"), + ]; + + const modules = [addresses.set.basicIssuanceModule]; + const tokenName = "BED Index"; + const tokenSymbol = "BED"; + + const componentSwapDataIssue = [ + { + exchange: Exchange.UniV3, + fees: [3000], + path: [addresses.tokens.weth, addresses.tokens.wbtc], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [3000], + path: [addresses.tokens.weth, addresses.tokens.dpi], + pool: ADDRESS_ZERO, + }, + ]; + + const componentSwapDataRedeem = componentSwapDataIssue.map(item => ({ + ...item, + path: [...item.path].reverse(), + })); + + before(async () => { + const tx = await legacySetTokenCreator.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 legacyBasicIssuanceModule.initialize( + setToken.address, + ADDRESS_ZERO, + ); + await flashMintDex.approveSetToken(setToken.address, legacyBasicIssuanceModule.address); + + issueParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, + }; + + redeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataRedeem, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, + }; + }); + + it("setToken is deployed correctly", async () => { + expect(await setToken.symbol()).to.eq(tokenSymbol); + }); + + it("Can return ETH quantity required to issue legacy set token", async () => { + const ethRequired = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + expect(ethRequired).to.eq(BigNumber.from("3498514628413285230")); + }); + + it("Can return USDC quantity required to issue legacy set token", async () => { + const usdcRequired = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + expect(usdcRequired).to.eq(BigNumber.from("11075363007")); + }); + + it("Can issue legacy set token from ETH", async () => { + const ethEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + const maxEthIn = ethEstimate.mul(1005).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const ethBalanceBefore = await owner.wallet.getBalance(); + await flashMintDex.issueExactSetFromETH(issueParams, 0, { value: maxEthIn }); + const ethBalanceAfter = await owner.wallet.getBalance(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(ethBalanceAfter).to.gte(ethBalanceBefore.sub(maxEthIn)); + }); + + it("Can issue legacy set token from WETH", async () => { + const wethEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: wethEstimate.mul(1005).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); + await wethToken.deposit({ value: paymentInfo.limitAmt }); + wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, 0); + const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + }); + + it("Can issue set token from USDC", async () => { + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcEstimate.mul(1005).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdcToken.connect(whaleSigner).transfer(owner.address, paymentInfo.limitAmt); + usdcToken.approve(flashMintDex.address, paymentInfo.limitAmt); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, 0); + const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + }); + + describe("When legacy set token has been issued", () => { + beforeEach(async () => { + await flashMintDex.issueExactSetFromETH( + issueParams, + 0, + { + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), + }, + ); + await setToken.approve(flashMintDex.address, setTokenAmount); + }); + + it("Can return ETH quantity received when redeeming legacy set token", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + expect(ethReceivedEstimate).to.eq(BigNumber.from("3492695444625661021")); + }); + + it("Can return USDC quantity received when redeeming legacy set token", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + expect(usdcReceivedEstimate).to.eq(BigNumber.from("11054123420")); + }); + + it("Can redeem legacy set token for ETH", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + const minAmountOut = ethReceivedEstimate.mul(995).div(1000); // 0.5% slippage + const outputTokenBalanceBefore = await owner.wallet.getBalance(); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await owner.wallet.getBalance(); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gte(outputTokenBalanceBefore.add(minAmountOut)); + }); + + it("Can redeem legacy set token for WETH", async () => { + const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: wethReceivedEstimate.mul(995).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); + const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForERC20(redeemParams, paymentInfo); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); + }); + + it("Can redeem set token for USDC", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcReceivedEstimate.mul(995).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); + const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForERC20(redeemParams, paymentInfo); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); + }); + }); + }); + + context("when setToken is deployed on Index Protocol", () => { + let setToken: SetToken; + let issueParams: IssueRedeemParams; + let redeemParams: IssueRedeemParams; + const setTokenAmount = ether(10); + + const components = [ + addresses.tokens.wstEth, + addresses.tokens.rETH, + addresses.tokens.swETH, + addresses.tokens.comp, + ]; + const positions = [ + ethers.utils.parseEther("0.25"), + ethers.utils.parseEther("0.25"), + ethers.utils.parseEther("0.25"), + ethers.utils.parseEther("0.25"), + ]; + + const componentSwapDataIssue = [ + { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.weth, addresses.tokens.wstEth], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.weth, addresses.tokens.rETH], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.swETH], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.Sushiswap, + fees: [], + path: [addresses.tokens.weth, addresses.tokens.comp], + pool: ADDRESS_ZERO, + }, + ]; + + const componentSwapDataRedeem = [ + { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.wstEth, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.rETH, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.swETH, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + { + exchange: Exchange.Sushiswap, + fees: [], + path: [addresses.tokens.comp, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }, + ]; + + const modules = [addresses.setFork.debtIssuanceModuleV2]; + const tokenName = "Simple Index"; + const tokenSymbol = "icSimple"; + + 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 flashMintDex.approveSetToken(setToken.address, debtIssuanceModule.address); + + issueParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + + redeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataRedeem, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: false, + }; + }); + + it("setToken is deployed correctly", async () => { + expect(await setToken.symbol()).to.eq(tokenSymbol); + }); + + it("Can return ETH quantity required to issue set token", async () => { + const ethRequired = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + expect(ethRequired).to.eq(BigNumber.from("8427007884995480469")); + }); + + it("Can return USDC quantity required to issue set token", async () => { + const usdcRequired = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + expect(usdcRequired).to.eq(BigNumber.from("26678902800")); + }); + + context("When issuing from ETH or WETH", () => { + let ethRequiredEstimate: BigNumber; + let maxEthIn: BigNumber; + let setTokenBalanceBefore: BigNumber; + let ethBalanceBefore: BigNumber; + let excessEth: BigNumber; + let wethToken: IWETH; + let wethInContractBefore: BigNumber; + + beforeEach(async () => { + ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + maxEthIn = ethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage + excessEth = maxEthIn.sub(ethRequiredEstimate); + setTokenBalanceBefore = await setToken.balanceOf(owner.address); + ethBalanceBefore = await owner.wallet.getBalance(); + wethToken = IWETH__factory.connect(addresses.tokens.weth, owner.wallet); + wethInContractBefore = await wethToken.balanceOf(flashMintDex.address); + }); + + it("Can return unused ETH to the user if above a specified amount", async () => { + const minEthRefund = ether(0.001); + const tx = await flashMintDex.issueExactSetFromETH(issueParams, minEthRefund, { value: maxEthIn }); + const receipt = await tx.wait(); + const gasCost = receipt.gasUsed.mul(tx.gasPrice); + const ethBalanceAfter = await owner.wallet.getBalance(); + const wethInContractAfter = await wethToken.balanceOf(flashMintDex.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(ethBalanceAfter).to.eq(ethBalanceBefore.sub(maxEthIn).sub(gasCost).add(excessEth)); + expect(wethInContractAfter).to.eq(wethInContractBefore); + }); + + it("Can leave unused ETH in the contract as WETH if below a specified amount", async () => { + const minEthRefund = ether(1); + const tx = await flashMintDex.issueExactSetFromETH(issueParams, minEthRefund, { value: maxEthIn }); + const receipt = await tx.wait(); + const gasCost = receipt.gasUsed.mul(tx.gasPrice); + const ethBalanceAfter = await owner.wallet.getBalance(); + const wethInContractAfter = await wethToken.balanceOf(flashMintDex.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(ethBalanceAfter).to.eq(ethBalanceBefore.sub(maxEthIn).sub(gasCost)); + expect(wethInContractAfter).to.eq(wethInContractBefore.add(excessEth)); + }); + + it("Can return unused WETH to the user if above a specified amount", async () => { + const minWethRefund = ether(0.01); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ethRequiredEstimate.mul(1005).div(1000), // 0.5% slippage, + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + await wethToken.deposit({ value: paymentInfo.limitAmt }); + wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); + const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, minWethRefund); + const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + const wethInContractAfter = await wethToken.balanceOf(flashMintDex.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.eq(inputTokenBalanceBefore.sub(paymentInfo.limitAmt).add(excessEth)); + expect(wethInContractAfter).to.eq(wethInContractBefore); + }); + + it("Can leave unused WETH in contract if below a specified amount", async () => { + const minWethRefund = ether(1); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ethRequiredEstimate.mul(1005).div(1000), // 0.5% slippage, + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + await wethToken.deposit({ value: paymentInfo.limitAmt }); + wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); + const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, minWethRefund); + const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + const wethInContractAfter = await wethToken.balanceOf(flashMintDex.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.eq(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + expect(wethInContractAfter).to.eq(wethInContractBefore.add(excessEth)); + }); + }); + + it("Can issue set token from USDC and return leftover funds to user as USDC", async () => { + const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const minRefundValueInWeth = ether(0); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcRequiredEstimate.mul(1005).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdcToken.connect(whaleSigner).transfer(owner.address, paymentInfo.limitAmt); + usdcToken.approve(flashMintDex.address, paymentInfo.limitAmt); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, minRefundValueInWeth); + const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + }); + + it("Can issue set token from USDC and leave unused funds in the contract as WETH", async () => { + const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const minRefundValueInWeth = ether(1); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcRequiredEstimate.mul(1005).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdcToken.connect(whaleSigner).transfer(owner.address, paymentInfo.limitAmt); + usdcToken.approve(flashMintDex.address, paymentInfo.limitAmt); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + const wethToken = IWETH__factory.connect(addresses.tokens.weth, owner.wallet); + const wethInContractBefore = await wethToken.balanceOf(flashMintDex.address); + + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo, minRefundValueInWeth); + const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const wethInContractAfter = await wethToken.balanceOf(flashMintDex.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.eq(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + expect(wethInContractAfter).to.gt(wethInContractBefore); + }); + + describe("When set token has been issued", () => { + beforeEach(async () => { + await flashMintDex.issueExactSetFromETH( + issueParams, + 0, + { + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), + }, + ); + await setToken.approve(flashMintDex.address, setTokenAmount); + }); + + it("Can return ETH quantity received when redeeming set token", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + expect(ethReceivedEstimate).to.eq(BigNumber.from("8424778030321284651")); + }); + + it("Can return USDC quantity received when redeeming set token", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + expect(usdcReceivedEstimate).to.eq(BigNumber.from("26650292996")); + }); + + it("Can redeem set token for ETH", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + const minAmountOut = ethReceivedEstimate.mul(995).div(1000); // 0.5% slippage + const outputTokenBalanceBefore = await owner.wallet.getBalance(); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await owner.wallet.getBalance(); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); + }); + + it("Can redeem set token for WETH", async () => { + const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: wethReceivedEstimate.mul(995).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); + const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForERC20(redeemParams, paymentInfo); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); + }); + + it("Can redeem set token for USDC", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcReceivedEstimate.mul(995).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); + const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await flashMintDex.redeemExactSetForERC20(redeemParams, paymentInfo); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); + }); + }); + + context("When invalid inputs are given", () => { + let invalidIssueParams: IssueRedeemParams; + beforeEach(async () => { + invalidIssueParams = { ...issueParams }; + }); + + it("Should revert when trying to issue set token with invalid swap data", async () => { + const invalidSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.weth, addresses.tokens.comp], + pool: ADDRESS_ZERO, + }; + + invalidIssueParams.componentSwapData = [invalidSwapData]; + + await expect( + flashMintDex.issueExactSetFromETH( + invalidIssueParams, + 0, + { + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), + }, + ), + ).to.be.revertedWith("FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + }); + + it("should revert when not enough ETH is sent for issuance", async () => { + const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(invalidIssueParams, swapDataEmpty); + const notEnoughEth = ethRequiredEstimate.div(2); + + await expect( + flashMintDex.issueExactSetFromETH(invalidIssueParams, 0, { value: notEnoughEth }), + ).to.be.revertedWith("STF"); + }); + + it("should revert when not enough ERC20 is sent for issuance", async () => { + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const usdc = IERC20__factory.connect(addresses.tokens.USDC, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdc.connect(whaleSigner).transfer(owner.address, usdcEstimate); + usdc.approve(flashMintDex.address, usdcEstimate); + + const paymentInfoNotEnoughUsdc: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcEstimate.div(2), + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + await expect( + flashMintDex.issueExactSetFromERC20(issueParams, paymentInfoNotEnoughUsdc, 0), + ).to.be.revertedWith("STF"); + + const wethToken = IWETH__factory.connect(addresses.tokens.weth, owner.wallet); + await wethToken.deposit({ value: ether(100) }); + await wethToken.transfer(flashMintDex.address, ether(100)); + await expect( + flashMintDex.issueExactSetFromERC20(issueParams, paymentInfoNotEnoughUsdc, 0), + ).to.be.revertedWith("FlashMint: OVERSPENT WETH"); + }); + + it("should revert when minimum ETH is not received during redemption", async () => { + const setToken = SetToken__factory.connect(redeemParams.setToken, owner.wallet); + setToken.approve(flashMintDex.address, redeemParams.amountSetToken); + await flashMintDex.issueExactSetFromETH(issueParams, 0, { + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), + }); + const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + const minAmountOutTooHigh = ethEstimate.mul(2); + await expect( + flashMintDex.redeemExactSetForETH(redeemParams, minAmountOutTooHigh), + ).to.be.revertedWith("FlashMint: INSUFFICIENT WETH RECEIVED"); + }); + + it("should revert when minimum ERC20 is not received during redemption", async () => { + const setToken = SetToken__factory.connect(redeemParams.setToken, owner.wallet); + setToken.approve(flashMintDex.address, redeemParams.amountSetToken); + const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + const paymentInfoNotEnoughUsdc: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcEstimate.mul(2), + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + await expect( + flashMintDex.redeemExactSetForERC20(redeemParams, paymentInfoNotEnoughUsdc), + ).to.be.revertedWith("FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + }); + + it("issueExactSetFromETH should revert when incompatible set token is provided", async () => { + invalidIssueParams.setToken = addresses.tokens.dpi; + await expect( + flashMintDex.issueExactSetFromETH(invalidIssueParams, 0, { value: ether(1) }), + ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); + }); + + it("issueExactSetFromERC20 should revert when incompatible issuance module is provided", async () => { + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const usdc = IERC20__factory.connect(addresses.tokens.USDC, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdc.connect(whaleSigner).transfer(owner.address, usdcEstimate); + usdc.approve(flashMintDex.address, usdcEstimate); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcEstimate, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + invalidIssueParams.issuanceModule = addresses.set.basicIssuanceModule; + await expect( + flashMintDex.issueExactSetFromERC20(invalidIssueParams, paymentInfo, 0) + ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); + }); + + it("redeemExactSetForETH should revert when incompatible set token is provided", async () => { + const invalidRedeemParams = { ...redeemParams }; + const minEthOut = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + invalidRedeemParams.setToken = addresses.tokens.dpi; + await expect( + flashMintDex.redeemExactSetForETH(invalidRedeemParams, minEthOut), + ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); + }); + + it("redeemExactSetForERC20 should revert when incompatible issuance module is provided", async () => { + const invalidRedeemParams = { ...redeemParams }; + const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: usdcEstimate, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + invalidRedeemParams.issuanceModule = addresses.set.basicIssuanceModule; + await expect( + flashMintDex.redeemExactSetForERC20(invalidRedeemParams, paymentInfo), + ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); + }); + }); + }); + }); + }); +} 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/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 8faad9a4..480e35f5 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -54,6 +54,7 @@ import { FlashMintLeveragedForCompound__factory } from "../../typechain/factorie import { FlashMintWrapped } from "../../typechain/FlashMintWrapped"; import { FlashMintWrapped__factory } from "../../typechain/factories/FlashMintWrapped__factory"; import { ExchangeIssuanceZeroEx__factory } from "../../typechain/factories/ExchangeIssuanceZeroEx__factory"; +import { FlashMintDex__factory } from "../../typechain/factories/FlashMintDex__factory"; import { FlashMintPerp__factory } from "../../typechain/factories/FlashMintPerp__factory"; import { FeeSplitExtension__factory } from "../../typechain/factories/FeeSplitExtension__factory"; import { PrtFeeSplitExtension__factory } from "../../typechain/factories/PrtFeeSplitExtension__factory"; @@ -483,6 +484,43 @@ export default class DeployExtensions { swapTarget, ); } + public async deployFlashMintDex( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + dexAdapterV2Address: Address, + setControllerAddress: Address, + indexControllerAddress: Address, + ) { + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", + ); + + return await new FlashMintDex__factory( + // @ts-ignore + { + [linkId]: dexAdapterV2Address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + setControllerAddress, + indexControllerAddress, + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }, + ); + } public async deployFlashMintNotional( wethAddress: Address,