From 263d307d72579c5d1daee789accab76af1c4c10b Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 24 Jul 2024 01:08:51 -0400 Subject: [PATCH 01/61] swap data instead of component quotes --- contracts/exchangeIssuance/FlashMintDex.sol | 523 ++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 contracts/exchangeIssuance/FlashMintDex.sol diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol new file mode 100644 index 00000000..2f678c11 --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -0,0 +1,523 @@ +/* + Copyright 2022 Index Cooperative + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + SPDX-License-Identifier: Apache License, Version 2.0 +*/ +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Math } from "@openzeppelin/contracts/math/Math.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { 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 + */ +contract FlashMintDex is Ownable, ReentrancyGuard { + + 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; + // address public immutable swapTarget; + + /* ============ 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), "ExchangeIssuance: INVALID ISSUANCE MODULE"); + _; + } + + constructor( + address _weth, + IController _setController + // address _swapTarget + ) + public + { + setController = _setController; + + WETH = _weth; + // swapTarget = _swapTarget; + } + + /* ============ 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, "ExchangeIssuance: 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); + } + } + + /** + * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. + * The excess amount of tokens is returned in an equivalent amount of ether. + * + * @param _setToken Address of the SetToken to be issued + * @param _inputToken Address of the input token + * @param _amountSetToken Amount of SetTokens to issue + * @param _maxAmountInputToken Maximum amount of input tokens to be used to issue SetTokens. + * @param _swapData Swap data from input token to each component token + * + * @return totalInputTokenSold Amount of input token spent for issuance + */ + function issueExactSetFromToken( + ISetToken _setToken, + IERC20 _inputToken, + uint256 _amountSetToken, + uint256 _maxAmountInputToken, + DexAdapter.SwapData[] memory _swapData, + address _issuanceModule, + bool _isDebtIssuance + ) + isValidModule(_issuanceModule) + external + nonReentrant + returns (uint256) + { + + _inputToken.safeTransferFrom(msg.sender, address(this), _maxAmountInputToken); + // _safeApprove(_inputToken, swapTarget, _maxAmountInputToken); + + uint256 totalInputTokenSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _swapData, _inputToken, _issuanceModule, _isDebtIssuance); + require(totalInputTokenSold <= _maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); + + IBasicIssuanceModule(_issuanceModule).issue(_setToken, _amountSetToken, msg.sender); + + _returnExcessInputToken(_inputToken, _maxAmountInputToken, totalInputTokenSold); + + emit FlashMint(msg.sender, _setToken, _inputToken, _maxAmountInputToken, _amountSetToken); + return totalInputTokenSold; + } + + + /** + * Issues an exact amount of SetTokens for given amount of ETH. + * The excess amount of tokens is returned in an equivalent amount of ether. + * + * @param _setToken Address of the SetToken to be issued + * @param _amountSetToken Amount of SetTokens to issue + * @param _componentQuotes The encoded 0x transactions to execute + * + * @return amountEthReturn Amount of ether returned to the caller + */ + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _amountSetToken, + bytes[] memory _componentQuotes, + address _issuanceModule, + bool _isDebtIssuance + ) + isValidModule(_issuanceModule) + external + nonReentrant + payable + returns (uint256) + { + require(msg.value > 0, "ExchangeIssuance: NO ETH SENT"); + + IWETH(WETH).deposit{value: msg.value}(); + _safeApprove(IERC20(WETH), swapTarget, msg.value); + + uint256 totalEthSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _componentQuotes, IERC20(WETH), _issuanceModule, _isDebtIssuance); + + require(totalEthSold <= msg.value, "ExchangeIssuance: OVERSPENT ETH"); + IBasicIssuanceModule(_issuanceModule).issue(_setToken, _amountSetToken, msg.sender); + + uint256 amountEthReturn = msg.value.sub(totalEthSold); + if (amountEthReturn > 0) { + IWETH(WETH).withdraw(amountEthReturn); + payable(msg.sender).sendValue(amountEthReturn); + } + + emit FlashMint(msg.sender, _setToken, IERC20(ETH_ADDRESS), totalEthSold, _amountSetToken); + return amountEthReturn; + } + + /** + * Redeems an exact amount of SetTokens for an ERC20 token. + * The SetToken must be approved by the sender to this contract. + * + * @param _setToken Address of the SetToken being redeemed + * @param _outputToken Address of output token + * @param _amountSetToken Amount SetTokens to redeem + * @param _minOutputReceive Minimum amount of output token to receive + * @param _componentQuotes The encoded 0x transactions execute (components -> WETH). + * @param _issuanceModule Address of issuance Module to use + * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * + * @return outputAmount Amount of output tokens sent to the caller + */ + function redeemExactSetForToken( + ISetToken _setToken, + IERC20 _outputToken, + uint256 _amountSetToken, + uint256 _minOutputReceive, + bytes[] memory _componentQuotes, + address _issuanceModule, + bool _isDebtIssuance + ) + isValidModule(_issuanceModule) + external + nonReentrant + returns (uint256) + { + + uint256 outputAmount; + _redeemExactSet(_setToken, _amountSetToken, _issuanceModule); + + outputAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _componentQuotes, _outputToken, _issuanceModule, _isDebtIssuance); + require(outputAmount >= _minOutputReceive, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); + + // Transfer sender output token + _outputToken.safeTransfer(msg.sender, outputAmount); + // Emit event + emit FlashRedeem(msg.sender, _setToken, _outputToken, _amountSetToken, outputAmount); + // Return output amount + return outputAmount; + } + + /** + * Redeems an exact amount of SetTokens for ETH. + * The SetToken must be approved by the sender to this contract. + * + * @param _setToken Address of the SetToken being redeemed + * @param _amountSetToken Amount SetTokens to redeem + * @param _minEthReceive Minimum amount of Eth to receive + * @param _componentQuotes The encoded 0x transactions execute + * @param _issuanceModule Address of issuance Module to use + * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * + * @return outputAmount Amount of output tokens sent to the caller + */ + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _amountSetToken, + uint256 _minEthReceive, + bytes[] memory _componentQuotes, + address _issuanceModule, + bool _isDebtIssuance + ) + isValidModule(_issuanceModule) + external + nonReentrant + returns (uint256) + { + _redeemExactSet(_setToken, _amountSetToken, _issuanceModule); + uint ethAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _componentQuotes, IERC20(WETH), _issuanceModule, _isDebtIssuance); + require(ethAmount >= _minEthReceive, "ExchangeIssuance: INSUFFICIENT WETH RECEIVED"); + + IWETH(WETH).withdraw(ethAmount); + (payable(msg.sender)).sendValue(ethAmount); + + emit FlashRedeem(msg.sender, _setToken, IERC20(ETH_ADDRESS), _amountSetToken, ethAmount); + return ethAmount; + + } + + + /** + * 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); + } + } + + /** + * Issues an exact amount of SetTokens using WETH. + * Acquires SetToken components by executing the 0x swaps whose callata is passed in _quotes. + * Uses the acquired components to issue the SetTokens. + * + * @param _setToken Address of the SetToken being issued + * @param _amountSetToken Amount of SetTokens to be issued + * @param _swapData Swap data from input token to each component token + * @param _inputToken Token to use to pay for issuance. Must be the sellToken of the 0x trades. + * @param _issuanceModule Issuance module to use for set token issuance. + * + * @return totalInputTokenSold Total amount of input token spent on this issuance + */ + function _buyComponentsForInputToken( + ISetToken _setToken, + uint256 _amountSetToken, + DexAdapter.SwapData[] memory _swapData, + IERC20 _inputToken, + address _issuanceModule, + bool _isDebtIssuance + ) + internal + returns (uint256 totalInputTokenSold) + { + uint256 componentAmountBought; + + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); + + uint256 inputTokenBalanceBefore = _inputToken.balanceOf(address(this)); + for (uint256 i = 0; i < components.length; i++) { + address component = components[i]; + uint256 units = componentUnits[i]; + + // If the component is equal to the input token we don't have to trade + if(component == address(_inputToken)) { + totalInputTokenSold = totalInputTokenSold.add(units); + componentAmountBought = units; + } + else { + uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); + DexAdapter.swapTokensForExactTokens(_swapData[i]); + uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); + componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); + require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); + } + } + uint256 inputTokenBalanceAfter = _inputToken.balanceOf(address(this)); + totalInputTokenSold = totalInputTokenSold.add(inputTokenBalanceBefore.sub(inputTokenBalanceAfter)); + } + + /** + * Redeems a given list of SetToken components for given token. + * + * @param _setToken The set token being swapped. + * @param _amountSetToken The amount of set token being swapped. + * @param _swaps An array containing ZeroExSwap swaps. + * @param _outputToken The token for which to sell the index components must be the same as the buyToken that was specified when generating the swaps + * @param _issuanceModule Address of issuance Module to use + * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * + * @return totalOutputTokenBought Total amount of output token received after liquidating all SetToken components + */ + function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, bytes[] memory _swaps, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) + internal + returns (uint256 totalOutputTokenBought) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); + uint256 outputTokenBalanceBefore = _outputToken.balanceOf(address(this)); + for (uint256 i = 0; i < _swaps.length; i++) { + uint256 maxAmountSell = componentUnits[i]; + + uint256 componentAmountSold; + + // If the component is equal to the output token we don't have to trade + if(components[i] == address(_outputToken)) { + totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); + componentAmountSold = maxAmountSell; + } + else { + _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); + uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); + _fillQuote(_swaps[i]); + uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); + componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); + require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); + } + + } + uint256 outputTokenBalanceAfter = _outputToken.balanceOf(address(this)); + totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); + } + + /** + * Execute a 0x Swap quote + * + * @param _quote Swap quote as returned by 0x API + * + */ + function _fillQuote( + bytes memory _quote + ) + internal + + { + + (bool success, bytes memory returndata) = swapTarget.call(_quote); + + // Forwarding errors including new custom errors + // Taken from: https://ethereum.stackexchange.com/a/111187/73805 + if (!success) { + if (returndata.length == 0) revert(); + assembly { + revert(add(32, returndata), mload(returndata)) + } + } + + } + + /** + * 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 _redeemExactSet(ISetToken _setToken, uint256 _amount, address _issuanceModule) internal returns (uint256) { + _setToken.safeTransferFrom(msg.sender, address(this), _amount); + IBasicIssuanceModule(_issuanceModule).redeem(_setToken, _amount, address(this)); + } + + /** + * Returns excess input token + * + * @param _inputToken Address of the input token to return + * @param _receivedAmount Amount received by the caller + * @param _spentAmount Amount spent for issuance + */ + function _returnExcessInputToken(IERC20 _inputToken, uint256 _receivedAmount, uint256 _spentAmount) internal { + uint256 amountTokenReturn = _receivedAmount.sub(_spentAmount); + if (amountTokenReturn > 0) { + _inputToken.safeTransfer(msg.sender, amountTokenReturn); + } + } + + /** + * 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); + } + } + } +} From 6aaaa31dec35ea0d5a7a5eda8057d3ef5f07a37e Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 24 Jul 2024 11:16:35 -0400 Subject: [PATCH 02/61] stack still too deep --- contracts/exchangeIssuance/FlashMintDex.sol | 164 +++++++++----------- 1 file changed, 72 insertions(+), 92 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 2f678c11..3a1dfb69 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -34,7 +34,7 @@ import { DEXAdapterV2 } from "./DEXAdapterV2.sol"; * @title FlashMintDex */ contract FlashMintDex is Ownable, ReentrancyGuard { - + using DEXAdapterV2 for DEXAdapterV2.Addresses; using Address for address payable; using SafeMath for uint256; using PreciseUnitMath for uint256; @@ -50,8 +50,30 @@ contract FlashMintDex is Ownable, ReentrancyGuard { address public immutable WETH; IController public immutable setController; + DEXAdapterV2.Addresses public dexAdapter; // address public immutable swapTarget; + /* ============ Structs ============ */ + struct IssueParams { + ISetToken setToken; // The address of the SetToken to be issued + IERC20 inputToken; // The address of the input token + uint256 amountSetToken; // The amount of SetTokens to issue + uint256 maxAmountInputToken; // The maximum amount of input tokens to be used to issue SetTokens + DEXAdapterV2.SwapData[] swapData; // The swap data from input token 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 RedeemParams { + ISetToken setToken; // The address of the SetToken to be redeemed + IERC20 outputToken; // The address of the output token + uint256 amountSetToken; // The amount of SetTokens to redeem + uint256 minOutputReceive; // The minimum amount of output tokens to receive + DEXAdapterV2.SwapData[] swapData; // The swap data from each component token to the output 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 + } + /* ============ Events ============ */ event FlashMint( @@ -79,13 +101,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { constructor( address _weth, - IController _setController + IController _setController, + DEXAdapterV2.Addresses memory _dexAddresses // address _swapTarget ) public { setController = _setController; - + dexAdapter = _dexAddresses; WETH = _weth; // swapTarget = _swapTarget; } @@ -160,41 +183,35 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. * The excess amount of tokens is returned in an equivalent amount of ether. * - * @param _setToken Address of the SetToken to be issued - * @param _inputToken Address of the input token - * @param _amountSetToken Amount of SetTokens to issue - * @param _maxAmountInputToken Maximum amount of input tokens to be used to issue SetTokens. - * @param _swapData Swap data from input token to each component token + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * * @return totalInputTokenSold Amount of input token spent for issuance */ - function issueExactSetFromToken( - ISetToken _setToken, - IERC20 _inputToken, - uint256 _amountSetToken, - uint256 _maxAmountInputToken, - DexAdapter.SwapData[] memory _swapData, - address _issuanceModule, - bool _isDebtIssuance - ) - isValidModule(_issuanceModule) + function issueExactSetFromToken(IssueParams memory _issueParams) + isValidModule(_issueParams.issuanceModule) external nonReentrant returns (uint256) { - _inputToken.safeTransferFrom(msg.sender, address(this), _maxAmountInputToken); - // _safeApprove(_inputToken, swapTarget, _maxAmountInputToken); + _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); - uint256 totalInputTokenSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _swapData, _inputToken, _issuanceModule, _isDebtIssuance); - require(totalInputTokenSold <= _maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); + uint256 totalInputTokenSold = _buyComponentsForInputToken( + _issueParams.setToken, + _issueParams.amountSetToken, + _issueParams.swapData, + _issueParams.inputToken, + _issueParams.issuanceModule, + _issueParams.isDebtIssuance + ); + require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); - IBasicIssuanceModule(_issuanceModule).issue(_setToken, _amountSetToken, msg.sender); + IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); - _returnExcessInputToken(_inputToken, _maxAmountInputToken, totalInputTokenSold); + _returnExcessInputToken(_issueParams.inputToken, _issueParams.maxAmountInputToken, totalInputTokenSold); - emit FlashMint(msg.sender, _setToken, _inputToken, _maxAmountInputToken, _amountSetToken); - return totalInputTokenSold; + emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); + return totalInputTokenSold; } @@ -204,14 +221,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _setToken Address of the SetToken to be issued * @param _amountSetToken Amount of SetTokens to issue - * @param _componentQuotes The encoded 0x transactions to execute + * @param _swapData Swap data from ETH to each component token * * @return amountEthReturn Amount of ether returned to the caller */ function issueExactSetFromETH( ISetToken _setToken, uint256 _amountSetToken, - bytes[] memory _componentQuotes, + DEXAdapterV2.SwapData[] memory _swapData, address _issuanceModule, bool _isDebtIssuance ) @@ -224,9 +241,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { require(msg.value > 0, "ExchangeIssuance: NO ETH SENT"); IWETH(WETH).deposit{value: msg.value}(); - _safeApprove(IERC20(WETH), swapTarget, msg.value); - uint256 totalEthSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _componentQuotes, IERC20(WETH), _issuanceModule, _isDebtIssuance); + uint256 totalEthSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _swapData, IERC20(WETH), _issuanceModule, _isDebtIssuance); require(totalEthSold <= msg.value, "ExchangeIssuance: OVERSPENT ETH"); IBasicIssuanceModule(_issuanceModule).issue(_setToken, _amountSetToken, msg.sender); @@ -245,42 +261,32 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * Redeems an exact amount of SetTokens for an ERC20 token. * The SetToken must be approved by the sender to this contract. * - * @param _setToken Address of the SetToken being redeemed - * @param _outputToken Address of output token - * @param _amountSetToken Amount SetTokens to redeem - * @param _minOutputReceive Minimum amount of output token to receive - * @param _componentQuotes The encoded 0x transactions execute (components -> WETH). - * @param _issuanceModule Address of issuance Module to use - * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance * * @return outputAmount Amount of output tokens sent to the caller */ - function redeemExactSetForToken( - ISetToken _setToken, - IERC20 _outputToken, - uint256 _amountSetToken, - uint256 _minOutputReceive, - bytes[] memory _componentQuotes, - address _issuanceModule, - bool _isDebtIssuance - ) - isValidModule(_issuanceModule) + function redeemExactSetForToken(RedeemParams memory _redeemParams) + isValidModule(_redeemParams.issuanceModule) external nonReentrant returns (uint256) { - uint256 outputAmount; - _redeemExactSet(_setToken, _amountSetToken, _issuanceModule); + _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + + outputAmount = _sellComponentsForOutputToken( + _redeemParams.setToken, + _redeemParams.amountSetToken, + _redeemParams.swapData, + _redeemParams.outputToken, + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance + ); + require(outputAmount >= _redeemParams.minOutputReceive, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); - outputAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _componentQuotes, _outputToken, _issuanceModule, _isDebtIssuance); - require(outputAmount >= _minOutputReceive, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); + _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); - // Transfer sender output token - _outputToken.safeTransfer(msg.sender, outputAmount); - // Emit event - emit FlashRedeem(msg.sender, _setToken, _outputToken, _amountSetToken, outputAmount); - // Return output amount + emit FlashRedeem(msg.sender, _redeemParams.setToken, _redeemParams.outputToken, _redeemParams.amountSetToken, outputAmount); return outputAmount; } @@ -291,7 +297,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @param _setToken Address of the SetToken being redeemed * @param _amountSetToken Amount SetTokens to redeem * @param _minEthReceive Minimum amount of Eth to receive - * @param _componentQuotes The encoded 0x transactions execute + * @param _swapData Swap data from input token to each component token * @param _issuanceModule Address of issuance Module to use * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module * @@ -301,7 +307,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ISetToken _setToken, uint256 _amountSetToken, uint256 _minEthReceive, - bytes[] memory _componentQuotes, + DEXAdapterV2.SwapData[] memory _swapData, address _issuanceModule, bool _isDebtIssuance ) @@ -311,7 +317,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { returns (uint256) { _redeemExactSet(_setToken, _amountSetToken, _issuanceModule); - uint ethAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _componentQuotes, IERC20(WETH), _issuanceModule, _isDebtIssuance); + uint ethAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _swapData, IERC20(WETH), _issuanceModule, _isDebtIssuance); require(ethAmount >= _minEthReceive, "ExchangeIssuance: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); @@ -353,7 +359,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { function _buyComponentsForInputToken( ISetToken _setToken, uint256 _amountSetToken, - DexAdapter.SwapData[] memory _swapData, + DEXAdapterV2.SwapData[] memory _swapData, IERC20 _inputToken, address _issuanceModule, bool _isDebtIssuance @@ -377,7 +383,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - DexAdapter.swapTokensForExactTokens(_swapData[i]); + dexAdapter.swapTokensForExactTokens(componentAmountBought, totalInputTokenSold, _swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); @@ -392,20 +398,20 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _setToken The set token being swapped. * @param _amountSetToken The amount of set token being swapped. - * @param _swaps An array containing ZeroExSwap swaps. + * @param _swapData Swap data from input token to each component token * @param _outputToken The token for which to sell the index components must be the same as the buyToken that was specified when generating the swaps * @param _issuanceModule Address of issuance Module to use * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module * * @return totalOutputTokenBought Total amount of output token received after liquidating all SetToken components */ - function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, bytes[] memory _swaps, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) + function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, DEXAdapterV2.SwapData[] memory _swapData, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) internal returns (uint256 totalOutputTokenBought) { (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); uint256 outputTokenBalanceBefore = _outputToken.balanceOf(address(this)); - for (uint256 i = 0; i < _swaps.length; i++) { + for (uint256 i = 0; i < components.length; i++) { uint256 maxAmountSell = componentUnits[i]; uint256 componentAmountSold; @@ -416,9 +422,9 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountSold = maxAmountSell; } else { - _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); + // _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); - _fillQuote(_swaps[i]); + dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _swapData[i]); uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); @@ -429,32 +435,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); } - /** - * Execute a 0x Swap quote - * - * @param _quote Swap quote as returned by 0x API - * - */ - function _fillQuote( - bytes memory _quote - ) - internal - - { - - (bool success, bytes memory returndata) = swapTarget.call(_quote); - - // Forwarding errors including new custom errors - // Taken from: https://ethereum.stackexchange.com/a/111187/73805 - if (!success) { - if (returndata.length == 0) revert(); - assembly { - revert(add(32, returndata), mload(returndata)) - } - } - - } - /** * Transfers given amount of set token from the sender and redeems it for underlying components. * Obtained component tokens are sent to this contract. From 34583aff76f730c6b67a552cd92b858fd5db5d70 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 24 Jul 2024 11:32:49 -0400 Subject: [PATCH 03/61] refactor issuance functions --- contracts/exchangeIssuance/FlashMintDex.sol | 87 ++++++++------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 3a1dfb69..24b8e3ce 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -62,6 +62,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { DEXAdapterV2.SwapData[] swapData; // The swap data from input token 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 + bool isETH; // A flag indicating whether the input token is ETH } struct RedeemParams { @@ -194,24 +195,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { returns (uint256) { - _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); + _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); - uint256 totalInputTokenSold = _buyComponentsForInputToken( - _issueParams.setToken, - _issueParams.amountSetToken, - _issueParams.swapData, - _issueParams.inputToken, - _issueParams.issuanceModule, - _issueParams.isDebtIssuance - ); - require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); + uint256 totalInputTokenSold = _buyComponentsForInputToken(_issueParams); + require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); - IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); + IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); - _returnExcessInputToken(_issueParams.inputToken, _issueParams.maxAmountInputToken, totalInputTokenSold); + _returnExcessInputToken(_issueParams.inputToken, _issueParams.maxAmountInputToken, totalInputTokenSold); - emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); - return totalInputTokenSold; + emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); + return totalInputTokenSold; } @@ -219,33 +213,27 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * Issues an exact amount of SetTokens for given amount of ETH. * The excess amount of tokens is returned in an equivalent amount of ether. * - * @param _setToken Address of the SetToken to be issued - * @param _amountSetToken Amount of SetTokens to issue - * @param _swapData Swap data from ETH to each component token + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * * @return amountEthReturn Amount of ether returned to the caller */ - function issueExactSetFromETH( - ISetToken _setToken, - uint256 _amountSetToken, - DEXAdapterV2.SwapData[] memory _swapData, - address _issuanceModule, - bool _isDebtIssuance - ) - isValidModule(_issuanceModule) + function issueExactSetFromETH(IssueParams memory _issueParams) + isValidModule(_issueParams.issuanceModule) external nonReentrant payable returns (uint256) { + require(_issueParams.isETH, "ExchangeIssuance: isETH must be true for this function"); require(msg.value > 0, "ExchangeIssuance: NO ETH SENT"); IWETH(WETH).deposit{value: msg.value}(); + // _safeApprove(IERC20(WETH), swapTarget, msg.value); - uint256 totalEthSold = _buyComponentsForInputToken(_setToken, _amountSetToken, _swapData, IERC20(WETH), _issuanceModule, _isDebtIssuance); + uint256 totalEthSold = _buyComponentsForInputToken(_issueParams); require(totalEthSold <= msg.value, "ExchangeIssuance: OVERSPENT ETH"); - IBasicIssuanceModule(_issuanceModule).issue(_setToken, _amountSetToken, msg.sender); + IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); uint256 amountEthReturn = msg.value.sub(totalEthSold); if (amountEthReturn > 0) { @@ -253,7 +241,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { payable(msg.sender).sendValue(amountEthReturn); } - emit FlashMint(msg.sender, _setToken, IERC20(ETH_ADDRESS), totalEthSold, _amountSetToken); + emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), totalEthSold, _issueParams.amountSetToken); return amountEthReturn; } @@ -344,52 +332,45 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Issues an exact amount of SetTokens using WETH. - * Acquires SetToken components by executing the 0x swaps whose callata is passed in _quotes. - * Uses the acquired components to issue the SetTokens. + * Acquires SetToken components by executing swaps whose callata is passed in _swapData. + * Acquired components are then used to issue the SetTokens. * - * @param _setToken Address of the SetToken being issued - * @param _amountSetToken Amount of SetTokens to be issued - * @param _swapData Swap data from input token to each component token - * @param _inputToken Token to use to pay for issuance. Must be the sellToken of the 0x trades. - * @param _issuanceModule Issuance module to use for set token issuance. + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * * @return totalInputTokenSold Total amount of input token spent on this issuance */ - function _buyComponentsForInputToken( - ISetToken _setToken, - uint256 _amountSetToken, - DEXAdapterV2.SwapData[] memory _swapData, - IERC20 _inputToken, - address _issuanceModule, - bool _isDebtIssuance - ) - internal - returns (uint256 totalInputTokenSold) + function _buyComponentsForInputToken(IssueParams memory _issueParams) + internal + returns (uint256 totalInputTokenSold) { uint256 componentAmountBought; - (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); - uint256 inputTokenBalanceBefore = _inputToken.balanceOf(address(this)); + uint256 inputTokenBalanceBefore = _issueParams.inputToken.balanceOf(address(this)); for (uint256 i = 0; i < components.length; i++) { address component = components[i]; uint256 units = componentUnits[i]; // If the component is equal to the input token we don't have to trade - if(component == address(_inputToken)) { + if (component == address(_issueParams.inputToken)) { totalInputTokenSold = totalInputTokenSold.add(units); componentAmountBought = units; - } - else { + } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - dexAdapter.swapTokensForExactTokens(componentAmountBought, totalInputTokenSold, _swapData[i]); + // DexAdapterV2.swapTokensForExactTokens(_issueParams.swapData[i]); + dexAdapter.swapTokensForExactTokens(componentAmountBought, totalInputTokenSold, _issueParams.swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); } } - uint256 inputTokenBalanceAfter = _inputToken.balanceOf(address(this)); + uint256 inputTokenBalanceAfter = _issueParams.inputToken.balanceOf(address(this)); totalInputTokenSold = totalInputTokenSold.add(inputTokenBalanceBefore.sub(inputTokenBalanceAfter)); } From 63295ff995e8902017e00bc841b7b4c54a4a4629 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 24 Jul 2024 12:31:01 -0400 Subject: [PATCH 04/61] refactor redeem functions --- contracts/exchangeIssuance/FlashMintDex.sol | 104 +++++++++++--------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 24b8e3ce..1e74aa98 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -262,14 +262,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 outputAmount; _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - outputAmount = _sellComponentsForOutputToken( - _redeemParams.setToken, - _redeemParams.amountSetToken, - _redeemParams.swapData, - _redeemParams.outputToken, - _redeemParams.issuanceModule, - _redeemParams.isDebtIssuance - ); + outputAmount = _sellComponentsForOutputToken(_redeemParams); require(outputAmount >= _redeemParams.minOutputReceive, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); @@ -282,39 +275,29 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * Redeems an exact amount of SetTokens for ETH. * The SetToken must be approved by the sender to this contract. * - * @param _setToken Address of the SetToken being redeemed - * @param _amountSetToken Amount SetTokens to redeem - * @param _minEthReceive Minimum amount of Eth to receive - * @param _swapData Swap data from input token to each component token - * @param _issuanceModule Address of issuance Module to use - * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance * - * @return outputAmount Amount of output tokens sent to the caller + * @return ethAmount The amount of ETH received. */ - function redeemExactSetForETH( - ISetToken _setToken, - uint256 _amountSetToken, - uint256 _minEthReceive, - DEXAdapterV2.SwapData[] memory _swapData, - address _issuanceModule, - bool _isDebtIssuance - ) - isValidModule(_issuanceModule) + function redeemExactSetForETH(RedeemParams memory _redeemParams) + isValidModule(_redeemParams.issuanceModule) external nonReentrant returns (uint256) { - _redeemExactSet(_setToken, _amountSetToken, _issuanceModule); - uint ethAmount = _sellComponentsForOutputToken(_setToken, _amountSetToken, _swapData, IERC20(WETH), _issuanceModule, _isDebtIssuance); - require(ethAmount >= _minEthReceive, "ExchangeIssuance: INSUFFICIENT WETH RECEIVED"); + require(_redeemParams.outputToken == IERC20(WETH), "ExchangeIssuance: OUTPUT TOKEN MUST BE WETH"); + _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + + uint ethAmount = _sellComponentsForOutputToken(_redeemParams); + require(ethAmount >= _redeemParams.minOutputReceive, "ExchangeIssuance: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); - (payable(msg.sender)).sendValue(ethAmount); + payable(msg.sender).sendValue(ethAmount); - emit FlashRedeem(msg.sender, _setToken, IERC20(ETH_ADDRESS), _amountSetToken, ethAmount); + emit FlashRedeem(msg.sender, _redeemParams.setToken, IERC20(ETH_ADDRESS), _redeemParams.amountSetToken, ethAmount); return ethAmount; - } + /** @@ -377,44 +360,71 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Redeems a given list of SetToken components for given token. * - * @param _setToken The set token being swapped. - * @param _amountSetToken The amount of set token being swapped. - * @param _swapData Swap data from input token to each component token - * @param _outputToken The token for which to sell the index components must be the same as the buyToken that was specified when generating the swaps - * @param _issuanceModule Address of issuance Module to use - * @param _isDebtIssuance Flag indicating wether given issuance module is a debt issuance module + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance * * @return totalOutputTokenBought Total amount of output token received after liquidating all SetToken components */ - function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, DEXAdapterV2.SwapData[] memory _swapData, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) + function _sellComponentsForOutputToken(RedeemParams memory _redeemParams) internal returns (uint256 totalOutputTokenBought) { - (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); - uint256 outputTokenBalanceBefore = _outputToken.balanceOf(address(this)); - for (uint256 i = 0; i < components.length; i++) { - uint256 maxAmountSell = componentUnits[i]; + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + uint256 outputTokenBalanceBefore = _redeemParams.outputToken.balanceOf(address(this)); + for (uint256 i = 0; i < _redeemParams.swapData.length; i++) { + uint256 maxAmountSell = componentUnits[i]; uint256 componentAmountSold; // If the component is equal to the output token we don't have to trade - if(components[i] == address(_outputToken)) { + if (components[i] == address(_redeemParams.outputToken)) { totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); componentAmountSold = maxAmountSell; - } - else { + } else { // _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); - dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _swapData[i]); + dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.swapData[i]); uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); } - } - uint256 outputTokenBalanceAfter = _outputToken.balanceOf(address(this)); + uint256 outputTokenBalanceAfter = _redeemParams.outputToken.balanceOf(address(this)); totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); } + // function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, DEXAdapterV2.SwapData[] memory _swapData, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) + // internal + // returns (uint256 totalOutputTokenBought) + // { + // (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); + // uint256 outputTokenBalanceBefore = _outputToken.balanceOf(address(this)); + // for (uint256 i = 0; i < components.length; i++) { + // uint256 maxAmountSell = componentUnits[i]; + + // uint256 componentAmountSold; + + // // If the component is equal to the output token we don't have to trade + // if(components[i] == address(_outputToken)) { + // totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); + // componentAmountSold = maxAmountSell; + // } + // else { + // // _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); + // uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); + // dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _swapData[i]); + // uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); + // componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); + // require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); + // } + + // } + // uint256 outputTokenBalanceAfter = _outputToken.balanceOf(address(this)); + // totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); + // } /** * Transfers given amount of set token from the sender and redeems it for underlying components. From 8c8f6b1696fd7f92bed705c8b9214a616a8a05ce Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 24 Jul 2024 12:45:09 -0400 Subject: [PATCH 05/61] clean up issue and redeem with eth --- contracts/exchangeIssuance/FlashMintDex.sol | 54 ++++----------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 1e74aa98..68cc9cfd 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -51,7 +51,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { address public immutable WETH; IController public immutable setController; DEXAdapterV2.Addresses public dexAdapter; - // address public immutable swapTarget; /* ============ Structs ============ */ struct IssueParams { @@ -62,7 +61,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { DEXAdapterV2.SwapData[] swapData; // The swap data from input token 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 - bool isETH; // A flag indicating whether the input token is ETH } struct RedeemParams { @@ -96,7 +94,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /* ============ Modifiers ============ */ modifier isValidModule(address _issuanceModule) { - require(setController.isModule(_issuanceModule), "ExchangeIssuance: INVALID ISSUANCE MODULE"); + require(setController.isModule(_issuanceModule), "FlashMint: INVALID ISSUANCE MODULE"); _; } @@ -104,14 +102,12 @@ contract FlashMintDex is Ownable, ReentrancyGuard { address _weth, IController _setController, DEXAdapterV2.Addresses memory _dexAddresses - // address _swapTarget ) public { setController = _setController; dexAdapter = _dexAddresses; WETH = _weth; - // swapTarget = _swapTarget; } /* ============ External Functions ============ */ @@ -135,7 +131,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { receive() external payable { // required for weth.withdraw() to work properly - require(msg.sender == WETH, "ExchangeIssuance: Direct deposits not allowed"); + require(msg.sender == WETH, "FlashMint: Direct deposits not allowed"); } /* ============ Public Functions ============ */ @@ -198,7 +194,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); uint256 totalInputTokenSold = _buyComponentsForInputToken(_issueParams); - require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "ExchangeIssuance: OVERSPENT TOKEN"); + require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "FlashMint: OVERSPENT TOKEN"); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); @@ -224,15 +220,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { payable returns (uint256) { - require(_issueParams.isETH, "ExchangeIssuance: isETH must be true for this function"); - require(msg.value > 0, "ExchangeIssuance: NO ETH SENT"); + require(_issueParams.inputToken == IERC20(WETH), "FlashMint: INPUT TOKEN MUST BE WETH"); + require(msg.value > 0, "FlashMint: NO ETH SENT"); IWETH(WETH).deposit{value: msg.value}(); - // _safeApprove(IERC20(WETH), swapTarget, msg.value); uint256 totalEthSold = _buyComponentsForInputToken(_issueParams); - require(totalEthSold <= msg.value, "ExchangeIssuance: OVERSPENT ETH"); + require(totalEthSold <= msg.value, "FlashMint: OVERSPENT ETH"); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); uint256 amountEthReturn = msg.value.sub(totalEthSold); @@ -263,7 +258,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); outputAmount = _sellComponentsForOutputToken(_redeemParams); - require(outputAmount >= _redeemParams.minOutputReceive, "ExchangeIssuance: INSUFFICIENT OUTPUT AMOUNT"); + require(outputAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); @@ -285,11 +280,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - require(_redeemParams.outputToken == IERC20(WETH), "ExchangeIssuance: OUTPUT TOKEN MUST BE WETH"); + require(_redeemParams.outputToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint ethAmount = _sellComponentsForOutputToken(_redeemParams); - require(ethAmount >= _redeemParams.minOutputReceive, "ExchangeIssuance: INSUFFICIENT WETH RECEIVED"); + require(ethAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); payable(msg.sender).sendValue(ethAmount); @@ -346,7 +341,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - // DexAdapterV2.swapTokensForExactTokens(_issueParams.swapData[i]); dexAdapter.swapTokensForExactTokens(componentAmountBought, totalInputTokenSold, _issueParams.swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); @@ -385,7 +379,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); componentAmountSold = maxAmountSell; } else { - // _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.swapData[i]); uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); @@ -396,35 +389,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 outputTokenBalanceAfter = _redeemParams.outputToken.balanceOf(address(this)); totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); } - // function _sellComponentsForOutputToken(ISetToken _setToken, uint256 _amountSetToken, DEXAdapterV2.SwapData[] memory _swapData, IERC20 _outputToken, address _issuanceModule, bool _isDebtIssuance) - // internal - // returns (uint256 totalOutputTokenBought) - // { - // (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents(_issuanceModule, _isDebtIssuance, _setToken, _amountSetToken); - // uint256 outputTokenBalanceBefore = _outputToken.balanceOf(address(this)); - // for (uint256 i = 0; i < components.length; i++) { - // uint256 maxAmountSell = componentUnits[i]; - - // uint256 componentAmountSold; - - // // If the component is equal to the output token we don't have to trade - // if(components[i] == address(_outputToken)) { - // totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); - // componentAmountSold = maxAmountSell; - // } - // else { - // // _safeApprove(IERC20(components[i]), address(swapTarget), maxAmountSell); - // uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); - // dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _swapData[i]); - // uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); - // componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); - // require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); - // } - - // } - // uint256 outputTokenBalanceAfter = _outputToken.balanceOf(address(this)); - // totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); - // } /** * Transfers given amount of set token from the sender and redeems it for underlying components. From a1c7f3e6d48b4aed56bf3b3c08215c9f10e90b65 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 25 Jul 2024 15:56:26 -0400 Subject: [PATCH 06/61] WIP FlashMintDex integration tests --- contracts/exchangeIssuance/FlashMintDex.sol | 2 +- test/integration/ethereum/addresses.ts | 4 ++ utils/deploys/deployExtensions.ts | 41 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 68cc9cfd..9eecb252 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -341,7 +341,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - dexAdapter.swapTokensForExactTokens(componentAmountBought, totalInputTokenSold, _issueParams.swapData[i]); + dexAdapter.swapTokensForExactTokens(units, _issueParams.maxAmountInputToken, _issueParams.swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 42782117..c09b431e 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -38,6 +38,9 @@ export const PRODUCTION_ADDRESSES = { rswEth: "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0", acrossWethLP: "0x28F77208728B0A45cAb24c4868334581Fe86F95B", morphoRe7WETH: "0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0", + wstEth: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + sfrxEth: "0xac3E018457B222d93114458476f3E3416Abbe38F", + osEth: "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", @@ -82,6 +85,7 @@ export const PRODUCTION_ADDRESSES = { }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", + basicIssuanceModule: "0xd8EF3cACe8b4907117a45B0b125c68560532F94D", debtIssuanceModule: "0x39F024d621367C044BacE2bf0Fb15Fb3612eCB92", debtIssuanceModuleV2: "0x69a592D2129415a4A1d1b1E309C17051B7F28d57", aaveLeverageModule: "0x251Bd1D42Df1f153D86a5BA2305FaADE4D5f51DC", diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 8faad9a4..c5fd8f50 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,46 @@ export default class DeployExtensions { swapTarget, ); } + public async deployFlashMintDex( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + setControllerAddress: Address, + debtIssuanceModuleAddress: Address, + stETHAddress: Address, + curveStEthEthPoolAddress: Address, + ) { + const dexAdapter = await this.deployDEXAdapterV2(); + + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", + ); + + return await new FlashMintDex__factory( + // @ts-ignore + { + [linkId]: dexAdapter.address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + wethAddress, + setControllerAddress, + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }, + ); + } public async deployFlashMintNotional( wethAddress: Address, From c111735d568c5f0ec571253339f6c7c962ce7ff1 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 26 Jul 2024 10:44:31 -0400 Subject: [PATCH 07/61] add test file --- .../integration/ethereum/flashMintDex.spec.ts | 372 ++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 test/integration/ethereum/flashMintDex.spec.ts diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts new file mode 100644 index 00000000..baaca55d --- /dev/null +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -0,0 +1,372 @@ +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, + IWETH, + IWETH__factory, +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { ether, usdc } 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 IssueParams = { + setToken: Address; + inputToken: Address; + amountSetToken: BigNumber; + maxAmountInputToken: BigNumber; + swapData: SwapData[]; + issuanceModule: Address; + isDebtIssuance: boolean; +}; + +const NO_OP_SWAP_DATA: SwapData = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + exchange: Exchange.None, +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintDex - Integration Test", async () => { + const addresses = PRODUCTION_ADDRESSES; + let owner: Account; + let deployer: DeployHelper; + + let setTokenCreator: SetTokenCreator; + // let basicIssuanceModule: IBasicIssuanceModule; + let debtIssuanceModule: IDebtIssuanceModule; + + // const collateralTokenAddress = addresses.tokens.stEth; + setBlockNumber(20030042, true); + + before(async () => { + [owner] = await getAccounts(); + deployer = new DeployHelper(owner.wallet); + setTokenCreator = SetTokenCreator__factory.connect( + addresses.setFork.setTokenCreator, + owner.wallet, + ); + // basicIssuanceModule = 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.setFork.controller, + addresses.setFork.debtIssuanceModuleV2, + addresses.tokens.stEth, + addresses.dexes.curve.pools.stEthEth, + ); + }); + + 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("controller address is set correctly", async () => { + expect(await flashMintDex.setController()).to.eq( + utils.getAddress(addresses.setFork.controller), + ); + }); + + context("when setToken with dsETH composition is deployed", () => { + let setToken: SetToken; + const components = [ + addresses.tokens.wstEth, + addresses.tokens.rETH, + addresses.tokens.sfrxEth, + addresses.tokens.osEth, + addresses.tokens.ETHx, + addresses.tokens.swETH, + ]; + const positions = [ + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.16"), + ]; + + const componentSwapDataIssue = [ + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + NO_OP_SWAP_DATA, + { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.swETH], + pool: ADDRESS_ZERO, + }, + ]; + + // const componentSwapDataRedeem = [ + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // { + // exchange: Exchange.UniV3, + // fees: [500], + // path: [ addresses.tokens.swETH, addresses.tokens.weth], + // pool: ADDRESS_ZERO, + // }, + // ]; + + const modules = [addresses.setFork.debtIssuanceModuleV2]; + const tokenName = "Diversified Staked ETH Index"; + const tokenSymbol = "dsETH"; + + 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.approveTokens( + [ + addresses.tokens.wstEth, + addresses.tokens.rETH, + addresses.tokens.sfrxEth, + addresses.tokens.osEth, + addresses.tokens.ETHx, + addresses.tokens.swETH, + ], + debtIssuanceModule.address + ); + + await flashMintDex.approveSetToken(setToken.address, debtIssuanceModule.address); + }); + it("setToken is deployed correctly", async () => { + expect(await setToken.symbol()).to.eq(tokenSymbol); + }); + + ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { + describe(`When inputToken is ${inputTokenName}`, () => { + const ethIn = ether(1001); + const maxAmountIn = inputTokenName == "USDC" ? usdc(4000000) : ethIn; + const setTokenAmount = ether(1000); + let inputToken: IERC20 | IWETH; + // let swapDataInputTokenToEth: SwapData; + // let swapDataEthToInputToken: SwapData; + let issueParams: IssueParams; + + before(async () => { + if (inputTokenName != "eth") { + inputToken = IWETH__factory.connect(addresses.tokens[inputTokenName], owner.wallet); + inputToken.approve(flashMintDex.address, maxAmountIn); + } + if (inputTokenName === "weth") { + await inputToken.deposit({ value: maxAmountIn }); + // swapDataInputTokenToEth = { + // path: [addresses.tokens.weth, ETH_ADDRESS], + // fees: [], + // pool: ADDRESS_ZERO, + // exchange: 0, + // }; + // swapDataEthToInputToken = { + // path: [ETH_ADDRESS, addresses.tokens.weth], + // fees: [], + // pool: ADDRESS_ZERO, + // exchange: 0, + // }; + } + if (inputTokenName === "USDC") { + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); + swapDataInputTokenToEth = { + path: [addresses.tokens.USDC, addresses.tokens.weth], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + swapDataEthToInputToken = { + path: [addresses.tokens.weth, addresses.tokens.USDC], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + } + }); + function subject() { + issueParams = { + setToken: setToken.address, + inputToken: inputToken.address, + amountSetToken: setTokenAmount, + maxAmountInputToken: maxAmountIn, + swapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + + if (inputTokenName === "eth") { + // When issuing from ETH use WETH address for inputToken + issueParams.inputToken = addresses.tokens.weth; + return flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + } else { + return flashMintDex.issueExactSetFromToken(issueParams); + } + } + it("Can issue set token", async () => { + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputTokenBalanceAfter = + inputTokenName === "eth" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + }); + }); + }); + + // describe("When set token has been issued", () => { + // const minAmountOut = maxAmountIn.mul(8).div(10); + // beforeEach(async () => { + // await flashMintDex.issueExactSetFromETH( + // setToken.address, + // setTokenAmount, + // componentSwapDataIssue, + // { + // value: ethIn, + // }, + // ); + // await setToken.approve(flashMintDex.address, setTokenAmount); + // }); + + // function subject() { + // if (inputTokenName === "eth") { + // return flashMintDex.redeemExactSetForETH( + // setToken.address, + // setTokenAmount, + // minAmountOut, + // componentSwapDataRedeem, + // ); + // } else { + // return flashMintDex.redeemExactSetForERC20( + // setToken.address, + // setTokenAmount, + // inputToken.address, + // minAmountOut, + // swapDataEthToInputToken, + // componentSwapDataRedeem, + // ); + // } + // } + + // it("Can redeem set token", async () => { + // const inputTokenBalanceBefore = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + // await subject(); + // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + // const inputTokenBalanceAfter = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); + // }); + // }); + // }); + // }); + }); + }); + }); +} From aa2f5ad9d04a8d0d499526298b3834ad65bc468a Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 26 Jul 2024 11:26:17 -0400 Subject: [PATCH 08/61] refactor tests wip --- .../integration/ethereum/flashMintDex.spec.ts | 145 ++++++++---------- 1 file changed, 66 insertions(+), 79 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index baaca55d..d78fa1b8 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -15,8 +15,8 @@ import { SetTokenCreator__factory, FlashMintDex, IERC20, + IERC20__factory, IWETH, - IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; import { ADDRESS_ZERO } from "@utils/constants"; @@ -50,6 +50,16 @@ type IssueParams = { isDebtIssuance: boolean; }; +// type RedeemParams = { +// setToken: Address; +// outputToken: Address; +// amountSetToken: BigNumber; +// maxAmountInputToken: BigNumber; +// swapData: SwapData[]; +// issuanceModule: Address; +// isDebtIssuance: boolean; +// }; + const NO_OP_SWAP_DATA: SwapData = { path: [], fees: [], @@ -234,45 +244,24 @@ if (process.env.INTEGRATIONTEST) { // let swapDataInputTokenToEth: SwapData; // let swapDataEthToInputToken: SwapData; let issueParams: IssueParams; + // let redeemParams: RedeemParams; + before(async () => { if (inputTokenName != "eth") { - inputToken = IWETH__factory.connect(addresses.tokens[inputTokenName], owner.wallet); + inputToken = IERC20__factory.connect(addresses.tokens[inputTokenName], owner.wallet); inputToken.approve(flashMintDex.address, maxAmountIn); } if (inputTokenName === "weth") { await inputToken.deposit({ value: maxAmountIn }); - // swapDataInputTokenToEth = { - // path: [addresses.tokens.weth, ETH_ADDRESS], - // fees: [], - // pool: ADDRESS_ZERO, - // exchange: 0, - // }; - // swapDataEthToInputToken = { - // path: [ETH_ADDRESS, addresses.tokens.weth], - // fees: [], - // pool: ADDRESS_ZERO, - // exchange: 0, - // }; } if (inputTokenName === "USDC") { const whaleSigner = await impersonateAccount(addresses.whales.USDC); await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); - swapDataInputTokenToEth = { - path: [addresses.tokens.USDC, addresses.tokens.weth], - fees: [500], - pool: ADDRESS_ZERO, - exchange: Exchange.UniV3, - }; - swapDataEthToInputToken = { - path: [addresses.tokens.weth, addresses.tokens.USDC], - fees: [500], - pool: ADDRESS_ZERO, - exchange: Exchange.UniV3, - }; } }); function subject() { + console.log("debtIssuanceModule", debtIssuanceModule.address); issueParams = { setToken: setToken.address, inputToken: inputToken.address, @@ -312,60 +301,58 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); }); - }); - // describe("When set token has been issued", () => { - // const minAmountOut = maxAmountIn.mul(8).div(10); - // beforeEach(async () => { - // await flashMintDex.issueExactSetFromETH( - // setToken.address, - // setTokenAmount, - // componentSwapDataIssue, - // { - // value: ethIn, - // }, - // ); - // await setToken.approve(flashMintDex.address, setTokenAmount); - // }); - - // function subject() { - // if (inputTokenName === "eth") { - // return flashMintDex.redeemExactSetForETH( - // setToken.address, - // setTokenAmount, - // minAmountOut, - // componentSwapDataRedeem, - // ); - // } else { - // return flashMintDex.redeemExactSetForERC20( - // setToken.address, - // setTokenAmount, - // inputToken.address, - // minAmountOut, - // swapDataEthToInputToken, - // componentSwapDataRedeem, - // ); - // } - // } - - // it("Can redeem set token", async () => { - // const inputTokenBalanceBefore = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - // await subject(); - // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - // const inputTokenBalanceAfter = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); - // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); - // }); - // }); - // }); - // }); + + // describe("When set token has been issued", () => { + // const minAmountOut = maxAmountIn.mul(8).div(10); + // beforeEach(async () => { + // await flashMintDex.issueExactSetFromETH( + // issueParams, + // { + // value: maxAmountIn, + // }, + // ); + // await setToken.approve(flashMintDex.address, setTokenAmount); + // }); + + // function subject() { + // if (inputTokenName === "eth") { + // return flashMintDex.redeemExactSetForETH( + // setToken.address, + // setTokenAmount, + // minAmountOut, + // componentSwapDataRedeem, + // ); + // } else { + // return flashMintDex.redeemExactSetForERC20( + // setToken.address, + // setTokenAmount, + // inputToken.address, + // minAmountOut, + // swapDataEthToInputToken, + // componentSwapDataRedeem, + // ); + // } + // } + + // it("Can redeem set token", async () => { + // const inputTokenBalanceBefore = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + // await subject(); + // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + // const inputTokenBalanceAfter = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); + // }); + // }); + // }); + }); }); }); }); From 70a8915a764ac115233e12a483e4fd748b68134c Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 26 Jul 2024 14:08:15 -0400 Subject: [PATCH 09/61] flashmint dex issuance test for 1 component --- test/integration/ethereum/addresses.ts | 1 - .../integration/ethereum/flashMintDex.spec.ts | 265 ++++++++---------- 2 files changed, 120 insertions(+), 146 deletions(-) diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index c09b431e..553d78ba 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -85,7 +85,6 @@ export const PRODUCTION_ADDRESSES = { }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", - basicIssuanceModule: "0xd8EF3cACe8b4907117a45B0b125c68560532F94D", debtIssuanceModule: "0x39F024d621367C044BacE2bf0Fb15Fb3612eCB92", debtIssuanceModuleV2: "0x69a592D2129415a4A1d1b1E309C17051B7F28d57", aaveLeverageModule: "0x251Bd1D42Df1f153D86a5BA2305FaADE4D5f51DC", diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d78fa1b8..77ee1f44 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -14,14 +14,15 @@ import { SetTokenCreator, SetTokenCreator__factory, FlashMintDex, - IERC20, - IERC20__factory, - IWETH, + // IERC20, + // IERC20__factory, + // IWETH, + // IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; import { ADDRESS_ZERO } from "@utils/constants"; -import { ether, usdc } from "@utils/index"; -import { impersonateAccount } from "./utils"; +import { ether, /*usdc*/ } from "@utils/index"; +// import { impersonateAccount } from "./utils"; const expect = getWaffleExpect(); @@ -60,12 +61,12 @@ type IssueParams = { // isDebtIssuance: boolean; // }; -const NO_OP_SWAP_DATA: SwapData = { - path: [], - fees: [], - pool: ADDRESS_ZERO, - exchange: Exchange.None, -}; +// const NO_OP_SWAP_DATA: SwapData = { +// path: [], +// fees: [], +// pool: ADDRESS_ZERO, +// exchange: Exchange.None, +// }; if (process.env.INTEGRATIONTEST) { describe.only("FlashMintDex - Integration Test", async () => { @@ -77,8 +78,7 @@ if (process.env.INTEGRATIONTEST) { // let basicIssuanceModule: IBasicIssuanceModule; let debtIssuanceModule: IDebtIssuanceModule; - // const collateralTokenAddress = addresses.tokens.stEth; - setBlockNumber(20030042, true); + setBlockNumber(20385208, true); before(async () => { [owner] = await getAccounts(); @@ -146,28 +146,28 @@ if (process.env.INTEGRATIONTEST) { context("when setToken with dsETH composition is deployed", () => { let setToken: SetToken; const components = [ - addresses.tokens.wstEth, - addresses.tokens.rETH, - addresses.tokens.sfrxEth, - addresses.tokens.osEth, - addresses.tokens.ETHx, + // addresses.tokens.wstEth, + // addresses.tokens.rETH, + // addresses.tokens.sfrxEth, + // addresses.tokens.osEth, + // addresses.tokens.ETHx, addresses.tokens.swETH, ]; const positions = [ - ethers.utils.parseEther("0.16"), - ethers.utils.parseEther("0.16"), - ethers.utils.parseEther("0.16"), - ethers.utils.parseEther("0.16"), - ethers.utils.parseEther("0.16"), + // 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"), ethers.utils.parseEther("0.16"), ]; const componentSwapDataIssue = [ - NO_OP_SWAP_DATA, - NO_OP_SWAP_DATA, - NO_OP_SWAP_DATA, - NO_OP_SWAP_DATA, - NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, + // NO_OP_SWAP_DATA, { exchange: Exchange.UniV3, fees: [500], @@ -235,124 +235,99 @@ if (process.env.INTEGRATIONTEST) { expect(await setToken.symbol()).to.eq(tokenSymbol); }); - ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { - describe(`When inputToken is ${inputTokenName}`, () => { - const ethIn = ether(1001); - const maxAmountIn = inputTokenName == "USDC" ? usdc(4000000) : ethIn; - const setTokenAmount = ether(1000); - let inputToken: IERC20 | IWETH; - // let swapDataInputTokenToEth: SwapData; - // let swapDataEthToInputToken: SwapData; - let issueParams: IssueParams; - // let redeemParams: RedeemParams; - - - before(async () => { - if (inputTokenName != "eth") { - inputToken = IERC20__factory.connect(addresses.tokens[inputTokenName], owner.wallet); - inputToken.approve(flashMintDex.address, maxAmountIn); - } - if (inputTokenName === "weth") { - await inputToken.deposit({ value: maxAmountIn }); - } - if (inputTokenName === "USDC") { - const whaleSigner = await impersonateAccount(addresses.whales.USDC); - await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); - } - }); - function subject() { - console.log("debtIssuanceModule", debtIssuanceModule.address); - issueParams = { - setToken: setToken.address, - inputToken: inputToken.address, - amountSetToken: setTokenAmount, - maxAmountInputToken: maxAmountIn, - swapData: componentSwapDataIssue, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, - }; - - if (inputTokenName === "eth") { - // When issuing from ETH use WETH address for inputToken - issueParams.inputToken = addresses.tokens.weth; - return flashMintDex.issueExactSetFromETH( - issueParams, - { - value: maxAmountIn, - }, - ); - } else { - return flashMintDex.issueExactSetFromToken(issueParams); - } - } - it("Can issue set token", async () => { - const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - const inputTokenBalanceBefore = - inputTokenName === "eth" - ? await owner.wallet.getBalance() - : await inputToken.balanceOf(owner.address); - await subject(); - const inputTokenBalanceAfter = - inputTokenName === "eth" - ? await owner.wallet.getBalance() - : await inputToken.balanceOf(owner.address); - const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); - }); - }); - - - // describe("When set token has been issued", () => { - // const minAmountOut = maxAmountIn.mul(8).div(10); - // beforeEach(async () => { - // await flashMintDex.issueExactSetFromETH( - // issueParams, - // { - // value: maxAmountIn, - // }, - // ); - // await setToken.approve(flashMintDex.address, setTokenAmount); - // }); - - // function subject() { - // if (inputTokenName === "eth") { - // return flashMintDex.redeemExactSetForETH( - // setToken.address, - // setTokenAmount, - // minAmountOut, - // componentSwapDataRedeem, - // ); - // } else { - // return flashMintDex.redeemExactSetForERC20( - // setToken.address, - // setTokenAmount, - // inputToken.address, - // minAmountOut, - // swapDataEthToInputToken, - // componentSwapDataRedeem, - // ); - // } - // } - - // it("Can redeem set token", async () => { - // const inputTokenBalanceBefore = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - // await subject(); - // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - // const inputTokenBalanceAfter = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); - // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); - // }); - // }); - // }); + it("Can issue set token from ETH", async () => { + const maxAmountIn = ether(11); + const setTokenAmount = ether(10); + const issueParams: IssueParams = { + setToken: setToken.address, + inputToken: addresses.tokens.weth, + amountSetToken: setTokenAmount, + maxAmountInputToken: maxAmountIn, + swapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await owner.wallet.getBalance(); + await flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + const inputTokenBalanceAfter = await owner.wallet.getBalance(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); + + // ["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); + // let inputToken: IERC20 | IWETH; + // // let swapDataInputTokenToEth: SwapData; + // // let swapDataEthToInputToken: SwapData; + // let issueParams: IssueParams; + // // let redeemParams: RedeemParams; + + + // before(async () => { + // if (inputTokenName != "eth") { + // inputToken.approve(flashMintDex.address, maxAmountIn); + // } + // if (inputTokenName === "weth") { + // inputToken = IWETH__factory.connect(addresses.tokens[inputTokenName], owner.wallet); + // await inputToken.deposit({ value: maxAmountIn }); + // } + // if (inputTokenName === "USDC") { + // inputToken = IERC20__factory.connect(addresses.tokens[inputTokenName], owner.wallet); + // const whaleSigner = await impersonateAccount(addresses.whales.USDC); + // await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); + // } + // }); + // function subject() { + // issueParams = { + // setToken: setToken.address, + // inputToken: inputToken.address, + // amountSetToken: setTokenAmount, + // maxAmountInputToken: maxAmountIn, + // swapData: componentSwapDataIssue, + // issuanceModule: debtIssuanceModule.address, + // isDebtIssuance: true, + // }; + + // if (inputTokenName === "eth") { + // // When issuing from ETH use WETH address for inputToken + // issueParams.inputToken = addresses.tokens.weth; + // return flashMintDex.issueExactSetFromETH( + // issueParams, + // { + // value: maxAmountIn, + // }, + // ); + // } else { + // return flashMintDex.issueExactSetFromToken(issueParams); + // } + // } + // it("Can issue set token", async () => { + // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + // console.log('setTokenBalanceBefore', setTokenBalanceBefore.toString()); + // const inputTokenBalanceBefore = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // await subject(); + // const inputTokenBalanceAfter = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + // }); + // }); }); }); }); From 06beea44062a0ee5dd1ee8df537486f0ff6f498f Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 26 Jul 2024 15:03:00 -0400 Subject: [PATCH 10/61] univ3 tests pass --- contracts/exchangeIssuance/FlashMintDex.sol | 2 + .../integration/ethereum/flashMintDex.spec.ts | 67 +++++++++---------- utils/deploys/deployExtensions.ts | 3 - 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 9eecb252..a20bf2ad 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -341,6 +341,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); + // TODO: Calculate the max amount input token to be used for each swap dexAdapter.swapTokensForExactTokens(units, _issueParams.maxAmountInputToken, _issueParams.swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); @@ -380,6 +381,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountSold = maxAmountSell; } else { uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); + // TODO: Calculate the min amount output token to be received for each swap dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.swapData[i]); uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 77ee1f44..3de21f53 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -25,6 +25,7 @@ import { ether, /*usdc*/ } from "@utils/index"; // import { impersonateAccount } from "./utils"; const expect = getWaffleExpect(); +// const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; enum Exchange { None, @@ -109,9 +110,6 @@ if (process.env.INTEGRATIONTEST) { addresses.dexes.curve.calculator, addresses.dexes.curve.addressProvider, addresses.setFork.controller, - addresses.setFork.debtIssuanceModuleV2, - addresses.tokens.stEth, - addresses.dexes.curve.pools.stEthEth, ); }); @@ -143,37 +141,46 @@ if (process.env.INTEGRATIONTEST) { ); }); - context("when setToken with dsETH composition is deployed", () => { + context("when setToken with a simple LST composition is deployed", () => { let setToken: SetToken; const components = [ - // addresses.tokens.wstEth, - // addresses.tokens.rETH, - // addresses.tokens.sfrxEth, - // addresses.tokens.osEth, - // addresses.tokens.ETHx, + addresses.tokens.wstEth, + addresses.tokens.rETH, addresses.tokens.swETH, + // addresses.tokens.ETHx, ]; const positions = [ - // ethers.utils.parseEther("0.16"), - // ethers.utils.parseEther("0.16"), - // ethers.utils.parseEther("0.16"), - // ethers.utils.parseEther("0.16"), - // ethers.utils.parseEther("0.16"), - ethers.utils.parseEther("0.16"), + ethers.utils.parseEther("0.25"), + ethers.utils.parseEther("0.25"), + ethers.utils.parseEther("0.25"), + // ethers.utils.parseEther("0.25"), ]; const componentSwapDataIssue = [ - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, + { + exchange: Exchange.UniV3, + fees: [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.Curve, + // fees: [], + // path: [ETH_ADDRESS, addresses.tokens.ETHx], + // pool: "0x59Ab5a5b5d617E478a2479B0cAD80DA7e2831492", + // }, ]; // const componentSwapDataRedeem = [ @@ -191,8 +198,8 @@ if (process.env.INTEGRATIONTEST) { // ]; const modules = [addresses.setFork.debtIssuanceModuleV2]; - const tokenName = "Diversified Staked ETH Index"; - const tokenSymbol = "dsETH"; + const tokenName = "Simple Diversified Staked ETH Index"; + const tokenSymbol = "dsETHSimp"; before(async () => { const tx = await setTokenCreator.create( @@ -216,21 +223,9 @@ if (process.env.INTEGRATIONTEST) { owner.address, ADDRESS_ZERO, ); - - await flashMintDex.approveTokens( - [ - addresses.tokens.wstEth, - addresses.tokens.rETH, - addresses.tokens.sfrxEth, - addresses.tokens.osEth, - addresses.tokens.ETHx, - addresses.tokens.swETH, - ], - debtIssuanceModule.address - ); - await flashMintDex.approveSetToken(setToken.address, debtIssuanceModule.address); }); + it("setToken is deployed correctly", async () => { expect(await setToken.symbol()).to.eq(tokenSymbol); }); @@ -256,6 +251,8 @@ if (process.env.INTEGRATIONTEST) { }, ); const inputTokenBalanceAfter = await owner.wallet.getBalance(); + console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); + console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index c5fd8f50..37c9093d 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -493,9 +493,6 @@ export default class DeployExtensions { curveCalculatorAddress: Address, curveAddressProviderAddress: Address, setControllerAddress: Address, - debtIssuanceModuleAddress: Address, - stETHAddress: Address, - curveStEthEthPoolAddress: Address, ) { const dexAdapter = await this.deployDEXAdapterV2(); From 71af8c02971d856c3ed1a21146800782d688982c Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 29 Jul 2024 10:29:07 -0400 Subject: [PATCH 11/61] test sushiswap --- contracts/exchangeIssuance/FlashMintDex.sol | 12 ++++++-- test/integration/ethereum/addresses.ts | 1 + .../integration/ethereum/flashMintDex.spec.ts | 28 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index a20bf2ad..5f7969a1 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -91,6 +91,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 _amountOutputToken // The amount of output tokens received by the recipient ); + event MaxAmountInputTokenLogged(uint256 maxAmountInputToken); + /* ============ Modifiers ============ */ modifier isValidModule(address _issuanceModule) { @@ -341,8 +343,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - // TODO: Calculate the max amount input token to be used for each swap - dexAdapter.swapTokensForExactTokens(units, _issueParams.maxAmountInputToken, _issueParams.swapData[i]); + // Calculate the max amount of input token to be used for the swap + uint256 maxAmountInputToken = DEXAdapterV2.getAmountIn( + dexAdapter, + _issueParams.swapData[i], + units + ); + emit MaxAmountInputTokenLogged(maxAmountInputToken); + dexAdapter.swapTokensForExactTokens(units, maxAmountInputToken, _issueParams.swapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 553d78ba..e9120220 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -41,6 +41,7 @@ export const PRODUCTION_ADDRESSES = { wstEth: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", sfrxEth: "0xac3E018457B222d93114458476f3E3416Abbe38F", osEth: "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", + comp: "0xc00e94Cb662C3520282E6f5717214004A7f26888", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 3de21f53..a47a6087 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -147,13 +147,13 @@ if (process.env.INTEGRATIONTEST) { addresses.tokens.wstEth, addresses.tokens.rETH, addresses.tokens.swETH, - // addresses.tokens.ETHx, + 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"), + ethers.utils.parseEther("0.25"), ]; const componentSwapDataIssue = [ @@ -175,6 +175,12 @@ if (process.env.INTEGRATIONTEST) { path: [addresses.tokens.weth, addresses.tokens.swETH], pool: ADDRESS_ZERO, }, + { + exchange: Exchange.Sushiswap, + fees: [], + path: [addresses.tokens.weth, addresses.tokens.comp], + pool: ADDRESS_ZERO, + }, // { // exchange: Exchange.Curve, // fees: [], @@ -198,8 +204,8 @@ if (process.env.INTEGRATIONTEST) { // ]; const modules = [addresses.setFork.debtIssuanceModuleV2]; - const tokenName = "Simple Diversified Staked ETH Index"; - const tokenSymbol = "dsETHSimp"; + const tokenName = "Simple Index"; + const tokenSymbol = "icSimple"; before(async () => { const tx = await setTokenCreator.create( @@ -244,12 +250,24 @@ if (process.env.INTEGRATIONTEST) { }; const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await owner.wallet.getBalance(); - await flashMintDex.issueExactSetFromETH( + const tx = await flashMintDex.issueExactSetFromETH( issueParams, { value: maxAmountIn, }, ); + // Wait for the transaction to be mined + const receipt = await tx.wait(); + + const events = receipt.events.filter(event => event.event === "MaxAmountInputTokenLogged"); + + // Log and check the value of each maxAmountInputToken + events.forEach(event => { + const maxAmountInputToken = event.args.maxAmountInputToken; + console.log("Max Amount Input Token:", maxAmountInputToken.toString()); + // Additional assertions can be made here + }); + const inputTokenBalanceAfter = await owner.wallet.getBalance(); console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); From b038f67c2bf64ab2f851274ccbe2100fd9695b30 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 29 Jul 2024 12:52:17 -0400 Subject: [PATCH 12/61] wip usdc support --- contracts/exchangeIssuance/FlashMintDex.sol | 50 +++++++++++---- .../integration/ethereum/flashMintDex.spec.ts | 64 +++++++++++++++++-- 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 5f7969a1..413927cd 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -54,13 +54,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /* ============ Structs ============ */ struct IssueParams { - ISetToken setToken; // The address of the SetToken to be issued - IERC20 inputToken; // The address of the input token - uint256 amountSetToken; // The amount of SetTokens to issue - uint256 maxAmountInputToken; // The maximum amount of input tokens to be used to issue SetTokens - DEXAdapterV2.SwapData[] swapData; // The swap data from input token 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 + ISetToken setToken; // The address of the SetToken to be issued + IERC20 inputToken; // The address of the input token + uint256 amountSetToken; // The amount of SetTokens to issue + uint256 maxAmountInputToken; // The maximum amount of input tokens to be used to issue SetTokens + DEXAdapterV2.SwapData[] componentSwapData; // The swap data from WETH to each component token + DEXAdapterV2.SwapData paymentTokenSwapData; // The swap data from input token to WETH + 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 RedeemParams { @@ -68,7 +69,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IERC20 outputToken; // The address of the output token uint256 amountSetToken; // The amount of SetTokens to redeem uint256 minOutputReceive; // The minimum amount of output tokens to receive - DEXAdapterV2.SwapData[] swapData; // The swap data from each component token to the output token + DEXAdapterV2.SwapData[] componentSwapData; // The swap data from each component token to the output 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 } @@ -194,6 +195,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { { _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); + _issueParams.maxAmountInputToken = _swapInputTokenForWETH(_issueParams); + _issueParams.inputToken = IERC20(WETH); uint256 totalInputTokenSold = _buyComponentsForInputToken(_issueParams); require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "FlashMint: OVERSPENT TOKEN"); @@ -312,10 +315,29 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Acquires SetToken components by executing swaps whose callata is passed in _swapData. + * Swaps a given amount of an ERC20 token for WETH using the DEXAdapter. + * + * @param _issueParams Struct containing input token amount and swap params + * + * @return amountWethOut Amount of WETH received after the swap + */ + function _swapInputTokenForWETH(IssueParams memory _issueParams) + internal + returns (uint256 amountWethOut) + { + if (_issueParams.inputToken == IERC20(WETH)) { + return _issueParams.maxAmountInputToken; + } + if (_issueParams.inputToken != IERC20(WETH)) { + return dexAdapter.swapTokensForExactTokens(_issueParams.maxAmountInputToken, _issueParams.maxAmountInputToken, _issueParams.paymentTokenSwapData); + } + } + + /** + * 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 + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * * @return totalInputTokenSold Total amount of input token spent on this issuance */ @@ -346,11 +368,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { // Calculate the max amount of input token to be used for the swap uint256 maxAmountInputToken = DEXAdapterV2.getAmountIn( dexAdapter, - _issueParams.swapData[i], + _issueParams.componentSwapData[i], units ); emit MaxAmountInputTokenLogged(maxAmountInputToken); - dexAdapter.swapTokensForExactTokens(units, maxAmountInputToken, _issueParams.swapData[i]); + dexAdapter.swapTokensForExactTokens(units, maxAmountInputToken, _issueParams.componentSwapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); @@ -379,7 +401,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); uint256 outputTokenBalanceBefore = _redeemParams.outputToken.balanceOf(address(this)); - for (uint256 i = 0; i < _redeemParams.swapData.length; i++) { + for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { uint256 maxAmountSell = componentUnits[i]; uint256 componentAmountSold; @@ -390,7 +412,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } else { uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); // TODO: Calculate the min amount output token to be received for each swap - dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.swapData[i]); + dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.componentSwapData[i]); uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index a47a6087..33f204db 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -15,14 +15,14 @@ import { SetTokenCreator__factory, FlashMintDex, // IERC20, - // IERC20__factory, + IERC20__factory, // IWETH, // IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; import { ADDRESS_ZERO } from "@utils/constants"; -import { ether, /*usdc*/ } from "@utils/index"; -// import { impersonateAccount } from "./utils"; +import { ether, usdc } from "@utils/index"; +import { impersonateAccount } from "./utils"; const expect = getWaffleExpect(); // const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; @@ -47,7 +47,8 @@ type IssueParams = { inputToken: Address; amountSetToken: BigNumber; maxAmountInputToken: BigNumber; - swapData: SwapData[]; + componentSwapData: SwapData[]; + paymentTokenSwapData: SwapData; issuanceModule: Address; isDebtIssuance: boolean; }; @@ -189,6 +190,13 @@ if (process.env.INTEGRATIONTEST) { // }, ]; + const paymentTokenSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.USDC, addresses.tokens.weth], + pool: ADDRESS_ZERO, + }; + // const componentSwapDataRedeem = [ // NO_OP_SWAP_DATA, // NO_OP_SWAP_DATA, @@ -244,7 +252,8 @@ if (process.env.INTEGRATIONTEST) { inputToken: addresses.tokens.weth, amountSetToken: setTokenAmount, maxAmountInputToken: maxAmountIn, - swapData: componentSwapDataIssue, + componentSwapData: componentSwapDataIssue, + paymentTokenSwapData: paymentTokenSwapData, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; @@ -256,6 +265,8 @@ if (process.env.INTEGRATIONTEST) { value: maxAmountIn, }, ); + + /* Start of Debugging - remove before PR */ // Wait for the transaction to be mined const receipt = await tx.wait(); @@ -267,6 +278,7 @@ if (process.env.INTEGRATIONTEST) { console.log("Max Amount Input Token:", maxAmountInputToken.toString()); // Additional assertions can be made here }); + /* End Debugging */ const inputTokenBalanceAfter = await owner.wallet.getBalance(); console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); @@ -276,6 +288,48 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); + it("Can issue set token from USDC", async () => { + const maxAmountIn = usdc(30000); + const setTokenAmount = ether(10); + const issueParams: IssueParams = { + setToken: setToken.address, + inputToken: addresses.tokens.USDC, + amountSetToken: setTokenAmount, + maxAmountInputToken: maxAmountIn, + componentSwapData: componentSwapDataIssue, + paymentTokenSwapData: paymentTokenSwapData, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + const usdcToken = IERC20__factory.connect(issueParams.inputToken, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); + usdcToken.approve(flashMintDex.address, maxAmountIn); + // flashMintDex.approveToken(issueParams.inputToken, issueParams.issuanceModule); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); + const tx = await flashMintDex.issueExactSetFromToken(issueParams); + /* Start of Debugging - remove before PR */ + // Wait for the transaction to be mined + const receipt = await tx.wait(); + + const events = receipt.events.filter(event => event.event === "MaxAmountInputTokenLogged"); + + // Log and check the value of each maxAmountInputToken + events.forEach(event => { + const maxAmountInputToken = event.args.maxAmountInputToken; + console.log("Max Amount Input Token:", maxAmountInputToken.toString()); + // Additional assertions can be made here + }); + /* End Debugging */ + const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + }); + // ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { // describe(`When inputToken is ${inputTokenName}`, () => { // const ethIn = ether(1001); From a8783e7e6891ea0248743f5e48b528c23fe99f48 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 29 Jul 2024 14:37:15 -0400 Subject: [PATCH 13/61] need to change buycomponents function to use weth only --- contracts/exchangeIssuance/FlashMintDex.sol | 13 ++++++++---- test/integration/ethereum/addresses.ts | 2 +- .../integration/ethereum/flashMintDex.spec.ts | 20 +++++++------------ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 413927cd..fef8c768 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -93,6 +93,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); event MaxAmountInputTokenLogged(uint256 maxAmountInputToken); + // event InputTokenBalanceLogged(uint256 inputTokenBalance); /* ============ Modifiers ============ */ @@ -321,16 +322,20 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return amountWethOut Amount of WETH received after the swap */ + /* TODO: Change to internal */ function _swapInputTokenForWETH(IssueParams memory _issueParams) - internal + public returns (uint256 amountWethOut) { if (_issueParams.inputToken == IERC20(WETH)) { return _issueParams.maxAmountInputToken; } - if (_issueParams.inputToken != IERC20(WETH)) { - return dexAdapter.swapTokensForExactTokens(_issueParams.maxAmountInputToken, _issueParams.maxAmountInputToken, _issueParams.paymentTokenSwapData); - } + + return dexAdapter.swapExactTokensForTokens( + _issueParams.maxAmountInputToken, + 0, + _issueParams.paymentTokenSwapData + ); } /** diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index e9120220..fb603f04 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -45,7 +45,7 @@ export const PRODUCTION_ADDRESSES = { }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", - dai: "0x075e72a5edf65f0a5f44699c7654c1a76941ddc8", + dai: "0xBF293D5138a2a1BA407B43672643434C43827179", weth: "0x2f0b23f53734252bda2277357e97e1517d6b042a", USDC: "0x55fe002aeff02f77364de339a1292923a15844b8", }, diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 33f204db..b2a3415a 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -182,17 +182,11 @@ if (process.env.INTEGRATIONTEST) { path: [addresses.tokens.weth, addresses.tokens.comp], pool: ADDRESS_ZERO, }, - // { - // exchange: Exchange.Curve, - // fees: [], - // path: [ETH_ADDRESS, addresses.tokens.ETHx], - // pool: "0x59Ab5a5b5d617E478a2479B0cAD80DA7e2831492", - // }, ]; const paymentTokenSwapData = { exchange: Exchange.UniV3, - fees: [100], + fees: [500], path: [addresses.tokens.USDC, addresses.tokens.weth], pool: ADDRESS_ZERO, }; @@ -288,7 +282,7 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); - it("Can issue set token from USDC", async () => { + it.only("Can issue set token from USDC", async () => { const maxAmountIn = usdc(30000); const setTokenAmount = ether(10); const issueParams: IssueParams = { @@ -305,8 +299,8 @@ if (process.env.INTEGRATIONTEST) { const whaleSigner = await impersonateAccount(addresses.whales.USDC); await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); usdcToken.approve(flashMintDex.address, maxAmountIn); - // flashMintDex.approveToken(issueParams.inputToken, issueParams.issuanceModule); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + console.log("setTokenBalanceBefore", setTokenBalanceBefore.toString()); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); const tx = await flashMintDex.issueExactSetFromToken(issueParams); @@ -314,18 +308,18 @@ if (process.env.INTEGRATIONTEST) { // Wait for the transaction to be mined const receipt = await tx.wait(); - const events = receipt.events.filter(event => event.event === "MaxAmountInputTokenLogged"); + const events = receipt.events.filter(event => event.event === "InputTokenBalanceLogged"); // Log and check the value of each maxAmountInputToken events.forEach(event => { - const maxAmountInputToken = event.args.maxAmountInputToken; - console.log("Max Amount Input Token:", maxAmountInputToken.toString()); + const inputTokenBalanceBefore = event.args.inputTokenBalanceBefore; + console.log("Max Amount Input Token:", inputTokenBalanceBefore.toString()); // Additional assertions can be made here }); /* End Debugging */ const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); - console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + console.log("setTokenBalanceAfter", setTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); From e35ba96d4d5686748cac2f369b58e91d834488d0 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 29 Jul 2024 15:51:04 -0400 Subject: [PATCH 14/61] return excess input token --- contracts/exchangeIssuance/FlashMintDex.sol | 92 ++++++++++++------- .../integration/ethereum/flashMintDex.spec.ts | 24 +++-- 2 files changed, 77 insertions(+), 39 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index fef8c768..e6639d3a 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -59,7 +59,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 amountSetToken; // The amount of SetTokens to issue uint256 maxAmountInputToken; // The maximum amount of input tokens to be used to issue SetTokens DEXAdapterV2.SwapData[] componentSwapData; // The swap data from WETH to each component token - DEXAdapterV2.SwapData paymentTokenSwapData; // The swap data from input token to WETH + DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from input token to WETH + DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to input 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 } @@ -189,22 +190,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @return totalInputTokenSold Amount of input token spent for issuance */ function issueExactSetFromToken(IssueParams memory _issueParams) - isValidModule(_issueParams.issuanceModule) external + isValidModule(_issueParams.issuanceModule) nonReentrant returns (uint256) { _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); - _issueParams.maxAmountInputToken = _swapInputTokenForWETH(_issueParams); - _issueParams.inputToken = IERC20(WETH); + _swapInputTokenForWETH(_issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.swapDataTokenToWeth); - uint256 totalInputTokenSold = _buyComponentsForInputToken(_issueParams); - require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "FlashMint: OVERSPENT TOKEN"); + uint256 totalInputTokenSold = _buyComponentsWithWeth(_issueParams); + // require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "FlashMint: OVERSPENT TOKEN"); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); - _returnExcessInputToken(_issueParams.inputToken, _issueParams.maxAmountInputToken, totalInputTokenSold); + // Swap leftover WETH back to input token and return to the caller + _swapRemainingWethForInputToken(_issueParams.inputToken, _issueParams.swapDataWethToToken); + _issueParams.inputToken.safeTransfer(msg.sender, _issueParams.inputToken.balanceOf(address(this))); emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); return totalInputTokenSold; @@ -220,10 +222,10 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @return amountEthReturn Amount of ether returned to the caller */ function issueExactSetFromETH(IssueParams memory _issueParams) - isValidModule(_issueParams.issuanceModule) external - nonReentrant payable + isValidModule(_issueParams.issuanceModule) + nonReentrant returns (uint256) { require(_issueParams.inputToken == IERC20(WETH), "FlashMint: INPUT TOKEN MUST BE WETH"); @@ -231,7 +233,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IWETH(WETH).deposit{value: msg.value}(); - uint256 totalEthSold = _buyComponentsForInputToken(_issueParams); + uint256 totalEthSold = _buyComponentsWithWeth(_issueParams); require(totalEthSold <= msg.value, "FlashMint: OVERSPENT ETH"); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); @@ -318,23 +320,52 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Swaps a given amount of an ERC20 token for WETH using the DEXAdapter. * - * @param _issueParams Struct containing input token amount and swap params + * @param _inputToken Address of the input token + * @param _inputTokenAmount Amount of input token to swap + * @param _swapData Swap data from ERC20 to WETH * * @return amountWethOut Amount of WETH received after the swap */ - /* TODO: Change to internal */ - function _swapInputTokenForWETH(IssueParams memory _issueParams) - public + function _swapInputTokenForWETH( + IERC20 _inputToken, + uint256 _inputTokenAmount, + DEXAdapterV2.SwapData memory _swapData + ) + internal returns (uint256 amountWethOut) { - if (_issueParams.inputToken == IERC20(WETH)) { - return _issueParams.maxAmountInputToken; + if (_inputToken == IERC20(WETH)) { + return _inputTokenAmount; + } + + return dexAdapter.swapExactTokensForTokens( + _inputTokenAmount, + 0, + _swapData + ); + } + + /** + * Swaps a given amount of an WETH for ERC20 using the DEXAdapter. + * + * @param _inputToken Address of the input token + * @param _swapData Swap data from WETH to ERC20 + * + * @return amountOut Amount of ERC20 received after the swap + */ + function _swapRemainingWethForInputToken(IERC20 _inputToken, DEXAdapterV2.SwapData memory _swapData) + internal + returns (uint256 amountOut) + { + uint256 wethAmount = IWETH(WETH).balanceOf(address(this)); + if (_inputToken == IERC20(WETH)) { + return wethAmount; } return dexAdapter.swapExactTokensForTokens( - _issueParams.maxAmountInputToken, + wethAmount, 0, - _issueParams.paymentTokenSwapData + _swapData ); } @@ -344,11 +375,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalInputTokenSold Total amount of input token spent on this issuance + * @return totalWethSold Total amount of WETH spent to buy components */ - function _buyComponentsForInputToken(IssueParams memory _issueParams) + function _buyComponentsWithWeth(IssueParams memory _issueParams) internal - returns (uint256 totalInputTokenSold) + returns (uint256 totalWethSold) { uint256 componentAmountBought; @@ -359,32 +390,31 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _issueParams.amountSetToken ); - uint256 inputTokenBalanceBefore = _issueParams.inputToken.balanceOf(address(this)); + uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); for (uint256 i = 0; i < components.length; i++) { address component = components[i]; uint256 units = componentUnits[i]; - // If the component is equal to the input token we don't have to trade - if (component == address(_issueParams.inputToken)) { - totalInputTokenSold = totalInputTokenSold.add(units); + // If the component is equal to WETH we don't have to trade + if (component == address(WETH)) { + totalWethSold = totalWethSold.add(units); componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - // Calculate the max amount of input token to be used for the swap - uint256 maxAmountInputToken = DEXAdapterV2.getAmountIn( + // Calculate the max amount of WETH to be used for the swap + uint256 maxAmountWeth = DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], units ); - emit MaxAmountInputTokenLogged(maxAmountInputToken); - dexAdapter.swapTokensForExactTokens(units, maxAmountInputToken, _issueParams.componentSwapData[i]); + dexAdapter.swapTokensForExactTokens(units, maxAmountWeth, _issueParams.componentSwapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); } } - uint256 inputTokenBalanceAfter = _issueParams.inputToken.balanceOf(address(this)); - totalInputTokenSold = totalInputTokenSold.add(inputTokenBalanceBefore.sub(inputTokenBalanceAfter)); + uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); + totalWethSold = totalWethSold.add(wethBalanceBefore.sub(wethBalanceAfter)); } /** diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index b2a3415a..d56bde52 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -48,7 +48,8 @@ type IssueParams = { amountSetToken: BigNumber; maxAmountInputToken: BigNumber; componentSwapData: SwapData[]; - paymentTokenSwapData: SwapData; + swapDataTokenToWeth: SwapData; + swapDataWethToToken: SwapData; issuanceModule: Address; isDebtIssuance: boolean; }; @@ -184,13 +185,20 @@ if (process.env.INTEGRATIONTEST) { }, ]; - const paymentTokenSwapData = { + const swapDataFromInputToken = { exchange: Exchange.UniV3, fees: [500], path: [addresses.tokens.USDC, addresses.tokens.weth], pool: ADDRESS_ZERO, }; + const swapDataToInputToken = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.USDC], + pool: ADDRESS_ZERO, + }; + // const componentSwapDataRedeem = [ // NO_OP_SWAP_DATA, // NO_OP_SWAP_DATA, @@ -247,7 +255,8 @@ if (process.env.INTEGRATIONTEST) { amountSetToken: setTokenAmount, maxAmountInputToken: maxAmountIn, componentSwapData: componentSwapDataIssue, - paymentTokenSwapData: paymentTokenSwapData, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; @@ -282,16 +291,17 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); - it.only("Can issue set token from USDC", async () => { + it("Can issue set token from USDC", async () => { const maxAmountIn = usdc(30000); const setTokenAmount = ether(10); const issueParams: IssueParams = { setToken: setToken.address, - inputToken: addresses.tokens.USDC, + inputToken: addresses.tokens.weth, amountSetToken: setTokenAmount, maxAmountInputToken: maxAmountIn, componentSwapData: componentSwapDataIssue, - paymentTokenSwapData: paymentTokenSwapData, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; @@ -300,9 +310,7 @@ if (process.env.INTEGRATIONTEST) { await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); usdcToken.approve(flashMintDex.address, maxAmountIn); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - console.log("setTokenBalanceBefore", setTokenBalanceBefore.toString()); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); const tx = await flashMintDex.issueExactSetFromToken(issueParams); /* Start of Debugging - remove before PR */ // Wait for the transaction to be mined From 5d7af6c7b20df3685565339242e2e53cbf935d77 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 30 Jul 2024 11:12:11 -0400 Subject: [PATCH 15/61] issue from erc20 --- contracts/exchangeIssuance/FlashMintDex.sol | 26 +++++++++++--- .../integration/ethereum/flashMintDex.spec.ts | 34 +++---------------- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index e6639d3a..da03025c 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -200,7 +200,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _swapInputTokenForWETH(_issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.swapDataTokenToWeth); uint256 totalInputTokenSold = _buyComponentsWithWeth(_issueParams); - // require(totalInputTokenSold <= _issueParams.maxAmountInputToken, "FlashMint: OVERSPENT TOKEN"); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); @@ -209,7 +208,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _issueParams.inputToken.safeTransfer(msg.sender, _issueParams.inputToken.balanceOf(address(this))); emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); - return totalInputTokenSold; + return 0; } @@ -301,7 +300,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { return ethAmount; } - + /* ============ Internal Functions ============ */ /** * Sets a max approval limit for an ERC20 token, provided the current allowance @@ -320,9 +319,9 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Swaps a given amount of an ERC20 token for WETH using the DEXAdapter. * - * @param _inputToken Address of the input token + * @param _inputToken Address of the ERC20 input token * @param _inputTokenAmount Amount of input token to swap - * @param _swapData Swap data from ERC20 to WETH + * @param _swapData Swap data from input token to WETH * * @return amountWethOut Amount of WETH received after the swap */ @@ -369,6 +368,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); } + /** + * Issues an exact amount of SetTokens for given amount of WETH. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return totalWethSold Amount of WETH used to buy components + */ + function _issueExactSetFromWeth(IssueParams memory _issueParams) + internal + isValidModule(_issueParams.issuanceModule) + nonReentrant + returns (uint256 totalWethSold) + { + uint256 totalWethSold = _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. diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d56bde52..7de17d28 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -262,27 +262,13 @@ if (process.env.INTEGRATIONTEST) { }; const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await owner.wallet.getBalance(); - const tx = await flashMintDex.issueExactSetFromETH( + await flashMintDex.issueExactSetFromETH( issueParams, { value: maxAmountIn, }, ); - /* Start of Debugging - remove before PR */ - // Wait for the transaction to be mined - const receipt = await tx.wait(); - - const events = receipt.events.filter(event => event.event === "MaxAmountInputTokenLogged"); - - // Log and check the value of each maxAmountInputToken - events.forEach(event => { - const maxAmountInputToken = event.args.maxAmountInputToken; - console.log("Max Amount Input Token:", maxAmountInputToken.toString()); - // Additional assertions can be made here - }); - /* End Debugging */ - const inputTokenBalanceAfter = await owner.wallet.getBalance(); console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); @@ -296,7 +282,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenAmount = ether(10); const issueParams: IssueParams = { setToken: setToken.address, - inputToken: addresses.tokens.weth, + inputToken: addresses.tokens.USDC, amountSetToken: setTokenAmount, maxAmountInputToken: maxAmountIn, componentSwapData: componentSwapDataIssue, @@ -311,20 +297,8 @@ if (process.env.INTEGRATIONTEST) { usdcToken.approve(flashMintDex.address, maxAmountIn); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - const tx = await flashMintDex.issueExactSetFromToken(issueParams); - /* Start of Debugging - remove before PR */ - // Wait for the transaction to be mined - const receipt = await tx.wait(); - - const events = receipt.events.filter(event => event.event === "InputTokenBalanceLogged"); - - // Log and check the value of each maxAmountInputToken - events.forEach(event => { - const inputTokenBalanceBefore = event.args.inputTokenBalanceBefore; - console.log("Max Amount Input Token:", inputTokenBalanceBefore.toString()); - // Additional assertions can be made here - }); - /* End Debugging */ + await flashMintDex.issueExactSetFromToken(issueParams); + const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); console.log("setTokenBalanceAfter", setTokenBalanceAfter.toString()); From 473501885b16a968b9baa8de72db3995a66f776e Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 30 Jul 2024 11:57:18 -0400 Subject: [PATCH 16/61] internal issue exact set from weth function --- contracts/exchangeIssuance/FlashMintDex.sol | 57 +++++++------------ .../integration/ethereum/flashMintDex.spec.ts | 6 +- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index da03025c..8b43b0fe 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -187,28 +187,25 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalInputTokenSold Amount of input token spent for issuance + * @return excessInputTokenAmt Amount of input token returned to the caller */ function issueExactSetFromToken(IssueParams memory _issueParams) external isValidModule(_issueParams.issuanceModule) nonReentrant - returns (uint256) + returns (uint256 excessInputTokenAmt) { - _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); - _swapInputTokenForWETH(_issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.swapDataTokenToWeth); + uint256 wethReceived = _swapInputTokenForWETH(_issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.swapDataTokenToWeth); - uint256 totalInputTokenSold = _buyComponentsWithWeth(_issueParams); - - IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); + uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); + require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); // Swap leftover WETH back to input token and return to the caller - _swapRemainingWethForInputToken(_issueParams.inputToken, _issueParams.swapDataWethToToken); - _issueParams.inputToken.safeTransfer(msg.sender, _issueParams.inputToken.balanceOf(address(this))); + excessInputTokenAmt = _swapWethForInputToken(wethReceived.sub(totalEthSold), _issueParams.inputToken, _issueParams.swapDataWethToToken); + _issueParams.inputToken.safeTransfer(msg.sender, excessInputTokenAmt); emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); - return 0; } @@ -232,10 +229,9 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IWETH(WETH).deposit{value: msg.value}(); - uint256 totalEthSold = _buyComponentsWithWeth(_issueParams); + uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); require(totalEthSold <= msg.value, "FlashMint: OVERSPENT ETH"); - IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); uint256 amountEthReturn = msg.value.sub(totalEthSold); if (amountEthReturn > 0) { @@ -244,7 +240,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), totalEthSold, _issueParams.amountSetToken); - return amountEthReturn; + return amountEthReturn; } /** @@ -256,8 +252,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @return outputAmount Amount of output tokens sent to the caller */ function redeemExactSetForToken(RedeemParams memory _redeemParams) - isValidModule(_redeemParams.issuanceModule) external + isValidModule(_redeemParams.issuanceModule) nonReentrant returns (uint256) { @@ -282,8 +278,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @return ethAmount The amount of ETH received. */ function redeemExactSetForETH(RedeemParams memory _redeemParams) - isValidModule(_redeemParams.issuanceModule) external + isValidModule(_redeemParams.issuanceModule) nonReentrant returns (uint256) { @@ -347,22 +343,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Swaps a given amount of an WETH for ERC20 using the DEXAdapter. * + * @param _wethAmount Amount of WETH to swap for input token * @param _inputToken Address of the input token - * @param _swapData Swap data from WETH to ERC20 + * @param _swapData Swap data from WETH to input token * * @return amountOut Amount of ERC20 received after the swap */ - function _swapRemainingWethForInputToken(IERC20 _inputToken, DEXAdapterV2.SwapData memory _swapData) + function _swapWethForInputToken(uint256 _wethAmount, IERC20 _inputToken, DEXAdapterV2.SwapData memory _swapData) internal returns (uint256 amountOut) { - uint256 wethAmount = IWETH(WETH).balanceOf(address(this)); + // If the input token is equal to WETH we don't have to trade if (_inputToken == IERC20(WETH)) { - return wethAmount; + return _wethAmount; } return dexAdapter.swapExactTokensForTokens( - wethAmount, + _wethAmount, 0, _swapData ); @@ -377,11 +374,9 @@ contract FlashMintDex is Ownable, ReentrancyGuard { */ function _issueExactSetFromWeth(IssueParams memory _issueParams) internal - isValidModule(_issueParams.issuanceModule) - nonReentrant returns (uint256 totalWethSold) { - uint256 totalWethSold = _buyComponentsWithWeth(_issueParams); + totalWethSold = _buyComponentsWithWeth(_issueParams); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); } @@ -426,7 +421,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { dexAdapter.swapTokensForExactTokens(units, maxAmountWeth, _issueParams.componentSwapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); - require(componentAmountBought >= units, "ExchangeIssuance: UNDERBOUGHT COMPONENT"); + require(componentAmountBought >= units, "FlashMint: UNDERBOUGHT COMPONENT"); } } uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); @@ -485,20 +480,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IBasicIssuanceModule(_issuanceModule).redeem(_setToken, _amount, address(this)); } - /** - * Returns excess input token - * - * @param _inputToken Address of the input token to return - * @param _receivedAmount Amount received by the caller - * @param _spentAmount Amount spent for issuance - */ - function _returnExcessInputToken(IERC20 _inputToken, uint256 _receivedAmount, uint256 _spentAmount) internal { - uint256 amountTokenReturn = _receivedAmount.sub(_spentAmount); - if (amountTokenReturn > 0) { - _inputToken.safeTransfer(msg.sender, amountTokenReturn); - } - } - /** * Returns component positions required for issuance * diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 7de17d28..d1647231 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -297,11 +297,15 @@ if (process.env.INTEGRATIONTEST) { usdcToken.approve(flashMintDex.address, maxAmountIn); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromToken(issueParams); + console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); + const tx = await flashMintDex.issueExactSetFromToken(issueParams); + const receipt = await tx.wait(); + console.log(`Gas used for myFunction: ${receipt.gasUsed.toString()}`); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); console.log("setTokenBalanceAfter", setTokenBalanceAfter.toString()); + console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); From 8836faa6cfe918f686c00718cc6e836dd6f72330 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 30 Jul 2024 17:16:00 -0400 Subject: [PATCH 17/61] failing redeem for eth test --- contracts/exchangeIssuance/FlashMintDex.sol | 40 ++++--- .../integration/ethereum/flashMintDex.spec.ts | 106 ++++++++++++++---- 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 8b43b0fe..9bb01f7b 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -71,6 +71,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 amountSetToken; // The amount of SetTokens to redeem uint256 minOutputReceive; // The minimum amount of output tokens to receive DEXAdapterV2.SwapData[] componentSwapData; // The swap data from each component token to the output token + DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from input token to WETH + DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to input 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 } @@ -260,7 +262,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 outputAmount; _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - outputAmount = _sellComponentsForOutputToken(_redeemParams); + outputAmount = _sellComponentsForWeth(_redeemParams); require(outputAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); @@ -283,10 +285,10 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - require(_redeemParams.outputToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); + // require(_redeemParams.outputToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - uint ethAmount = _sellComponentsForOutputToken(_redeemParams); + uint256 ethAmount = _sellComponentsForWeth(_redeemParams); require(ethAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); @@ -433,11 +435,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalOutputTokenBought Total amount of output token received after liquidating all SetToken components + * @return totalWethBought Total amount of output token received after liquidating all SetToken components */ - function _sellComponentsForOutputToken(RedeemParams memory _redeemParams) + function _sellComponentsForWeth(RedeemParams memory _redeemParams) internal - returns (uint256 totalOutputTokenBought) + returns (uint256 totalWethBought) { (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( _redeemParams.issuanceModule, @@ -446,26 +448,22 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemParams.amountSetToken ); - uint256 outputTokenBalanceBefore = _redeemParams.outputToken.balanceOf(address(this)); + uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - uint256 maxAmountSell = componentUnits[i]; - uint256 componentAmountSold; - // If the component is equal to the output token we don't have to trade - if (components[i] == address(_redeemParams.outputToken)) { - totalOutputTokenBought = totalOutputTokenBought.add(maxAmountSell); - componentAmountSold = maxAmountSell; + if (components[i] == address(WETH)) { + totalWethBought = totalWethBought.add(componentUnits[i]); } else { - uint256 componentBalanceBefore = IERC20(components[i]).balanceOf(address(this)); - // TODO: Calculate the min amount output token to be received for each swap - dexAdapter.swapTokensForExactTokens(componentAmountSold, totalOutputTokenBought, _redeemParams.componentSwapData[i]); - uint256 componentBalanceAfter = IERC20(components[i]).balanceOf(address(this)); - componentAmountSold = componentBalanceBefore.sub(componentBalanceAfter); - require(maxAmountSell >= componentAmountSold, "ExchangeIssuance: OVERSOLD COMPONENT"); + uint256 minAmountWeth = DEXAdapterV2.getAmountOut( + dexAdapter, + _redeemParams.componentSwapData[i], + componentUnits[i] + ); + dexAdapter.swapExactTokensForTokens(componentUnits[i], minAmountWeth, _redeemParams.componentSwapData[i]); } } - uint256 outputTokenBalanceAfter = _redeemParams.outputToken.balanceOf(address(this)); - totalOutputTokenBought = totalOutputTokenBought.add(outputTokenBalanceAfter.sub(outputTokenBalanceBefore)); + uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); + totalWethBought = wethBalanceAfter.sub(wethBalanceBefore); } /** diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d1647231..2f51f445 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -54,15 +54,17 @@ type IssueParams = { isDebtIssuance: boolean; }; -// type RedeemParams = { -// setToken: Address; -// outputToken: Address; -// amountSetToken: BigNumber; -// maxAmountInputToken: BigNumber; -// swapData: SwapData[]; -// issuanceModule: Address; -// isDebtIssuance: boolean; -// }; +type RedeemParams = { + setToken: Address; + outputToken: Address; + amountSetToken: BigNumber; + minAmountOutputToken: BigNumber; + componentSwapData: SwapData[]; + swapDataTokenToWeth: SwapData; + swapDataWethToToken: SwapData; + issuanceModule: Address; + isDebtIssuance: boolean; +}; // const NO_OP_SWAP_DATA: SwapData = { // path: [], @@ -199,19 +201,32 @@ if (process.env.INTEGRATIONTEST) { pool: ADDRESS_ZERO, }; - // const componentSwapDataRedeem = [ - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // NO_OP_SWAP_DATA, - // { - // exchange: Exchange.UniV3, - // fees: [500], - // path: [ addresses.tokens.swETH, addresses.tokens.weth], - // 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"; @@ -310,6 +325,53 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); + describe("When set token has been issued", () => { + const setTokenAmount = ether(10); + const minAmountOut = ether(5); + beforeEach(async () => { + + await setToken.approve(flashMintDex.address, ether(10)); + }); + + function subject() { + + const redeemParams: RedeemParams = { + setToken: setToken.address, + outputToken: addresses.tokens.weth, + amountSetToken: setTokenAmount, + minAmountOutputToken: minAmountOut, + componentSwapData: componentSwapDataRedeem, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + return flashMintDex.redeemExactSetForETH(redeemParams); + // } else { + // return flashMintHyETH.redeemExactSetForERC20( + // setToken.address, + // setTokenAmount, + // inputToken.address, + // minAmountOut, + // swapDataEthToInputToken, + // componentSwapDataRedeem, + // ); + // } + } + + it("Can redeem set token", async () => { + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + // const inputTokenBalanceAfter = + // inputTokenName === "eth" + // ? await owner.wallet.getBalance() + // : await inputToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); + }); + }); + // ["eth", "weth", "USDC"].forEach((inputTokenName: keyof typeof addresses.tokens | "eth") => { // describe(`When inputToken is ${inputTokenName}`, () => { // const ethIn = ether(1001); From 8d2ce4ce7567c6952379d5ce60160763b1da6335 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 30 Jul 2024 17:49:48 -0400 Subject: [PATCH 18/61] redeem for eth test passes --- test/integration/ethereum/flashMintDex.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 2f51f445..eefe8c0c 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -58,7 +58,7 @@ type RedeemParams = { setToken: Address; outputToken: Address; amountSetToken: BigNumber; - minAmountOutputToken: BigNumber; + minOutputReceive: BigNumber; componentSwapData: SwapData[]; swapDataTokenToWeth: SwapData; swapDataWethToToken: SwapData; @@ -145,7 +145,7 @@ if (process.env.INTEGRATIONTEST) { ); }); - context("when setToken with a simple LST composition is deployed", () => { + context("when setToken with a simple composition is deployed", () => { let setToken: SetToken; const components = [ addresses.tokens.wstEth, @@ -339,7 +339,7 @@ if (process.env.INTEGRATIONTEST) { setToken: setToken.address, outputToken: addresses.tokens.weth, amountSetToken: setTokenAmount, - minAmountOutputToken: minAmountOut, + minOutputReceive: minAmountOut, componentSwapData: componentSwapDataRedeem, swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, From 7b889e20b8efb2d29d913823a4ed563a6e029c2f Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 31 Jul 2024 11:42:58 -0400 Subject: [PATCH 19/61] redeem to usdc test --- contracts/exchangeIssuance/FlashMintDex.sol | 8 +-- .../integration/ethereum/flashMintDex.spec.ts | 53 +++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 9bb01f7b..c2b85abb 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -226,7 +226,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - require(_issueParams.inputToken == IERC20(WETH), "FlashMint: INPUT TOKEN MUST BE WETH"); + // require(_issueParams.inputToken == IERC20(WETH), "FlashMint: INPUT TOKEN MUST BE WETH"); require(msg.value > 0, "FlashMint: NO ETH SENT"); IWETH(WETH).deposit{value: msg.value}(); @@ -259,10 +259,10 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - uint256 outputAmount; _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - outputAmount = _sellComponentsForWeth(_redeemParams); + uint256 wethReceived = _sellComponentsForWeth(_redeemParams); + uint256 outputAmount = _swapWethForInputToken(wethReceived, _redeemParams.outputToken, _redeemParams.swapDataWethToToken); require(outputAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); @@ -415,6 +415,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); // Calculate the max amount of WETH to be used for the swap + // TODO: Change this to compare against maxAmountInputToken fter all components bought uint256 maxAmountWeth = DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], @@ -454,6 +455,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { if (components[i] == address(WETH)) { totalWethBought = totalWethBought.add(componentUnits[i]); } else { + // TODO: Change this to compare against minAmountOut after all components sold uint256 minAmountWeth = DEXAdapterV2.getAmountOut( dexAdapter, _redeemParams.componentSwapData[i], diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index eefe8c0c..d15f5f53 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -20,7 +20,7 @@ import { // IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; -import { ADDRESS_ZERO } from "@utils/constants"; +import { ADDRESS_ZERO, ETH_ADDRESS } from "@utils/constants"; import { ether, usdc } from "@utils/index"; import { impersonateAccount } from "./utils"; @@ -293,7 +293,7 @@ if (process.env.INTEGRATIONTEST) { }); it("Can issue set token from USDC", async () => { - const maxAmountIn = usdc(30000); + const maxAmountIn = usdc(27000); const setTokenAmount = ether(10); const issueParams: IssueParams = { setToken: setToken.address, @@ -327,17 +327,17 @@ if (process.env.INTEGRATIONTEST) { describe("When set token has been issued", () => { const setTokenAmount = ether(10); - const minAmountOut = ether(5); - beforeEach(async () => { + let outputToken: Address; + let minAmountOut: BigNumber; + beforeEach(async () => { await setToken.approve(flashMintDex.address, ether(10)); }); function subject() { - const redeemParams: RedeemParams = { setToken: setToken.address, - outputToken: addresses.tokens.weth, + outputToken: outputToken, amountSetToken: setTokenAmount, minOutputReceive: minAmountOut, componentSwapData: componentSwapDataRedeem, @@ -346,29 +346,38 @@ if (process.env.INTEGRATIONTEST) { issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; + if (redeemParams.outputToken === ETH_ADDRESS) { return flashMintDex.redeemExactSetForETH(redeemParams); - // } else { - // return flashMintHyETH.redeemExactSetForERC20( - // setToken.address, - // setTokenAmount, - // inputToken.address, - // minAmountOut, - // swapDataEthToInputToken, - // componentSwapDataRedeem, - // ); - // } + } else { + return flashMintDex.redeemExactSetForToken(redeemParams); + } } - it("Can redeem set token", async () => { + it("Can redeem set token for ETH", async () => { + outputToken = ETH_ADDRESS; + minAmountOut = ether(5); + const outputTokenBalanceBefore = await owner.wallet.getBalance(); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + 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 USDC", async () => { + outputToken = addresses.tokens.USDC; + minAmountOut = usdc(26000); + const usdcToken = IERC20__factory.connect(outputToken, owner.wallet); + const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + console.log("USDC Balance Before", outputTokenBalanceBefore.toString()); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - // const inputTokenBalanceAfter = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + console.log("USDC Balance After", outputTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); - // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.add(minAmountOut)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); }); From 59d44121366a7cb02cc41fbd83563c3249b21fa6 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 31 Jul 2024 14:09:48 -0400 Subject: [PATCH 20/61] issue and redeem from weth --- .../integration/ethereum/flashMintDex.spec.ts | 186 ++++++++---------- 1 file changed, 83 insertions(+), 103 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d15f5f53..cf9f5510 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -17,7 +17,7 @@ import { // IERC20, IERC20__factory, // IWETH, - // IWETH__factory, + IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; import { ADDRESS_ZERO, ETH_ADDRESS } from "@utils/constants"; @@ -25,7 +25,6 @@ import { ether, usdc } from "@utils/index"; import { impersonateAccount } from "./utils"; const expect = getWaffleExpect(); -// const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; enum Exchange { None, @@ -261,12 +260,15 @@ if (process.env.INTEGRATIONTEST) { expect(await setToken.symbol()).to.eq(tokenSymbol); }); - it("Can issue set token from ETH", async () => { - const maxAmountIn = ether(11); - const setTokenAmount = ether(10); + const setTokenAmount = ether(10); + let maxAmountIn: BigNumber; + let inputToken: Address; + function subject() { + console.log("inputToken", inputToken); + console.log("maxAmountIn", maxAmountIn.toString()); const issueParams: IssueParams = { setToken: setToken.address, - inputToken: addresses.tokens.weth, + inputToken: inputToken, amountSetToken: setTokenAmount, maxAmountInputToken: maxAmountIn, componentSwapData: componentSwapDataIssue, @@ -275,15 +277,25 @@ if (process.env.INTEGRATIONTEST) { issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; + if (issueParams.inputToken === ETH_ADDRESS) { + return flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + } else { + return flashMintDex.issueExactSetFromToken(issueParams); + } + } + + it("Can issue set token from ETH", async () => { + inputToken = ETH_ADDRESS; + maxAmountIn = ether(11); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await owner.wallet.getBalance(); - await flashMintDex.issueExactSetFromETH( - issueParams, - { - value: maxAmountIn, - }, - ); - + await subject(); const inputTokenBalanceAfter = await owner.wallet.getBalance(); console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); @@ -292,46 +304,67 @@ if (process.env.INTEGRATIONTEST) { expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); + it("Can issue set token from WETH", async () => { + inputToken = addresses.tokens.weth; + maxAmountIn = ether(11); + const wethToken = IWETH__factory.connect(inputToken, owner.wallet); + await wethToken.deposit({ value: maxAmountIn }); + wethToken.approve(flashMintDex.address, maxAmountIn); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + const tx = await subject(); + const receipt = await tx.wait(); + console.log(`Gas used for issuance: ${receipt.gasUsed.toString()}`); + const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + }); + it("Can issue set token from USDC", async () => { - const maxAmountIn = usdc(27000); - const setTokenAmount = ether(10); - const issueParams: IssueParams = { - setToken: setToken.address, - inputToken: addresses.tokens.USDC, - amountSetToken: setTokenAmount, - maxAmountInputToken: maxAmountIn, - componentSwapData: componentSwapDataIssue, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, - }; - const usdcToken = IERC20__factory.connect(issueParams.inputToken, owner.wallet); + inputToken = addresses.tokens.USDC; + maxAmountIn = usdc(27000); + const usdcToken = IERC20__factory.connect(inputToken, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); usdcToken.approve(flashMintDex.address, maxAmountIn); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); - const tx = await flashMintDex.issueExactSetFromToken(issueParams); + const tx = await subject(); const receipt = await tx.wait(); - console.log(`Gas used for myFunction: ${receipt.gasUsed.toString()}`); - + console.log(`Gas used for issuance: ${receipt.gasUsed.toString()}`); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - console.log("setTokenBalanceAfter", setTokenBalanceAfter.toString()); - console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); describe("When set token has been issued", () => { - const setTokenAmount = ether(10); let outputToken: Address; let minAmountOut: BigNumber; beforeEach(async () => { - await setToken.approve(flashMintDex.address, ether(10)); + const maxAmountIn = ether(11); + const issueParams: IssueParams = { + setToken: setToken.address, + inputToken: addresses.tokens.weth, + amountSetToken: setTokenAmount, + maxAmountInputToken: maxAmountIn, + componentSwapData: componentSwapDataIssue, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + await flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + await setToken.approve(flashMintDex.address, setTokenAmount); }); function subject() { @@ -365,6 +398,21 @@ if (process.env.INTEGRATIONTEST) { expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); + it("Can redeem set token for WETH", async () => { + outputToken = addresses.tokens.weth; + minAmountOut = ether(5); + const wethToken = IWETH__factory.connect(outputToken, owner.wallet); + const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + console.log("WETH Balance Before", outputTokenBalanceBefore.toString()); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + console.log("WETH Balance After", outputTokenBalanceAfter.toString()); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); + }); + it("Can redeem set token for USDC", async () => { outputToken = addresses.tokens.USDC; minAmountOut = usdc(26000); @@ -380,74 +428,6 @@ if (process.env.INTEGRATIONTEST) { expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); }); - - // ["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); - // let inputToken: IERC20 | IWETH; - // // let swapDataInputTokenToEth: SwapData; - // // let swapDataEthToInputToken: SwapData; - // let issueParams: IssueParams; - // // let redeemParams: RedeemParams; - - - // before(async () => { - // if (inputTokenName != "eth") { - // inputToken.approve(flashMintDex.address, maxAmountIn); - // } - // if (inputTokenName === "weth") { - // inputToken = IWETH__factory.connect(addresses.tokens[inputTokenName], owner.wallet); - // await inputToken.deposit({ value: maxAmountIn }); - // } - // if (inputTokenName === "USDC") { - // inputToken = IERC20__factory.connect(addresses.tokens[inputTokenName], owner.wallet); - // const whaleSigner = await impersonateAccount(addresses.whales.USDC); - // await inputToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); - // } - // }); - // function subject() { - // issueParams = { - // setToken: setToken.address, - // inputToken: inputToken.address, - // amountSetToken: setTokenAmount, - // maxAmountInputToken: maxAmountIn, - // swapData: componentSwapDataIssue, - // issuanceModule: debtIssuanceModule.address, - // isDebtIssuance: true, - // }; - - // if (inputTokenName === "eth") { - // // When issuing from ETH use WETH address for inputToken - // issueParams.inputToken = addresses.tokens.weth; - // return flashMintDex.issueExactSetFromETH( - // issueParams, - // { - // value: maxAmountIn, - // }, - // ); - // } else { - // return flashMintDex.issueExactSetFromToken(issueParams); - // } - // } - // it("Can issue set token", async () => { - // const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - // console.log('setTokenBalanceBefore', setTokenBalanceBefore.toString()); - // const inputTokenBalanceBefore = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // await subject(); - // const inputTokenBalanceAfter = - // inputTokenName === "eth" - // ? await owner.wallet.getBalance() - // : await inputToken.balanceOf(owner.address); - // const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - // expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - // expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); - // }); - // }); }); }); }); From e5f0e74d4237e5a1fb99c8e55db8a12c52d4a9cb Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 11:48:11 -0400 Subject: [PATCH 21/61] 1 deploymnent 2 controllers --- contracts/exchangeIssuance/FlashMintDex.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index c2b85abb..d4ebb67f 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -1,5 +1,6 @@ /* - Copyright 2022 Index Cooperative + 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 @@ -9,14 +10,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + SPDX-License-Identifier: Apache License, Version 2.0 */ + pragma solidity 0.6.10; pragma experimental ABIEncoderV2; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Math } from "@openzeppelin/contracts/math/Math.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; @@ -50,6 +52,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { address public immutable WETH; IController public immutable setController; + IController public immutable indexController; DEXAdapterV2.Addresses public dexAdapter; /* ============ Structs ============ */ @@ -95,24 +98,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 _amountOutputToken // The amount of output tokens received by the recipient ); - event MaxAmountInputTokenLogged(uint256 maxAmountInputToken); - // event InputTokenBalanceLogged(uint256 inputTokenBalance); - /* ============ Modifiers ============ */ modifier isValidModule(address _issuanceModule) { - require(setController.isModule(_issuanceModule), "FlashMint: INVALID ISSUANCE MODULE"); + require(setController.isModule(_issuanceModule) || indexController.isModule(_issuanceModule), "FlashMint: INVALID ISSUANCE MODULE"); _; } constructor( address _weth, IController _setController, + IController _indexController, DEXAdapterV2.Addresses memory _dexAddresses ) public { setController = _setController; + indexController = _indexController; dexAdapter = _dexAddresses; WETH = _weth; } From dd16c5623adc2550b62baaa7a25627e0b3656f8f Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 12:43:30 -0400 Subject: [PATCH 22/61] update tests for combined params --- contracts/exchangeIssuance/FlashMintDex.sol | 90 ++++++++----------- .../integration/ethereum/flashMintDex.spec.ts | 60 ++++++------- utils/deploys/deployExtensions.ts | 2 + 3 files changed, 69 insertions(+), 83 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index d4ebb67f..dbc8e777 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -56,30 +56,18 @@ contract FlashMintDex is Ownable, ReentrancyGuard { DEXAdapterV2.Addresses public dexAdapter; /* ============ Structs ============ */ - struct IssueParams { + struct IssueRedeemParams { ISetToken setToken; // The address of the SetToken to be issued - IERC20 inputToken; // The address of the input token + IERC20 paymentToken; // The address of the input/output token for issuance/redemption uint256 amountSetToken; // The amount of SetTokens to issue - uint256 maxAmountInputToken; // The maximum amount of input tokens to be used to issue SetTokens + uint256 paymentTokenLimit; // Max/min amount of payment token spent/received DEXAdapterV2.SwapData[] componentSwapData; // The swap data from WETH to each component token - DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from input token to WETH - DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to input token + DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from payment token to WETH + DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to payment 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 RedeemParams { - ISetToken setToken; // The address of the SetToken to be redeemed - IERC20 outputToken; // The address of the output token - uint256 amountSetToken; // The amount of SetTokens to redeem - uint256 minOutputReceive; // The minimum amount of output tokens to receive - DEXAdapterV2.SwapData[] componentSwapData; // The swap data from each component token to the output token - DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from input token to WETH - DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to input 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 - } - /* ============ Events ============ */ event FlashMint( @@ -93,7 +81,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { 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 + 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 ); @@ -191,25 +179,25 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return excessInputTokenAmt Amount of input token returned to the caller + * @return excessPaymentTokenAmt Amount of input token returned to the caller */ - function issueExactSetFromToken(IssueParams memory _issueParams) + function issueExactSetFromToken(IssueRedeemParams memory _issueParams) external isValidModule(_issueParams.issuanceModule) nonReentrant - returns (uint256 excessInputTokenAmt) + returns (uint256 excessPaymentTokenAmt) { - _issueParams.inputToken.safeTransferFrom(msg.sender, address(this), _issueParams.maxAmountInputToken); - uint256 wethReceived = _swapInputTokenForWETH(_issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.swapDataTokenToWeth); + _issueParams.paymentToken.safeTransferFrom(msg.sender, address(this), _issueParams.paymentTokenLimit); + uint256 wethReceived = _swapPaymentTokenForWETH(_issueParams.paymentToken, _issueParams.paymentTokenLimit, _issueParams.swapDataTokenToWeth); uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); // Swap leftover WETH back to input token and return to the caller - excessInputTokenAmt = _swapWethForInputToken(wethReceived.sub(totalEthSold), _issueParams.inputToken, _issueParams.swapDataWethToToken); - _issueParams.inputToken.safeTransfer(msg.sender, excessInputTokenAmt); + excessPaymentTokenAmt = _swapWethForPaymentToken(wethReceived.sub(totalEthSold), _issueParams.paymentToken, _issueParams.swapDataWethToToken); + _issueParams.paymentToken.safeTransfer(msg.sender, excessPaymentTokenAmt); - emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.inputToken, _issueParams.maxAmountInputToken, _issueParams.amountSetToken); + emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.paymentToken, _issueParams.paymentTokenLimit, _issueParams.amountSetToken); } @@ -221,7 +209,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return amountEthReturn Amount of ether returned to the caller */ - function issueExactSetFromETH(IssueParams memory _issueParams) + function issueExactSetFromETH(IssueRedeemParams memory _issueParams) external payable isValidModule(_issueParams.issuanceModule) @@ -255,7 +243,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return outputAmount Amount of output tokens sent to the caller */ - function redeemExactSetForToken(RedeemParams memory _redeemParams) + function redeemExactSetForToken(IssueRedeemParams memory _redeemParams) external isValidModule(_redeemParams.issuanceModule) nonReentrant @@ -264,12 +252,12 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 wethReceived = _sellComponentsForWeth(_redeemParams); - uint256 outputAmount = _swapWethForInputToken(wethReceived, _redeemParams.outputToken, _redeemParams.swapDataWethToToken); - require(outputAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _redeemParams.paymentToken, _redeemParams.swapDataWethToToken); + require(outputAmount >= _redeemParams.paymentTokenLimit, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); - _redeemParams.outputToken.safeTransfer(msg.sender, outputAmount); + _redeemParams.paymentToken.safeTransfer(msg.sender, outputAmount); - emit FlashRedeem(msg.sender, _redeemParams.setToken, _redeemParams.outputToken, _redeemParams.amountSetToken, outputAmount); + emit FlashRedeem(msg.sender, _redeemParams.setToken, _redeemParams.paymentToken, _redeemParams.amountSetToken, outputAmount); return outputAmount; } @@ -281,17 +269,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return ethAmount The amount of ETH received. */ - function redeemExactSetForETH(RedeemParams memory _redeemParams) + function redeemExactSetForETH(IssueRedeemParams memory _redeemParams) external isValidModule(_redeemParams.issuanceModule) nonReentrant returns (uint256) { - // require(_redeemParams.outputToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); + // require(_redeemParams.paymentToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 ethAmount = _sellComponentsForWeth(_redeemParams); - require(ethAmount >= _redeemParams.minOutputReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); + require(ethAmount >= _redeemParams.paymentTokenLimit, "FlashMint: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); payable(msg.sender).sendValue(ethAmount); @@ -319,26 +307,26 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Swaps a given amount of an ERC20 token for WETH using the DEXAdapter. * - * @param _inputToken Address of the ERC20 input token - * @param _inputTokenAmount Amount of input token to swap + * @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 _swapInputTokenForWETH( - IERC20 _inputToken, - uint256 _inputTokenAmount, + function _swapPaymentTokenForWETH( + IERC20 _paymentToken, + uint256 _paymentTokenAmount, DEXAdapterV2.SwapData memory _swapData ) internal returns (uint256 amountWethOut) { - if (_inputToken == IERC20(WETH)) { - return _inputTokenAmount; + if (_paymentToken == IERC20(WETH)) { + return _paymentTokenAmount; } return dexAdapter.swapExactTokensForTokens( - _inputTokenAmount, + _paymentTokenAmount, 0, _swapData ); @@ -348,17 +336,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * Swaps a given amount of an WETH for ERC20 using the DEXAdapter. * * @param _wethAmount Amount of WETH to swap for input token - * @param _inputToken Address of the 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 _swapWethForInputToken(uint256 _wethAmount, IERC20 _inputToken, DEXAdapterV2.SwapData memory _swapData) + function _swapWethForPaymentToken(uint256 _wethAmount, IERC20 _paymentToken, DEXAdapterV2.SwapData memory _swapData) internal returns (uint256 amountOut) { - // If the input token is equal to WETH we don't have to trade - if (_inputToken == IERC20(WETH)) { + // If the payment token is equal to WETH we don't have to trade + if (_paymentToken == IERC20(WETH)) { return _wethAmount; } @@ -376,7 +364,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return totalWethSold Amount of WETH used to buy components */ - function _issueExactSetFromWeth(IssueParams memory _issueParams) + function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) { @@ -392,7 +380,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return totalWethSold Total amount of WETH spent to buy components */ - function _buyComponentsWithWeth(IssueParams memory _issueParams) + function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) { @@ -417,7 +405,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); // Calculate the max amount of WETH to be used for the swap - // TODO: Change this to compare against maxAmountInputToken fter all components bought + // TODO: Change this to compare against paymentTokenLimit fter all components bought uint256 maxAmountWeth = DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], @@ -440,7 +428,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return totalWethBought Total amount of output token received after liquidating all SetToken components */ - function _sellComponentsForWeth(RedeemParams memory _redeemParams) + function _sellComponentsForWeth(IssueRedeemParams memory _redeemParams) internal returns (uint256 totalWethBought) { diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index cf9f5510..3ee58cd7 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -41,23 +41,11 @@ type SwapData = { exchange: Exchange; }; -type IssueParams = { +type IssueRedeemParams = { setToken: Address; - inputToken: Address; + paymentToken: Address; amountSetToken: BigNumber; - maxAmountInputToken: BigNumber; - componentSwapData: SwapData[]; - swapDataTokenToWeth: SwapData; - swapDataWethToToken: SwapData; - issuanceModule: Address; - isDebtIssuance: boolean; -}; - -type RedeemParams = { - setToken: Address; - outputToken: Address; - amountSetToken: BigNumber; - minOutputReceive: BigNumber; + paymentTokenLimit: BigNumber; componentSwapData: SwapData[]; swapDataTokenToWeth: SwapData; swapDataWethToToken: SwapData; @@ -112,6 +100,7 @@ if (process.env.INTEGRATIONTEST) { addresses.dexes.uniV3.quoter, addresses.dexes.curve.calculator, addresses.dexes.curve.addressProvider, + addresses.set.controller, addresses.setFork.controller, ); }); @@ -138,8 +127,14 @@ if (process.env.INTEGRATIONTEST) { expect(returnedAddresses.uniV3Router).to.eq(utils.getAddress(addresses.dexes.uniV3.router)); }); - it("controller address is set correctly", async () => { + 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), ); }); @@ -263,21 +258,22 @@ if (process.env.INTEGRATIONTEST) { const setTokenAmount = ether(10); let maxAmountIn: BigNumber; let inputToken: Address; + function subject() { console.log("inputToken", inputToken); console.log("maxAmountIn", maxAmountIn.toString()); - const issueParams: IssueParams = { + const issueParams: IssueRedeemParams = { setToken: setToken.address, - inputToken: inputToken, + paymentToken: inputToken, amountSetToken: setTokenAmount, - maxAmountInputToken: maxAmountIn, + paymentTokenLimit: maxAmountIn, componentSwapData: componentSwapDataIssue, swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; - if (issueParams.inputToken === ETH_ADDRESS) { + if (issueParams.paymentToken === ETH_ADDRESS) { return flashMintDex.issueExactSetFromETH( issueParams, { @@ -294,14 +290,14 @@ if (process.env.INTEGRATIONTEST) { maxAmountIn = ether(11); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - const inputTokenBalanceBefore = await owner.wallet.getBalance(); + const ethBalanceBefore = await owner.wallet.getBalance(); + console.log("ethBalanceBefore", ethBalanceBefore.toString()); await subject(); - const inputTokenBalanceAfter = await owner.wallet.getBalance(); - console.log("inputTokenBalanceBefore", inputTokenBalanceBefore.toString()); - console.log("inputTokenBalanceAfter", inputTokenBalanceAfter.toString()); + const ethBalanceAfter = await owner.wallet.getBalance(); + console.log("ethBalanceAfter", ethBalanceAfter.toString()); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxAmountIn)); }); it("Can issue set token from WETH", async () => { @@ -347,11 +343,11 @@ if (process.env.INTEGRATIONTEST) { beforeEach(async () => { const maxAmountIn = ether(11); - const issueParams: IssueParams = { + const issueParams: IssueRedeemParams = { setToken: setToken.address, - inputToken: addresses.tokens.weth, + paymentToken: ETH_ADDRESS, amountSetToken: setTokenAmount, - maxAmountInputToken: maxAmountIn, + paymentTokenLimit: maxAmountIn, componentSwapData: componentSwapDataIssue, swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, @@ -368,18 +364,18 @@ if (process.env.INTEGRATIONTEST) { }); function subject() { - const redeemParams: RedeemParams = { + const redeemParams: IssueRedeemParams = { setToken: setToken.address, - outputToken: outputToken, + paymentToken: outputToken, amountSetToken: setTokenAmount, - minOutputReceive: minAmountOut, + paymentTokenLimit: minAmountOut, componentSwapData: componentSwapDataRedeem, swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; - if (redeemParams.outputToken === ETH_ADDRESS) { + if (redeemParams.paymentToken === ETH_ADDRESS) { return flashMintDex.redeemExactSetForETH(redeemParams); } else { return flashMintDex.redeemExactSetForToken(redeemParams); diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index 37c9093d..c48f4463 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -493,6 +493,7 @@ export default class DeployExtensions { curveCalculatorAddress: Address, curveAddressProviderAddress: Address, setControllerAddress: Address, + indexControllerAddress: Address, ) { const dexAdapter = await this.deployDEXAdapterV2(); @@ -510,6 +511,7 @@ export default class DeployExtensions { ).deploy( wethAddress, setControllerAddress, + indexControllerAddress, { quickRouter: quickRouterAddress, sushiRouter: sushiRouterAddress, From bd749d24c846b382e8fe240fbb5178c6f105e553 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 13:34:47 -0400 Subject: [PATCH 23/61] cleanup --- contracts/exchangeIssuance/FlashMintDex.sol | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index dbc8e777..ce302e0d 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -192,12 +192,16 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); + uint256 unusedWeth = wethReceived.sub(totalEthSold); - // Swap leftover WETH back to input token and return to the caller - excessPaymentTokenAmt = _swapWethForPaymentToken(wethReceived.sub(totalEthSold), _issueParams.paymentToken, _issueParams.swapDataWethToToken); - _issueParams.paymentToken.safeTransfer(msg.sender, excessPaymentTokenAmt); + if (unusedWeth > 0) { + excessPaymentTokenAmt = _swapWethForPaymentToken(unusedWeth, _issueParams.paymentToken, _issueParams.swapDataWethToToken); + _issueParams.paymentToken.safeTransfer(msg.sender, excessPaymentTokenAmt); + } + + uint256 paymentTokenSold = _issueParams.paymentTokenLimit.sub(excessPaymentTokenAmt); - emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.paymentToken, _issueParams.paymentTokenLimit, _issueParams.amountSetToken); + emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.paymentToken, paymentTokenSold, _issueParams.amountSetToken); } @@ -216,7 +220,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - // require(_issueParams.inputToken == IERC20(WETH), "FlashMint: INPUT TOKEN MUST BE WETH"); require(msg.value > 0, "FlashMint: NO ETH SENT"); IWETH(WETH).deposit{value: msg.value}(); @@ -239,7 +242,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * 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 + * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance * * @return outputAmount Amount of output tokens sent to the caller */ @@ -275,7 +278,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - // require(_redeemParams.paymentToken == IERC20(WETH), "FlashMint: OUTPUT TOKEN MUST BE WETH"); _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 ethAmount = _sellComponentsForWeth(_redeemParams); @@ -309,9 +311,9 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @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 + * @param _swapData Swap data from input token to WETH * - * @return amountWethOut Amount of WETH received after the swap + * @return amountWethOut Amount of WETH received after the swap */ function _swapPaymentTokenForWETH( IERC20 _paymentToken, @@ -336,7 +338,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * 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 _paymentToken Address of the input token * @param _swapData Swap data from WETH to input token * * @return amountOut Amount of ERC20 received after the swap @@ -404,14 +406,12 @@ contract FlashMintDex is Ownable, ReentrancyGuard { componentAmountBought = units; } else { uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - // Calculate the max amount of WETH to be used for the swap - // TODO: Change this to compare against paymentTokenLimit fter all components bought - uint256 maxAmountWeth = DEXAdapterV2.getAmountIn( + uint256 wethSellAmt = DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], units ); - dexAdapter.swapTokensForExactTokens(units, maxAmountWeth, _issueParams.componentSwapData[i]); + dexAdapter.swapTokensForExactTokens(units, wethSellAmt, _issueParams.componentSwapData[i]); uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); require(componentAmountBought >= units, "FlashMint: UNDERBOUGHT COMPONENT"); @@ -441,17 +441,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + // If the component is equal to the output token we don't have to trade if (components[i] == address(WETH)) { totalWethBought = totalWethBought.add(componentUnits[i]); } else { - // TODO: Change this to compare against minAmountOut after all components sold - uint256 minAmountWeth = DEXAdapterV2.getAmountOut( + uint256 wethBuyAmt = DEXAdapterV2.getAmountOut( dexAdapter, _redeemParams.componentSwapData[i], componentUnits[i] ); - dexAdapter.swapExactTokensForTokens(componentUnits[i], minAmountWeth, _redeemParams.componentSwapData[i]); + dexAdapter.swapExactTokensForTokens(componentUnits[i], wethBuyAmt, _redeemParams.componentSwapData[i]); } } uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); From ce837f99c07ca31b4e5bf53c1265ef1c8258942f Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 16:00:41 -0400 Subject: [PATCH 24/61] finish separating payment info struct --- contracts/exchangeIssuance/FlashMintDex.sol | 39 ++++++++++--------- .../integration/ethereum/flashMintDex.spec.ts | 16 +++----- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index ce302e0d..c179e585 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -58,16 +58,19 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /* ============ Structs ============ */ struct IssueRedeemParams { ISetToken setToken; // The address of the SetToken to be issued - IERC20 paymentToken; // The address of the input/output token for issuance/redemption uint256 amountSetToken; // The amount of SetTokens to issue - uint256 paymentTokenLimit; // Max/min amount of payment token spent/received DEXAdapterV2.SwapData[] componentSwapData; // The swap data from WETH to each component token - DEXAdapterV2.SwapData swapDataTokenToWeth; // The swap data from payment token to WETH - DEXAdapterV2.SwapData swapDataWethToToken; // The swap data from WETH back to payment 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( @@ -181,27 +184,27 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return excessPaymentTokenAmt Amount of input token returned to the caller */ - function issueExactSetFromToken(IssueRedeemParams memory _issueParams) + function issueExactSetFromToken(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo) external isValidModule(_issueParams.issuanceModule) nonReentrant returns (uint256 excessPaymentTokenAmt) { - _issueParams.paymentToken.safeTransferFrom(msg.sender, address(this), _issueParams.paymentTokenLimit); - uint256 wethReceived = _swapPaymentTokenForWETH(_issueParams.paymentToken, _issueParams.paymentTokenLimit, _issueParams.swapDataTokenToWeth); + _paymentInfo.token.safeTransferFrom(msg.sender, address(this), _paymentInfo.limitAmt); + uint256 wethReceived = _swapPaymentTokenForWETH(_paymentInfo.token, _paymentInfo.limitAmt, _paymentInfo.swapDataTokenToWeth); uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); uint256 unusedWeth = wethReceived.sub(totalEthSold); if (unusedWeth > 0) { - excessPaymentTokenAmt = _swapWethForPaymentToken(unusedWeth, _issueParams.paymentToken, _issueParams.swapDataWethToToken); - _issueParams.paymentToken.safeTransfer(msg.sender, excessPaymentTokenAmt); + excessPaymentTokenAmt = _swapWethForPaymentToken(unusedWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + _paymentInfo.token.safeTransfer(msg.sender, excessPaymentTokenAmt); } - uint256 paymentTokenSold = _issueParams.paymentTokenLimit.sub(excessPaymentTokenAmt); + uint256 paymentTokenSold = _paymentInfo.limitAmt.sub(excessPaymentTokenAmt); - emit FlashMint(msg.sender, _issueParams.setToken, _issueParams.paymentToken, paymentTokenSold, _issueParams.amountSetToken); + emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSold, _issueParams.amountSetToken); } @@ -246,7 +249,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return outputAmount Amount of output tokens sent to the caller */ - function redeemExactSetForToken(IssueRedeemParams memory _redeemParams) + function redeemExactSetForToken(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) external isValidModule(_redeemParams.issuanceModule) nonReentrant @@ -255,12 +258,12 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 wethReceived = _sellComponentsForWeth(_redeemParams); - uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _redeemParams.paymentToken, _redeemParams.swapDataWethToToken); - require(outputAmount >= _redeemParams.paymentTokenLimit, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + require(outputAmount >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); - _redeemParams.paymentToken.safeTransfer(msg.sender, outputAmount); + _paymentInfo.token.safeTransfer(msg.sender, outputAmount); - emit FlashRedeem(msg.sender, _redeemParams.setToken, _redeemParams.paymentToken, _redeemParams.amountSetToken, outputAmount); + emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputAmount); return outputAmount; } @@ -272,7 +275,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return ethAmount The amount of ETH received. */ - function redeemExactSetForETH(IssueRedeemParams memory _redeemParams) + function redeemExactSetForETH(IssueRedeemParams memory _redeemParams, uint256 _minEthReceive) external isValidModule(_redeemParams.issuanceModule) nonReentrant @@ -281,7 +284,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 ethAmount = _sellComponentsForWeth(_redeemParams); - require(ethAmount >= _redeemParams.paymentTokenLimit, "FlashMint: INSUFFICIENT WETH RECEIVED"); + require(ethAmount >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); IWETH(WETH).withdraw(ethAmount); payable(msg.sender).sendValue(ethAmount); diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 3ee58cd7..d1e9c395 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -43,22 +43,18 @@ type SwapData = { type IssueRedeemParams = { setToken: Address; - paymentToken: Address; amountSetToken: BigNumber; - paymentTokenLimit: BigNumber; componentSwapData: SwapData[]; - swapDataTokenToWeth: SwapData; - swapDataWethToToken: SwapData; issuanceModule: Address; isDebtIssuance: boolean; }; -// const NO_OP_SWAP_DATA: SwapData = { -// path: [], -// fees: [], -// pool: ADDRESS_ZERO, -// exchange: Exchange.None, -// }; +// type PaymentInfo = { +// token: Address; +// limitAmt: BigNumber; +// swapDataTokenToWeth: SwapData; +// swapDataWethToToken: SwapData; +// } if (process.env.INTEGRATIONTEST) { describe.only("FlashMintDex - Integration Test", async () => { From 6e2a27a29c53647a8fdda5c1ea6ffef63f1110cf Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 16:21:50 -0400 Subject: [PATCH 25/61] refactor tests for separate payment struct --- .../integration/ethereum/flashMintDex.spec.ts | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d1e9c395..7df02dd8 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -14,9 +14,7 @@ import { SetTokenCreator, SetTokenCreator__factory, FlashMintDex, - // IERC20, IERC20__factory, - // IWETH, IWETH__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; @@ -49,12 +47,12 @@ type IssueRedeemParams = { isDebtIssuance: boolean; }; -// type PaymentInfo = { -// token: Address; -// limitAmt: BigNumber; -// swapDataTokenToWeth: SwapData; -// swapDataWethToToken: SwapData; -// } +type PaymentInfo = { + token: Address; + limitAmt: BigNumber; + swapDataTokenToWeth: SwapData; + swapDataWethToToken: SwapData; +}; if (process.env.INTEGRATIONTEST) { describe.only("FlashMintDex - Integration Test", async () => { @@ -260,16 +258,18 @@ if (process.env.INTEGRATIONTEST) { console.log("maxAmountIn", maxAmountIn.toString()); const issueParams: IssueRedeemParams = { setToken: setToken.address, - paymentToken: inputToken, amountSetToken: setTokenAmount, - paymentTokenLimit: maxAmountIn, componentSwapData: componentSwapDataIssue, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; - if (issueParams.paymentToken === ETH_ADDRESS) { + const paymentInfo: PaymentInfo = { + token: inputToken, + limitAmt: maxAmountIn, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + if (paymentInfo.token === ETH_ADDRESS) { return flashMintDex.issueExactSetFromETH( issueParams, { @@ -277,7 +277,7 @@ if (process.env.INTEGRATIONTEST) { }, ); } else { - return flashMintDex.issueExactSetFromToken(issueParams); + return flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); } } @@ -341,12 +341,8 @@ if (process.env.INTEGRATIONTEST) { const maxAmountIn = ether(11); const issueParams: IssueRedeemParams = { setToken: setToken.address, - paymentToken: ETH_ADDRESS, amountSetToken: setTokenAmount, - paymentTokenLimit: maxAmountIn, componentSwapData: componentSwapDataIssue, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; @@ -362,19 +358,22 @@ if (process.env.INTEGRATIONTEST) { function subject() { const redeemParams: IssueRedeemParams = { setToken: setToken.address, - paymentToken: outputToken, amountSetToken: setTokenAmount, - paymentTokenLimit: minAmountOut, componentSwapData: componentSwapDataRedeem, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; - if (redeemParams.paymentToken === ETH_ADDRESS) { - return flashMintDex.redeemExactSetForETH(redeemParams); + const paymentInfo: PaymentInfo = { + token: outputToken, + limitAmt: minAmountOut, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + if (paymentInfo.token === ETH_ADDRESS) { + return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); } else { - return flashMintDex.redeemExactSetForToken(redeemParams); + + return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); } } From c646fca87100e56fec120c69b81be435cdd4ff70 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 2 Aug 2024 16:30:47 -0400 Subject: [PATCH 26/61] cleanup console logs --- .../integration/ethereum/flashMintDex.spec.ts | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 7df02dd8..b70af8ef 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -61,7 +61,6 @@ if (process.env.INTEGRATIONTEST) { let deployer: DeployHelper; let setTokenCreator: SetTokenCreator; - // let basicIssuanceModule: IBasicIssuanceModule; let debtIssuanceModule: IDebtIssuanceModule; setBlockNumber(20385208, true); @@ -73,10 +72,7 @@ if (process.env.INTEGRATIONTEST) { addresses.setFork.setTokenCreator, owner.wallet, ); - // basicIssuanceModule = IBasicIssuanceModule__factory.connect( - // addresses.set.basicIssuanceModule, - // owner.wallet, - // ); + debtIssuanceModule = IDebtIssuanceModule__factory.connect( addresses.setFork.debtIssuanceModuleV2, owner.wallet, @@ -254,8 +250,6 @@ if (process.env.INTEGRATIONTEST) { let inputToken: Address; function subject() { - console.log("inputToken", inputToken); - console.log("maxAmountIn", maxAmountIn.toString()); const issueParams: IssueRedeemParams = { setToken: setToken.address, amountSetToken: setTokenAmount, @@ -287,10 +281,8 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - console.log("ethBalanceBefore", ethBalanceBefore.toString()); await subject(); const ethBalanceAfter = await owner.wallet.getBalance(); - console.log("ethBalanceAfter", ethBalanceAfter.toString()); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxAmountIn)); @@ -305,9 +297,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); - const tx = await subject(); - const receipt = await tx.wait(); - console.log(`Gas used for issuance: ${receipt.gasUsed.toString()}`); + await subject(); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -324,9 +314,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - const tx = await subject(); - const receipt = await tx.wait(); - console.log(`Gas used for issuance: ${receipt.gasUsed.toString()}`); + await subject(); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -394,12 +382,10 @@ if (process.env.INTEGRATIONTEST) { minAmountOut = ether(5); const wethToken = IWETH__factory.connect(outputToken, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); - console.log("WETH Balance Before", outputTokenBalanceBefore.toString()); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); - console.log("WETH Balance After", outputTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -409,12 +395,10 @@ if (process.env.INTEGRATIONTEST) { minAmountOut = usdc(26000); const usdcToken = IERC20__factory.connect(outputToken, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - console.log("USDC Balance Before", outputTokenBalanceBefore.toString()); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); - console.log("USDC Balance After", outputTokenBalanceAfter.toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); From fb38aebfa75e33f87c8a33a8a2146a35adbd07c5 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 5 Aug 2024 15:49:37 -0400 Subject: [PATCH 27/61] issue legacy set token with eth --- contracts/interfaces/IBasicIssuanceModule.sol | 1 + test/integration/ethereum/addresses.ts | 3 + .../integration/ethereum/flashMintDex.spec.ts | 142 +++++++++++++++++- 3 files changed, 145 insertions(+), 1 deletion(-) 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 fb603f04..e1cd525c 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -42,6 +42,7 @@ export const PRODUCTION_ADDRESSES = { sfrxEth: "0xac3E018457B222d93114458476f3E3416Abbe38F", osEth: "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", comp: "0xc00e94Cb662C3520282E6f5717214004A7f26888", + dpi: "0x1494CA1F11D487c2bBe4543E90080AeBa4BA3C2b", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", @@ -86,10 +87,12 @@ export const PRODUCTION_ADDRESSES = { }, 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 index b70af8ef..d5273e08 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -16,6 +16,8 @@ import { FlashMintDex, IERC20__factory, IWETH__factory, + IBasicIssuanceModule, + IBasicIssuanceModule__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; import { ADDRESS_ZERO, ETH_ADDRESS } from "@utils/constants"; @@ -54,13 +56,29 @@ type PaymentInfo = { swapDataWethToToken: SwapData; }; +const addresses = PRODUCTION_ADDRESSES; + +const swapDataFromInputToken = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.USDC, addresses.tokens.weth], + pool: ADDRESS_ZERO, +}; + +const swapDataToInputToken = { + 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 () => { - const addresses = PRODUCTION_ADDRESSES; let owner: Account; let deployer: DeployHelper; + let legacySetTokenCreator: SetTokenCreator; let setTokenCreator: SetTokenCreator; + let legacyBasicIssuanceModule: IBasicIssuanceModule; let debtIssuanceModule: IDebtIssuanceModule; setBlockNumber(20385208, true); @@ -68,11 +86,21 @@ if (process.env.INTEGRATIONTEST) { 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, @@ -81,6 +109,8 @@ if (process.env.INTEGRATIONTEST) { context("When FlashMintDex contract is deployed", () => { let flashMintDex: FlashMintDex; + + before(async () => { flashMintDex = await deployer.extensions.deployFlashMintDex( addresses.tokens.weth, @@ -129,6 +159,116 @@ if (process.env.INTEGRATIONTEST) { ); }); + context("when BED SetToken is deployed on Set Protocol", () => { + let setToken: SetToken; + 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"; + + 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); + }); + + it("setToken is deployed correctly", async () => { + expect(await setToken.symbol()).to.eq(tokenSymbol); + }); + + 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() + // })); + const setTokenAmount = ether(10); + let maxAmountIn: BigNumber; + let inputToken: Address; + + function subject() { + const issueParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, + }; + const paymentInfo: PaymentInfo = { + token: inputToken, + limitAmt: maxAmountIn, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + if (paymentInfo.token === ETH_ADDRESS) { + return flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + } else { + return flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); + } + } + + it("Can issue legacy set token from ETH", async () => { + inputToken = ETH_ADDRESS; + maxAmountIn = ether(11); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const ethBalanceBefore = await owner.wallet.getBalance(); + await subject(); + const ethBalanceAfter = await owner.wallet.getBalance(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxAmountIn)); + }); + }); + context("when setToken with a simple composition is deployed", () => { let setToken: SetToken; const components = [ From 65d5ab37743b18edc11d17c8a67dfdd047aac8a2 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 6 Aug 2024 10:37:56 -0400 Subject: [PATCH 28/61] erc20 tests for legacy set token --- .../integration/ethereum/flashMintDex.spec.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index d5273e08..b616d1fa 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -225,7 +225,7 @@ if (process.env.INTEGRATIONTEST) { // ...item, // path: [...item.path].reverse() // })); - const setTokenAmount = ether(10); + const setTokenAmount = ether(100); let maxAmountIn: BigNumber; let inputToken: Address; @@ -257,7 +257,7 @@ if (process.env.INTEGRATIONTEST) { it("Can issue legacy set token from ETH", async () => { inputToken = ETH_ADDRESS; - maxAmountIn = ether(11); + maxAmountIn = ether(5); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); @@ -267,6 +267,39 @@ if (process.env.INTEGRATIONTEST) { expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxAmountIn)); }); + + it("Can issue legacy set token from WETH", async () => { + inputToken = addresses.tokens.weth; + maxAmountIn = ether(5); + const wethToken = IWETH__factory.connect(inputToken, owner.wallet); + await wethToken.deposit({ value: maxAmountIn }); + wethToken.approve(flashMintDex.address, maxAmountIn); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + await subject(); + const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + }); + + it("Can issue set token from USDC", async () => { + inputToken = addresses.tokens.USDC; + maxAmountIn = usdc(20000); + const usdcToken = IERC20__factory.connect(inputToken, owner.wallet); + const whaleSigner = await impersonateAccount(addresses.whales.USDC); + await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); + usdcToken.approve(flashMintDex.address, maxAmountIn); + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + await subject(); + 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(maxAmountIn)); + }); }); context("when setToken with a simple composition is deployed", () => { From 79b5fe77f79077979588f2cdcb160393ad695bbc Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 6 Aug 2024 11:01:57 -0400 Subject: [PATCH 29/61] tests for redeem legacy set tokens --- .../integration/ethereum/flashMintDex.spec.ts | 91 ++++++++++++++++++- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index b616d1fa..999d7e9e 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -221,10 +221,10 @@ if (process.env.INTEGRATIONTEST) { pool: ADDRESS_ZERO, }, ]; - // const componentSwapDataRedeem = componentSwapDataIssue.map(item => ({ - // ...item, - // path: [...item.path].reverse() - // })); + const componentSwapDataRedeem = componentSwapDataIssue.map(item => ({ + ...item, + path: [...item.path].reverse(), + })); const setTokenAmount = ether(100); let maxAmountIn: BigNumber; let inputToken: Address; @@ -300,6 +300,89 @@ if (process.env.INTEGRATIONTEST) { expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); }); + + describe("When legacy set token has been issued", () => { + let outputToken: Address; + let minAmountOut: BigNumber; + + beforeEach(async () => { + const maxAmountIn = ether(5); + const issueParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, + }; + await flashMintDex.issueExactSetFromETH( + issueParams, + { + value: maxAmountIn, + }, + ); + await setToken.approve(flashMintDex.address, setTokenAmount); + }); + + function subject() { + const redeemParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataRedeem, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, + }; + const paymentInfo: PaymentInfo = { + token: outputToken, + limitAmt: minAmountOut, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + if (paymentInfo.token === ETH_ADDRESS) { + return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); + } else { + + return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); + } + } + + it("Can redeem set token for ETH", async () => { + outputToken = ETH_ADDRESS; + minAmountOut = maxAmountIn.mul(10).div(9); + const outputTokenBalanceBefore = await owner.wallet.getBalance(); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + 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 () => { + outputToken = addresses.tokens.weth; + minAmountOut = maxAmountIn.mul(10).div(9); + const wethToken = IWETH__factory.connect(outputToken, owner.wallet); + const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + 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(minAmountOut)); + }); + + it("Can redeem set token for USDC", async () => { + outputToken = addresses.tokens.USDC; + minAmountOut = usdc(5000); + const usdcToken = IERC20__factory.connect(outputToken, owner.wallet); + const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + 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(minAmountOut)); + }); + }); }); context("when setToken with a simple composition is deployed", () => { From a307e11f309f0bc950fac4ce8d8a345f08e05642 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 6 Aug 2024 11:47:00 -0400 Subject: [PATCH 30/61] add getIssueExactSet functions return input token amounts --- contracts/exchangeIssuance/FlashMintDex.sol | 83 +++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index c179e585..c93338e0 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -181,6 +181,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * The excess amount of tokens is returned in an equivalent amount of ether. * * @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 * * @return excessPaymentTokenAmt Amount of input token returned to the caller */ @@ -512,4 +513,86 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } } } + + /** + * Gets the input cost of issuing a given amount of a 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 + * + * @return totalEthNeeded Amount of input tokens required to perfrom the issuance + */ + function getIssueExactSetFromEth( + IssueRedeemParams memory _issueParams + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); + uint256 totalEthNeeded = 0; + for (uint256 i = 0; i < components.length; i++) { + address component = components[i]; + uint256 units = componentUnits[i]; + + // If the component is equal to WETH we don't have to trade + if (component == address(WETH)) { + totalEthNeeded = totalEthNeeded.add(units); + } else { + totalEthNeeded += DEXAdapterV2.getAmountIn( + dexAdapter, + _issueParams.componentSwapData[i], + units + ); + } + } + return totalEthNeeded; + } + + /** + * Gets the input cost of issuing a given amount of a 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 _paymentInfo Struct containing input token address, max amount to spend, and swap data to trade for WETH + * + * @return the amount of input tokens required to perfrom the issuance + */ + function getIssueExactSetFromToken( + IssueRedeemParams memory _issueParams, + PaymentInfo memory _paymentInfo + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); + uint256 totalWethNeeded = 0; + for (uint256 i = 0; i < components.length; i++) { + address component = components[i]; + uint256 units = componentUnits[i]; + + // If the component is equal to WETH we don't have to trade + if (component == address(WETH)) { + totalWethNeeded = totalWethNeeded.add(units); + } else { + totalWethNeeded += DEXAdapterV2.getAmountIn( + dexAdapter, + _issueParams.componentSwapData[i], + units + ); + } + } + return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethNeeded); + } } From 59ee2a2585268aad7657397d5b13106ad8195f2a Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 7 Aug 2024 14:51:07 -0400 Subject: [PATCH 31/61] tests for getting eth required --- .../integration/ethereum/flashMintDex.spec.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 999d7e9e..24f42587 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -159,7 +159,7 @@ if (process.env.INTEGRATIONTEST) { ); }); - context("when BED SetToken is deployed on Set Protocol", () => { + context("when SetToken is deployed on legacy Set Protocol", () => { let setToken: SetToken; const components = [ addresses.tokens.wbtc, @@ -255,6 +255,19 @@ if (process.env.INTEGRATIONTEST) { } } + it("Can return ETH quantity required to issue legacy set token", async () => { + const issueParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + console.log(ethRequired); + expect(ethRequired).to.eq(BigNumber.from("3498514628413285230")); + }); + it("Can issue legacy set token from ETH", async () => { inputToken = ETH_ADDRESS; maxAmountIn = ether(5); @@ -531,6 +544,18 @@ if (process.env.INTEGRATIONTEST) { } } + it("Can return ETH quantity required to issue set token", async () => { + const issueParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + expect(ethRequired).to.eq(BigNumber.from("8427007884995480469")); + }); + it("Can issue set token from ETH", async () => { inputToken = ETH_ADDRESS; maxAmountIn = ether(11); From b1bc29765900f24af6619c7f6f37d5a6b3eb6e46 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 7 Aug 2024 15:56:40 -0400 Subject: [PATCH 32/61] refactor legacy token tests --- .../integration/ethereum/flashMintDex.spec.ts | 207 ++++++++++-------- 1 file changed, 121 insertions(+), 86 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 24f42587..41dff541 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -58,6 +58,13 @@ type PaymentInfo = { const addresses = PRODUCTION_ADDRESSES; +const swapDataEmpty = { + exchange: Exchange.None, + fees: [], + path: [], + pool: ADDRESS_ZERO, +}; + const swapDataFromInputToken = { exchange: Exchange.UniV3, fees: [500], @@ -71,6 +78,7 @@ const swapDataToInputToken = { path: [addresses.tokens.weth, addresses.tokens.USDC], pool: ADDRESS_ZERO, }; + if (process.env.INTEGRATIONTEST) { describe.only("FlashMintDex - Integration Test", async () => { let owner: Account; @@ -161,6 +169,8 @@ if (process.env.INTEGRATIONTEST) { context("when SetToken is deployed on legacy Set Protocol", () => { let setToken: SetToken; + let issueParams: IssueRedeemParams; + const components = [ addresses.tokens.wbtc, addresses.tokens.weth, @@ -175,31 +185,7 @@ if (process.env.INTEGRATIONTEST) { const modules = [addresses.set.basicIssuanceModule]; const tokenName = "BED Index"; const tokenSymbol = "BED"; - - 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); - }); - - it("setToken is deployed correctly", async () => { - expect(await setToken.symbol()).to.eq(tokenSymbol); - }); + const setTokensToIssue = ether(100); const componentSwapDataIssue = [ { @@ -221,97 +207,128 @@ if (process.env.INTEGRATIONTEST) { pool: ADDRESS_ZERO, }, ]; + const componentSwapDataRedeem = componentSwapDataIssue.map(item => ({ ...item, path: [...item.path].reverse(), })); - const setTokenAmount = ether(100); - let maxAmountIn: BigNumber; - let inputToken: Address; - function subject() { - const issueParams: IssueRedeemParams = { + 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, + amountSetToken: setTokensToIssue, componentSwapData: componentSwapDataIssue, issuanceModule: legacyBasicIssuanceModule.address, isDebtIssuance: false, }; - const paymentInfo: PaymentInfo = { - token: inputToken, - limitAmt: maxAmountIn, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - if (paymentInfo.token === ETH_ADDRESS) { - return flashMintDex.issueExactSetFromETH( - issueParams, - { - value: maxAmountIn, - }, - ); - } else { - return flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); - } - } + }); + + 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 issueParams: IssueRedeemParams = { setToken: setToken.address, - amountSetToken: setTokenAmount, + amountSetToken: setTokensToIssue, componentSwapData: componentSwapDataIssue, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, + issuanceModule: legacyBasicIssuanceModule.address, + isDebtIssuance: false, }; const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); console.log(ethRequired); expect(ethRequired).to.eq(BigNumber.from("3498514628413285230")); }); + it("Can return USDC quantity required to issue legacy set token", async () => { + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + console.log(usdcRequired); + expect(usdcRequired).to.eq(BigNumber.from("11063559168")); + }); + it("Can issue legacy set token from ETH", async () => { - inputToken = ETH_ADDRESS; - maxAmountIn = ether(5); + const ethEstimate = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + const ethAmountIn = ethEstimate.mul(1005).div(1000); // 0.5% slippage const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - await subject(); + await flashMintDex.issueExactSetFromETH(issueParams, { value: ethAmountIn }); const ethBalanceAfter = await owner.wallet.getBalance(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxAmountIn)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(ethBalanceAfter).to.gte(ethBalanceBefore.sub(ethAmountIn)); }); it("Can issue legacy set token from WETH", async () => { - inputToken = addresses.tokens.weth; - maxAmountIn = ether(5); - const wethToken = IWETH__factory.connect(inputToken, owner.wallet); - await wethToken.deposit({ value: maxAmountIn }); - wethToken.approve(flashMintDex.address, maxAmountIn); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = wethEstimate.mul(1005).div(1000); // 0.5% slippage + + 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 subject(); + await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); it("Can issue set token from USDC", async () => { - inputToken = addresses.tokens.USDC; - maxAmountIn = usdc(20000); - const usdcToken = IERC20__factory.connect(inputToken, owner.wallet); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage + + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); - await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); - usdcToken.approve(flashMintDex.address, maxAmountIn); + 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 subject(); + await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); 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(maxAmountIn)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); describe("When legacy set token has been issued", () => { @@ -319,27 +336,28 @@ if (process.env.INTEGRATIONTEST) { let minAmountOut: BigNumber; beforeEach(async () => { - const maxAmountIn = ether(5); - const issueParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataIssue, - issuanceModule: legacyBasicIssuanceModule.address, - isDebtIssuance: false, - }; + // const maxAmountIn = ether(5); + // const issueParams: IssueRedeemParams = { + // setToken: setToken.address, + // amountSetToken: setTokensToIssue, + // componentSwapData: componentSwapDataIssue, + // issuanceModule: legacyBasicIssuanceModule.address, + // isDebtIssuance: false, + // }; + const ethAmountIn = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); await flashMintDex.issueExactSetFromETH( issueParams, { - value: maxAmountIn, + value: ethAmountIn, }, ); - await setToken.approve(flashMintDex.address, setTokenAmount); + await setToken.approve(flashMintDex.address, setTokensToIssue); }); function subject() { const redeemParams: IssueRedeemParams = { setToken: setToken.address, - amountSetToken: setTokenAmount, + amountSetToken: setTokensToIssue, componentSwapData: componentSwapDataRedeem, issuanceModule: legacyBasicIssuanceModule.address, isDebtIssuance: false, @@ -366,7 +384,7 @@ if (process.env.INTEGRATIONTEST) { await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await owner.wallet.getBalance(); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -379,7 +397,7 @@ if (process.env.INTEGRATIONTEST) { await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -392,7 +410,7 @@ if (process.env.INTEGRATIONTEST) { await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); }); @@ -556,6 +574,23 @@ if (process.env.INTEGRATIONTEST) { expect(ethRequired).to.eq(BigNumber.from("8427007884995480469")); }); + it("Can return USDC quantity required to issue set token", async () => { + const issueParams: IssueRedeemParams = { + setToken: setToken.address, + amountSetToken: setTokenAmount, + componentSwapData: componentSwapDataIssue, + issuanceModule: debtIssuanceModule.address, + isDebtIssuance: true, + }; + const paymentInfo: PaymentInfo = { + token: inputToken, + limitAmt: maxAmountIn, + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + expect(usdcRequired).to.eq(BigNumber.from("8427007884995480469")); + }); it("Can issue set token from ETH", async () => { inputToken = ETH_ADDRESS; maxAmountIn = ether(11); From f6d08452bbd0f47d7743d021d2fff4d8f83c1e6f Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 7 Aug 2024 16:33:26 -0400 Subject: [PATCH 33/61] getRedeemExactSetForEth --- contracts/exchangeIssuance/FlashMintDex.sol | 56 ++++++++--- .../integration/ethereum/flashMintDex.spec.ts | 98 ++++++++++--------- 2 files changed, 96 insertions(+), 58 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index c93338e0..1835cb01 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -537,17 +537,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); uint256 totalEthNeeded = 0; for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - uint256 units = componentUnits[i]; - // If the component is equal to WETH we don't have to trade - if (component == address(WETH)) { - totalEthNeeded = totalEthNeeded.add(units); + if (components[i] == address(WETH)) { + totalEthNeeded = totalEthNeeded.add(componentUnits[i]); } else { totalEthNeeded += DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], - units + componentUnits[i] ); } } @@ -579,20 +576,55 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); uint256 totalWethNeeded = 0; for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - uint256 units = componentUnits[i]; - // If the component is equal to WETH we don't have to trade - if (component == address(WETH)) { - totalWethNeeded = totalWethNeeded.add(units); + if (components[i] == address(WETH)) { + totalWethNeeded = totalWethNeeded.add(componentUnits[i]); } else { totalWethNeeded += DEXAdapterV2.getAmountIn( dexAdapter, _issueParams.componentSwapData[i], - units + componentUnits[i] ); } } return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethNeeded); } + + /** + * Gets the amount of ETH expected after redeeming a given amount of a 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 + * + * @return totalEthReceived Estimated amount of ETH received from redemption + */ + function getRedeemExactSetForEth( + IssueRedeemParams memory _redeemParams + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + uint256 totalEthReceived = 0; + for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalEthReceived = totalEthReceived.add(componentUnits[i]); + } else { + totalEthReceived += DEXAdapterV2.getAmountOut( + dexAdapter, + _redeemParams.componentSwapData[i], + componentUnits[i] + ); + } + } + return totalEthReceived; + } } diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 41dff541..cfc63305 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -169,7 +169,9 @@ if (process.env.INTEGRATIONTEST) { context("when SetToken is deployed on legacy Set Protocol", () => { let setToken: SetToken; + let setTokenAmount: BigNumber; let issueParams: IssueRedeemParams; + let redeemParams: IssueRedeemParams; const components = [ addresses.tokens.wbtc, @@ -185,7 +187,7 @@ if (process.env.INTEGRATIONTEST) { const modules = [addresses.set.basicIssuanceModule]; const tokenName = "BED Index"; const tokenSymbol = "BED"; - const setTokensToIssue = ether(100); + const setTokenAmount = ether(100); const componentSwapDataIssue = [ { @@ -235,11 +237,19 @@ if (process.env.INTEGRATIONTEST) { issueParams = { setToken: setToken.address, - amountSetToken: setTokensToIssue, + 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 () => { @@ -249,7 +259,7 @@ if (process.env.INTEGRATIONTEST) { it("Can return ETH quantity required to issue legacy set token", async () => { const issueParams: IssueRedeemParams = { setToken: setToken.address, - amountSetToken: setTokensToIssue, + amountSetToken: setTokenAmount, componentSwapData: componentSwapDataIssue, issuanceModule: legacyBasicIssuanceModule.address, isDebtIssuance: false, @@ -280,7 +290,7 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.issueExactSetFromETH(issueParams, { value: ethAmountIn }); const ethBalanceAfter = await owner.wallet.getBalance(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(ethBalanceAfter).to.gte(ethBalanceBefore.sub(ethAmountIn)); }); @@ -303,7 +313,7 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); @@ -327,64 +337,60 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); describe("When legacy set token has been issued", () => { let outputToken: Address; + let inputTokenAmount: BigNumber; let minAmountOut: BigNumber; beforeEach(async () => { - // const maxAmountIn = ether(5); - // const issueParams: IssueRedeemParams = { - // setToken: setToken.address, - // amountSetToken: setTokensToIssue, - // componentSwapData: componentSwapDataIssue, - // issuanceModule: legacyBasicIssuanceModule.address, - // isDebtIssuance: false, - // }; - const ethAmountIn = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + inputTokenAmount = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); await flashMintDex.issueExactSetFromETH( issueParams, { - value: ethAmountIn, + value: inputTokenAmount, }, ); - await setToken.approve(flashMintDex.address, setTokensToIssue); + await setToken.approve(flashMintDex.address, setTokenAmount); }); - function subject() { - const redeemParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokensToIssue, - componentSwapData: componentSwapDataRedeem, - issuanceModule: legacyBasicIssuanceModule.address, - isDebtIssuance: false, - }; - const paymentInfo: PaymentInfo = { - token: outputToken, - limitAmt: minAmountOut, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - if (paymentInfo.token === ETH_ADDRESS) { - return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); - } else { - - return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); - } - } - - it("Can redeem set token for ETH", async () => { - outputToken = ETH_ADDRESS; - minAmountOut = maxAmountIn.mul(10).div(9); + // function subject() { + // const redeemParams: IssueRedeemParams = { + // setToken: setToken.address, + // amountSetToken: setTokenAmount, + // componentSwapData: componentSwapDataRedeem, + // issuanceModule: legacyBasicIssuanceModule.address, + // isDebtIssuance: false, + // }; + // const paymentInfo: PaymentInfo = { + // token: outputToken, + // limitAmt: minAmountOut, + // swapDataTokenToWeth: swapDataFromInputToken, + // swapDataWethToToken: swapDataToInputToken, + // }; + // if (paymentInfo.token === ETH_ADDRESS) { + // return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); + // } else { + + // return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); + // } + // } + + it.only("Can redeem legacy set token for ETH", async () => { + // TODO - replace with getRedeemExactSetForEth + const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) + console.log("minAmountOut", minAmountOut.toString()); + console.log("inputTokenAmount", inputTokenAmount.toString()); const outputTokenBalanceBefore = await owner.wallet.getBalance(); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + await flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await owner.wallet.getBalance(); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -397,7 +403,7 @@ if (process.env.INTEGRATIONTEST) { await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -410,7 +416,7 @@ if (process.env.INTEGRATIONTEST) { await subject(); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); - expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokensToIssue)); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(minAmountOut)); }); }); From 55b58c7bd5b0dc0d355a681555ef982250322972 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 10:05:05 -0400 Subject: [PATCH 34/61] use getRedeemExactSet for legacy token test --- contracts/exchangeIssuance/FlashMintDex.sol | 6 -- .../integration/ethereum/flashMintDex.spec.ts | 74 ++++++++----------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 1835cb01..fd37e3c9 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -214,8 +214,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * The excess amount of tokens is returned in an equivalent amount of ether. * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance - * - * @return amountEthReturn Amount of ether returned to the caller */ function issueExactSetFromETH(IssueRedeemParams memory _issueParams) external @@ -239,7 +237,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), totalEthSold, _issueParams.amountSetToken); - return amountEthReturn; } /** @@ -247,8 +244,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * The SetToken must be approved by the sender to this contract. * * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance - * - * @return outputAmount Amount of output tokens sent to the caller */ function redeemExactSetForToken(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) external @@ -265,7 +260,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _paymentInfo.token.safeTransfer(msg.sender, outputAmount); emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputAmount); - return outputAmount; } /** diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index cfc63305..3e01db3a 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -169,9 +169,9 @@ if (process.env.INTEGRATIONTEST) { context("when SetToken is deployed on legacy Set Protocol", () => { let setToken: SetToken; - let setTokenAmount: BigNumber; let issueParams: IssueRedeemParams; let redeemParams: IssueRedeemParams; + const setTokenAmount = ether(100); const components = [ addresses.tokens.wbtc, @@ -187,7 +187,6 @@ if (process.env.INTEGRATIONTEST) { const modules = [addresses.set.basicIssuanceModule]; const tokenName = "BED Index"; const tokenSymbol = "BED"; - const setTokenAmount = ether(100); const componentSwapDataIssue = [ { @@ -342,7 +341,6 @@ if (process.env.INTEGRATIONTEST) { }); describe("When legacy set token has been issued", () => { - let outputToken: Address; let inputTokenAmount: BigNumber; let minAmountOut: BigNumber; @@ -357,33 +355,11 @@ if (process.env.INTEGRATIONTEST) { await setToken.approve(flashMintDex.address, setTokenAmount); }); - // function subject() { - // const redeemParams: IssueRedeemParams = { - // setToken: setToken.address, - // amountSetToken: setTokenAmount, - // componentSwapData: componentSwapDataRedeem, - // issuanceModule: legacyBasicIssuanceModule.address, - // isDebtIssuance: false, - // }; - // const paymentInfo: PaymentInfo = { - // token: outputToken, - // limitAmt: minAmountOut, - // swapDataTokenToWeth: swapDataFromInputToken, - // swapDataWethToToken: swapDataToInputToken, - // }; - // if (paymentInfo.token === ETH_ADDRESS) { - // return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); - // } else { - - // return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); - // } - // } - - it.only("Can redeem legacy set token for ETH", async () => { - // TODO - replace with getRedeemExactSetForEth + it("Can redeem legacy set token for ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + console.log("ethEstimate", ethEstimate.toString()); minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) - console.log("minAmountOut", minAmountOut.toString()); + console.log("setToken amount", setTokenAmount.toString()); console.log("inputTokenAmount", inputTokenAmount.toString()); const outputTokenBalanceBefore = await owner.wallet.getBalance(); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -391,33 +367,45 @@ if (process.env.INTEGRATIONTEST) { 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)); + expect(outputTokenBalanceAfter).to.gte(outputTokenBalanceBefore.add(minAmountOut)); }); - it("Can redeem set token for WETH", async () => { - outputToken = addresses.tokens.weth; - minAmountOut = maxAmountIn.mul(10).div(9); - const wethToken = IWETH__factory.connect(outputToken, owner.wallet); + it("Can redeem legacy set token for WETH", async () => { + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = wethEstimate.mul(50).div(100); // 50% slippage (why?) + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + await flashMintDex.redeemExactSetForToken(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(minAmountOut)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); it("Can redeem set token for USDC", async () => { - outputToken = addresses.tokens.USDC; - minAmountOut = usdc(5000); - const usdcToken = IERC20__factory.connect(outputToken, owner.wallet); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = usdcEstimate.mul(50).div(100); // 50% slippage (why?) + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + await flashMintDex.redeemExactSetForToken(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(minAmountOut)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); }); }); @@ -589,13 +577,13 @@ if (process.env.INTEGRATIONTEST) { isDebtIssuance: true, }; const paymentInfo: PaymentInfo = { - token: inputToken, - limitAmt: maxAmountIn, + token: addresses.tokens.USDC, + limitAmt: ether(0), swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - expect(usdcRequired).to.eq(BigNumber.from("8427007884995480469")); + expect(usdcRequired).to.eq(BigNumber.from("26648572145")); }); it("Can issue set token from ETH", async () => { inputToken = ETH_ADDRESS; From ad2e46679c7768ee519af6e9e706774b52c9f204 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 10:34:45 -0400 Subject: [PATCH 35/61] test clean up --- .../integration/ethereum/flashMintDex.spec.ts | 214 ++++++++---------- 1 file changed, 100 insertions(+), 114 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 3e01db3a..c4736522 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -20,8 +20,8 @@ import { IBasicIssuanceModule__factory, } from "../../../typechain"; import { PRODUCTION_ADDRESSES } from "./addresses"; -import { ADDRESS_ZERO, ETH_ADDRESS } from "@utils/constants"; -import { ether, usdc } from "@utils/index"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { ether } from "@utils/index"; import { impersonateAccount } from "./utils"; const expect = getWaffleExpect(); @@ -282,15 +282,15 @@ if (process.env.INTEGRATIONTEST) { it("Can issue legacy set token from ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); - const ethAmountIn = ethEstimate.mul(1005).div(1000); // 0.5% slippage + 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, { value: ethAmountIn }); + await flashMintDex.issueExactSetFromETH(issueParams, { 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(ethAmountIn)); + expect(ethBalanceAfter).to.gte(ethBalanceBefore.sub(maxEthIn)); }); it("Can issue legacy set token from WETH", async () => { @@ -341,15 +341,13 @@ if (process.env.INTEGRATIONTEST) { }); describe("When legacy set token has been issued", () => { - let inputTokenAmount: BigNumber; let minAmountOut: BigNumber; beforeEach(async () => { - inputTokenAmount = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); await flashMintDex.issueExactSetFromETH( issueParams, { - value: inputTokenAmount, + value: await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams), }, ); await setToken.approve(flashMintDex.address, setTokenAmount); @@ -357,10 +355,7 @@ if (process.env.INTEGRATIONTEST) { it("Can redeem legacy set token for ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); - console.log("ethEstimate", ethEstimate.toString()); minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) - console.log("setToken amount", setTokenAmount.toString()); - console.log("inputTokenAmount", inputTokenAmount.toString()); const outputTokenBalanceBefore = await owner.wallet.getBalance(); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); await flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); @@ -412,6 +407,10 @@ if (process.env.INTEGRATIONTEST) { context("when setToken with a simple composition is deployed", () => { let setToken: SetToken; + let issueParams: IssueRedeemParams; + let redeemParams: IssueRedeemParams; + const setTokenAmount = ether(10); + const components = [ addresses.tokens.wstEth, addresses.tokens.rETH, @@ -520,62 +519,34 @@ if (process.env.INTEGRATIONTEST) { ADDRESS_ZERO, ); await flashMintDex.approveSetToken(setToken.address, debtIssuanceModule.address); - }); - it("setToken is deployed correctly", async () => { - expect(await setToken.symbol()).to.eq(tokenSymbol); - }); - - const setTokenAmount = ether(10); - let maxAmountIn: BigNumber; - let inputToken: Address; - - function subject() { - const issueParams: IssueRedeemParams = { + issueParams = { setToken: setToken.address, amountSetToken: setTokenAmount, componentSwapData: componentSwapDataIssue, issuanceModule: debtIssuanceModule.address, isDebtIssuance: true, }; - const paymentInfo: PaymentInfo = { - token: inputToken, - limitAmt: maxAmountIn, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - if (paymentInfo.token === ETH_ADDRESS) { - return flashMintDex.issueExactSetFromETH( - issueParams, - { - value: maxAmountIn, - }, - ); - } else { - return flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); - } - } - it("Can return ETH quantity required to issue set token", async () => { - const issueParams: IssueRedeemParams = { + redeemParams = { setToken: setToken.address, amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataIssue, + componentSwapData: componentSwapDataRedeem, issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, + 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.getIssueExactSetFromEth(issueParams); expect(ethRequired).to.eq(BigNumber.from("8427007884995480469")); }); it("Can return USDC quantity required to issue set token", async () => { - const issueParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataIssue, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, - }; const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), @@ -585,102 +556,105 @@ if (process.env.INTEGRATIONTEST) { const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); expect(usdcRequired).to.eq(BigNumber.from("26648572145")); }); + it("Can issue set token from ETH", async () => { - inputToken = ETH_ADDRESS; - maxAmountIn = ether(11); + const ethEstimate = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + const maxEthIn = ethEstimate.mul(1005).div(1000); // 0.5% slippage const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - await subject(); + await flashMintDex.issueExactSetFromETH(issueParams, { 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.gt(ethBalanceBefore.sub(maxAmountIn)); + expect(ethBalanceAfter).to.gt(ethBalanceBefore.sub(maxEthIn)); }); it("Can issue set token from WETH", async () => { - inputToken = addresses.tokens.weth; - maxAmountIn = ether(11); - const wethToken = IWETH__factory.connect(inputToken, owner.wallet); - await wethToken.deposit({ value: maxAmountIn }); - wethToken.approve(flashMintDex.address, maxAmountIn); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = wethEstimate.mul(1005).div(1000); // 0.5% slippage + 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 subject(); + await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); - expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(maxAmountIn)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); it("Can issue set token from USDC", async () => { - inputToken = addresses.tokens.USDC; - maxAmountIn = usdc(27000); - const usdcToken = IERC20__factory.connect(inputToken, owner.wallet); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage + + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); - await usdcToken.connect(whaleSigner).transfer(owner.address, maxAmountIn); - usdcToken.approve(flashMintDex.address, maxAmountIn); + 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 subject(); + await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); 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(maxAmountIn)); + expect(inputTokenBalanceAfter).to.gte(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); }); describe("When set token has been issued", () => { - let outputToken: Address; - let minAmountOut: BigNumber; - beforeEach(async () => { - const maxAmountIn = ether(11); - const issueParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataIssue, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, - }; await flashMintDex.issueExactSetFromETH( issueParams, { - value: maxAmountIn, + value: await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams), }, ); await setToken.approve(flashMintDex.address, setTokenAmount); }); - function subject() { - const redeemParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataRedeem, - issuanceModule: debtIssuanceModule.address, - isDebtIssuance: true, - }; - const paymentInfo: PaymentInfo = { - token: outputToken, - limitAmt: minAmountOut, - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - if (paymentInfo.token === ETH_ADDRESS) { - return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); - } else { - - return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); - } - } + // function subject() { + // const redeemParams: IssueRedeemParams = { + // setToken: setToken.address, + // amountSetToken: setTokenAmount, + // componentSwapData: componentSwapDataRedeem, + // issuanceModule: debtIssuanceModule.address, + // isDebtIssuance: true, + // }; + // const paymentInfo: PaymentInfo = { + // token: outputToken, + // limitAmt: minAmountOut, + // swapDataTokenToWeth: swapDataFromInputToken, + // swapDataWethToToken: swapDataToInputToken, + // }; + // if (paymentInfo.token === ETH_ADDRESS) { + // return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); + // } else { + + // return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); + // } + // } it("Can redeem set token for ETH", async () => { - outputToken = ETH_ADDRESS; - minAmountOut = ether(5); + const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + const minAmountOut = ethEstimate.mul(995).div(1000); // 0.5% slippage const outputTokenBalanceBefore = await owner.wallet.getBalance(); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + 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)); @@ -688,29 +662,41 @@ if (process.env.INTEGRATIONTEST) { }); it("Can redeem set token for WETH", async () => { - outputToken = addresses.tokens.weth; - minAmountOut = ether(5); - const wethToken = IWETH__factory.connect(outputToken, owner.wallet); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.weth, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataEmpty, + swapDataWethToToken: swapDataEmpty, + }; + const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = wethEstimate.mul(99).div(100); // 50% slippage (why?) + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + await flashMintDex.redeemExactSetForToken(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(minAmountOut)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); it("Can redeem set token for USDC", async () => { - outputToken = addresses.tokens.USDC; - minAmountOut = usdc(26000); - const usdcToken = IERC20__factory.connect(outputToken, owner.wallet); + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + paymentInfo.limitAmt = usdcEstimate.mul(995).div(1000); // 50% slippage (why?) + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - await subject(); + await flashMintDex.redeemExactSetForToken(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(minAmountOut)); + expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); }); }); From b0d5d886675cfbb2d39ae0f716e7b7ed46918d29 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 11:10:40 -0400 Subject: [PATCH 36/61] getRedeemExactSetForToken wip --- contracts/exchangeIssuance/FlashMintDex.sol | 43 ++++++++++- .../integration/ethereum/flashMintDex.spec.ts | 73 ++++++++++--------- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index fd37e3c9..02fb8e4a 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -553,7 +553,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @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 * - * @return the amount of input tokens required to perfrom the issuance + * @return the amount of input tokens required to perform the issuance */ function getIssueExactSetFromToken( IssueRedeemParams memory _issueParams, @@ -584,6 +584,47 @@ contract FlashMintDex is Ownable, ReentrancyGuard { return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethNeeded); } + /** + * Gets the amount of specified payment token expected to be received after redeeming + * a given quantity of a 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 _paymentInfo Struct containing output token address, max amount to spend, and swap data to trade for WETH + * + * @return the amount of output tokens expected after performing redemption + */ + function getRedeemExactSetForToken( + IssueRedeemParams memory _redeemParams, + PaymentInfo memory _paymentInfo + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + uint256 totalWethReceived = 0; + for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalWethReceived = totalWethReceived.add(componentUnits[i]); + } else { + totalWethReceived += DEXAdapterV2.getAmountOut( + dexAdapter, + _redeemParams.componentSwapData[i], + componentUnits[i] + ); + } + } + return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethReceived); + } + /** * Gets the amount of ETH expected after redeeming a given amount of a set token with the provided redemption params. * This function is not marked view, but should be static called from frontends. diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index c4736522..31e35e7c 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -256,13 +256,6 @@ if (process.env.INTEGRATIONTEST) { }); it("Can return ETH quantity required to issue legacy set token", async () => { - const issueParams: IssueRedeemParams = { - setToken: setToken.address, - amountSetToken: setTokenAmount, - componentSwapData: componentSwapDataIssue, - issuanceModule: legacyBasicIssuanceModule.address, - isDebtIssuance: false, - }; const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); console.log(ethRequired); expect(ethRequired).to.eq(BigNumber.from("3498514628413285230")); @@ -353,6 +346,24 @@ if (process.env.INTEGRATIONTEST) { await setToken.approve(flashMintDex.address, setTokenAmount); }); + it("Can return ETH quantity received when redeeming legacy set token", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + console.log("ethReceived", ethReceivedEstimate); + expect(ethReceivedEstimate).to.eq(BigNumber.from("3492695444625661021")); + }); + + it("Can return USDC quantity received when redeeming legacy set token", async () => { + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(issueParams, paymentInfo); + console.log("usdcReceived", usdcReceived); + expect(usdcReceived).to.eq(BigNumber.from("2266952417466")); + }); + it("Can redeem legacy set token for ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) @@ -372,7 +383,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); paymentInfo.limitAmt = wethEstimate.mul(50).div(100); // 50% slippage (why?) const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); @@ -391,7 +402,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); paymentInfo.limitAmt = usdcEstimate.mul(50).div(100); // 50% slippage (why?) const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); @@ -554,7 +565,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataToInputToken, }; const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - expect(usdcRequired).to.eq(BigNumber.from("26648572145")); + expect(usdcRequired).to.eq(BigNumber.from("26648569515")); }); it("Can issue set token from ETH", async () => { @@ -627,27 +638,23 @@ if (process.env.INTEGRATIONTEST) { await setToken.approve(flashMintDex.address, setTokenAmount); }); - // function subject() { - // const redeemParams: IssueRedeemParams = { - // setToken: setToken.address, - // amountSetToken: setTokenAmount, - // componentSwapData: componentSwapDataRedeem, - // issuanceModule: debtIssuanceModule.address, - // isDebtIssuance: true, - // }; - // const paymentInfo: PaymentInfo = { - // token: outputToken, - // limitAmt: minAmountOut, - // swapDataTokenToWeth: swapDataFromInputToken, - // swapDataWethToToken: swapDataToInputToken, - // }; - // if (paymentInfo.token === ETH_ADDRESS) { - // return flashMintDex.redeemExactSetForETH(redeemParams, minAmountOut); - // } else { - - // return flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); - // } - // } + it("Can return ETH quantity received when redeeming set token", async () => { + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + console.log("ethReceived", ethReceivedEstimate); + expect(ethReceivedEstimate).to.eq(BigNumber.from("8423933102234975071")); + }); + + it("Can return USDC quantity received when redeeming legacy set token", async () => { + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataFromInputToken, + swapDataWethToToken: swapDataToInputToken, + }; + const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(issueParams, paymentInfo); + console.log("usdcReceived", usdcReceived); + expect(usdcReceived).to.eq(BigNumber.from("493778956261")); // Way off? + }); it("Can redeem set token for ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); @@ -668,7 +675,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); paymentInfo.limitAmt = wethEstimate.mul(99).div(100); // 50% slippage (why?) const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); @@ -687,7 +694,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); paymentInfo.limitAmt = usdcEstimate.mul(995).div(1000); // 50% slippage (why?) const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); From 71f661abba58743c165767a67af76049b9082cc8 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 14:14:03 -0400 Subject: [PATCH 37/61] fixes to redeem set token tests --- contracts/exchangeIssuance/FlashMintDex.sol | 2 -- .../integration/ethereum/flashMintDex.spec.ts | 26 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 02fb8e4a..df2e7d16 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -610,7 +610,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); uint256 totalWethReceived = 0; for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade if (components[i] == address(WETH)) { totalWethReceived = totalWethReceived.add(componentUnits[i]); @@ -648,7 +647,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { ); uint256 totalEthReceived = 0; for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade if (components[i] == address(WETH)) { totalEthReceived = totalEthReceived.add(componentUnits[i]); diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 31e35e7c..866165f8 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -257,7 +257,6 @@ if (process.env.INTEGRATIONTEST) { it("Can return ETH quantity required to issue legacy set token", async () => { const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); - console.log(ethRequired); expect(ethRequired).to.eq(BigNumber.from("3498514628413285230")); }); @@ -269,7 +268,6 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataToInputToken, }; const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - console.log(usdcRequired); expect(usdcRequired).to.eq(BigNumber.from("11063559168")); }); @@ -334,8 +332,6 @@ if (process.env.INTEGRATIONTEST) { }); describe("When legacy set token has been issued", () => { - let minAmountOut: BigNumber; - beforeEach(async () => { await flashMintDex.issueExactSetFromETH( issueParams, @@ -359,19 +355,21 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; - const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(issueParams, paymentInfo); + const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); console.log("usdcReceived", usdcReceived); - expect(usdcReceived).to.eq(BigNumber.from("2266952417466")); + expect(usdcReceived).to.eq(BigNumber.from("11054123420")); }); it("Can redeem legacy set token for ETH", async () => { const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); - minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) + console.log("ethEstimate", ethEstimate); + const minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) 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(); + console.log("eth received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gte(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -384,6 +382,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataEmpty, }; const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); + console.log("wethEstimate", wethEstimate); paymentInfo.limitAmt = wethEstimate.mul(50).div(100); // 50% slippage (why?) const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); @@ -391,6 +390,7 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); + console.log("weth received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); @@ -403,6 +403,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataToInputToken, }; const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); + console.log("usdcEstimate", usdcEstimate); paymentInfo.limitAmt = usdcEstimate.mul(50).div(100); // 50% slippage (why?) const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); @@ -410,6 +411,7 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); + console.log("usdc received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); @@ -644,16 +646,16 @@ if (process.env.INTEGRATIONTEST) { expect(ethReceivedEstimate).to.eq(BigNumber.from("8423933102234975071")); }); - it("Can return USDC quantity received when redeeming legacy set token", async () => { + it("Can return USDC quantity received when redeeming set token", async () => { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; - const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(issueParams, paymentInfo); + const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); console.log("usdcReceived", usdcReceived); - expect(usdcReceived).to.eq(BigNumber.from("493778956261")); // Way off? + expect(usdcReceived).to.eq(BigNumber.from("26643979253")); }); it("Can redeem set token for ETH", async () => { @@ -676,7 +678,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataEmpty, }; const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - paymentInfo.limitAmt = wethEstimate.mul(99).div(100); // 50% slippage (why?) + paymentInfo.limitAmt = wethEstimate.mul(995).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -695,7 +697,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataToInputToken, }; const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - paymentInfo.limitAmt = usdcEstimate.mul(995).div(1000); // 50% slippage (why?) + paymentInfo.limitAmt = usdcEstimate.mul(995).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); From cafd234adceeb2e649641a942e165dbb0c5c397b Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 15:00:11 -0400 Subject: [PATCH 38/61] fix redemption when weth is component --- contracts/exchangeIssuance/FlashMintDex.sol | 59 ++++++++++++++++--- .../integration/ethereum/flashMintDex.spec.ts | 48 +++++++-------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index df2e7d16..a3ab2e34 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -196,6 +196,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); + // TODO: returnExcessPaymentToken() function uint256 unusedWeth = wethReceived.sub(totalEthSold); if (unusedWeth > 0) { @@ -437,8 +438,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemParams.amountSetToken ); - uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); - for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID SWAP DATA"); + + // uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); + totalWethBought = 0; + for (uint256 i = 0; i < components.length; i++) { // If the component is equal to the output token we don't have to trade if (components[i] == address(WETH)) { @@ -449,11 +453,12 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemParams.componentSwapData[i], componentUnits[i] ); - dexAdapter.swapExactTokensForTokens(componentUnits[i], wethBuyAmt, _redeemParams.componentSwapData[i]); + uint256 wethReceived = dexAdapter.swapExactTokensForTokens(componentUnits[i], wethBuyAmt, _redeemParams.componentSwapData[i]); + totalWethBought = totalWethBought.add(wethReceived); } } - uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); - totalWethBought = wethBalanceAfter.sub(wethBalanceBefore); + // uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); + // totalWethBought = wethBalanceAfter.sub(wethBalanceBefore); } /** @@ -651,13 +656,53 @@ contract FlashMintDex is Ownable, ReentrancyGuard { if (components[i] == address(WETH)) { totalEthReceived = totalEthReceived.add(componentUnits[i]); } else { - totalEthReceived += DEXAdapterV2.getAmountOut( + totalEthReceived = totalEthReceived.add(DEXAdapterV2.getAmountOut( dexAdapter, _redeemParams.componentSwapData[i], componentUnits[i] - ); + )); } } return totalEthReceived; } + + /** + * Gets the amount of specified payment token expected to be received after redeeming + * a given quantity of a 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 the amount of output tokens expected after performing redemption + */ + function getRedeemExactSet( + IssueRedeemParams memory _redeemParams, + DEXAdapterV2.SwapData memory _swapDataWethToOutputToken + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + uint256 totalWethReceived = 0; + for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalWethReceived = totalWethReceived.add(componentUnits[i]); + } else { + totalWethReceived = totalWethReceived.add(DEXAdapterV2.getAmountOut( + dexAdapter, + _redeemParams.componentSwapData[i], + componentUnits[i] + )); + } + } + return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); + } } diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 866165f8..0bc9376a 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -65,14 +65,14 @@ const swapDataEmpty = { pool: ADDRESS_ZERO, }; -const swapDataFromInputToken = { +const swapDataUsdcToWeth = { exchange: Exchange.UniV3, fees: [500], path: [addresses.tokens.USDC, addresses.tokens.weth], pool: ADDRESS_ZERO, }; -const swapDataToInputToken = { +const swapDataWethToUsdc = { exchange: Exchange.UniV3, fees: [500], path: [addresses.tokens.weth, addresses.tokens.USDC], @@ -264,8 +264,8 @@ if (process.env.INTEGRATIONTEST) { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); expect(usdcRequired).to.eq(BigNumber.from("11063559168")); @@ -311,8 +311,8 @@ if (process.env.INTEGRATIONTEST) { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage @@ -343,27 +343,21 @@ if (process.env.INTEGRATIONTEST) { }); it("Can return ETH quantity received when redeeming legacy set token", async () => { - const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); console.log("ethReceived", ethReceivedEstimate); expect(ethReceivedEstimate).to.eq(BigNumber.from("3492695444625661021")); }); it("Can return USDC quantity received when redeeming legacy set token", async () => { - const paymentInfo: PaymentInfo = { - token: addresses.tokens.USDC, - limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); + const usdcReceived = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); console.log("usdcReceived", usdcReceived); expect(usdcReceived).to.eq(BigNumber.from("11054123420")); }); it("Can redeem legacy set token for ETH", async () => { - const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); - console.log("ethEstimate", ethEstimate); - const minAmountOut = ethEstimate.mul(500).div(1000); // 50% slippage (why?) + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + console.log("ethReceivedEstimate", ethReceivedEstimate.toString()); + 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); @@ -381,9 +375,9 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - console.log("wethEstimate", wethEstimate); - paymentInfo.limitAmt = wethEstimate.mul(50).div(100); // 50% slippage (why?) + const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + console.log("wethReceivedEstimate", wethReceivedEstimate.toString()); + paymentInfo.limitAmt = wethReceivedEstimate.mul(995).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -399,12 +393,12 @@ if (process.env.INTEGRATIONTEST) { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; - const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - console.log("usdcEstimate", usdcEstimate); - paymentInfo.limitAmt = usdcEstimate.mul(50).div(100); // 50% slippage (why?) + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + console.log("usdcReceivedEstimate", usdcReceivedEstimate); + paymentInfo.limitAmt = usdcReceivedEstimate.mul(995).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -567,7 +561,7 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataToInputToken, }; const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - expect(usdcRequired).to.eq(BigNumber.from("26648569515")); + expect(usdcRequired).to.eq(BigNumber.from("26643397666")); }); it("Can issue set token from ETH", async () => { @@ -655,7 +649,7 @@ if (process.env.INTEGRATIONTEST) { }; const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); console.log("usdcReceived", usdcReceived); - expect(usdcReceived).to.eq(BigNumber.from("26643979253")); + expect(usdcReceived).to.eq(BigNumber.from("26643397666")); }); it("Can redeem set token for ETH", async () => { From 8302f27a6ad2f97d690ce1b92c8e9ac22cbda3ac Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 8 Aug 2024 15:32:56 -0400 Subject: [PATCH 39/61] combine getissueexactset functions --- contracts/exchangeIssuance/FlashMintDex.sol | 309 ++++++------------ .../integration/ethereum/flashMintDex.spec.ts | 86 ++--- 2 files changed, 126 insertions(+), 269 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index a3ab2e34..14c99ed0 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -176,6 +176,45 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } } + /** + * 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 totalEthNeeded Amount of input tokens required to perfrom the issuance + */ + function getIssueExactSet( + IssueRedeemParams memory _issueParams, + DEXAdapterV2.SwapData memory _swapDataInputTokenToWeth + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( + _issueParams.issuanceModule, + _issueParams.isDebtIssuance, + _issueParams.setToken, + _issueParams.amountSetToken + ); + uint256 totalEthNeeded = 0; + for (uint256 i = 0; i < components.length; i++) { + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalEthNeeded = totalEthNeeded.add(componentUnits[i]); + } else { + totalEthNeeded += DEXAdapterV2.getAmountIn( + dexAdapter, + _issueParams.componentSwapData[i], + componentUnits[i] + ); + } + } + return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalEthNeeded); + } + /** * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. * The excess amount of tokens is returned in an equivalent amount of ether. @@ -240,6 +279,46 @@ contract FlashMintDex is Ownable, ReentrancyGuard { emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), totalEthSold, _issueParams.amountSetToken); } + /** + * 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 the amount of output tokens expected after performing redemption + */ + function getRedeemExactSet( + IssueRedeemParams memory _redeemParams, + DEXAdapterV2.SwapData memory _swapDataWethToOutputToken + ) + external + returns (uint256) + { + (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( + _redeemParams.issuanceModule, + _redeemParams.isDebtIssuance, + _redeemParams.setToken, + _redeemParams.amountSetToken + ); + uint256 totalWethReceived = 0; + for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalWethReceived = totalWethReceived.add(componentUnits[i]); + } else { + totalWethReceived = totalWethReceived.add(DEXAdapterV2.getAmountOut( + dexAdapter, + _redeemParams.componentSwapData[i], + componentUnits[i] + )); + } + } + return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); + } + /** * Redeems an exact amount of SetTokens for an ERC20 token. * The SetToken must be approved by the sender to this contract. @@ -252,7 +331,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 wethReceived = _sellComponentsForWeth(_redeemParams); uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); @@ -277,7 +356,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { nonReentrant returns (uint256) { - _redeemExactSet(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); + _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 ethAmount = _sellComponentsForWeth(_redeemParams); require(ethAmount >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); @@ -421,11 +500,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Redeems a given list of SetToken components for given token. + * 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 + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalWethBought Total amount of output token received after liquidating all SetToken components + * @return totalWethBought Total amount of WETH received after liquidating all SetToken components */ function _sellComponentsForWeth(IssueRedeemParams memory _redeemParams) internal @@ -440,7 +531,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID SWAP DATA"); - // uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); totalWethBought = 0; for (uint256 i = 0; i < components.length; i++) { @@ -457,20 +547,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { totalWethBought = totalWethBought.add(wethReceived); } } - // uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); - // totalWethBought = wethBalanceAfter.sub(wethBalanceBefore); - } - - /** - * 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 _redeemExactSet(ISetToken _setToken, uint256 _amount, address _issuanceModule) internal returns (uint256) { - _setToken.safeTransferFrom(msg.sender, address(this), _amount); - IBasicIssuanceModule(_issuanceModule).redeem(_setToken, _amount, address(this)); } /** @@ -512,197 +588,4 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } } } - - /** - * Gets the input cost of issuing a given amount of a 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 - * - * @return totalEthNeeded Amount of input tokens required to perfrom the issuance - */ - function getIssueExactSetFromEth( - IssueRedeemParams memory _issueParams - ) - external - returns (uint256) - { - (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( - _issueParams.issuanceModule, - _issueParams.isDebtIssuance, - _issueParams.setToken, - _issueParams.amountSetToken - ); - uint256 totalEthNeeded = 0; - for (uint256 i = 0; i < components.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalEthNeeded = totalEthNeeded.add(componentUnits[i]); - } else { - totalEthNeeded += DEXAdapterV2.getAmountIn( - dexAdapter, - _issueParams.componentSwapData[i], - componentUnits[i] - ); - } - } - return totalEthNeeded; - } - - /** - * Gets the input cost of issuing a given amount of a 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 _paymentInfo Struct containing input token address, max amount to spend, and swap data to trade for WETH - * - * @return the amount of input tokens required to perform the issuance - */ - function getIssueExactSetFromToken( - IssueRedeemParams memory _issueParams, - PaymentInfo memory _paymentInfo - ) - external - returns (uint256) - { - (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( - _issueParams.issuanceModule, - _issueParams.isDebtIssuance, - _issueParams.setToken, - _issueParams.amountSetToken - ); - uint256 totalWethNeeded = 0; - for (uint256 i = 0; i < components.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalWethNeeded = totalWethNeeded.add(componentUnits[i]); - } else { - totalWethNeeded += DEXAdapterV2.getAmountIn( - dexAdapter, - _issueParams.componentSwapData[i], - componentUnits[i] - ); - } - } - return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethNeeded); - } - - /** - * Gets the amount of specified payment token expected to be received after redeeming - * a given quantity of a 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 _paymentInfo Struct containing output token address, max amount to spend, and swap data to trade for WETH - * - * @return the amount of output tokens expected after performing redemption - */ - function getRedeemExactSetForToken( - IssueRedeemParams memory _redeemParams, - PaymentInfo memory _paymentInfo - ) - external - returns (uint256) - { - (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( - _redeemParams.issuanceModule, - _redeemParams.isDebtIssuance, - _redeemParams.setToken, - _redeemParams.amountSetToken - ); - uint256 totalWethReceived = 0; - for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalWethReceived = totalWethReceived.add(componentUnits[i]); - } else { - totalWethReceived += DEXAdapterV2.getAmountOut( - dexAdapter, - _redeemParams.componentSwapData[i], - componentUnits[i] - ); - } - } - return dexAdapter.getAmountOut(_paymentInfo.swapDataWethToToken, totalWethReceived); - } - - /** - * Gets the amount of ETH expected after redeeming a given amount of a 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 - * - * @return totalEthReceived Estimated amount of ETH received from redemption - */ - function getRedeemExactSetForEth( - IssueRedeemParams memory _redeemParams - ) - external - returns (uint256) - { - (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( - _redeemParams.issuanceModule, - _redeemParams.isDebtIssuance, - _redeemParams.setToken, - _redeemParams.amountSetToken - ); - uint256 totalEthReceived = 0; - for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalEthReceived = totalEthReceived.add(componentUnits[i]); - } else { - totalEthReceived = totalEthReceived.add(DEXAdapterV2.getAmountOut( - dexAdapter, - _redeemParams.componentSwapData[i], - componentUnits[i] - )); - } - } - return totalEthReceived; - } - - /** - * Gets the amount of specified payment token expected to be received after redeeming - * a given quantity of a 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 the amount of output tokens expected after performing redemption - */ - function getRedeemExactSet( - IssueRedeemParams memory _redeemParams, - DEXAdapterV2.SwapData memory _swapDataWethToOutputToken - ) - external - returns (uint256) - { - (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( - _redeemParams.issuanceModule, - _redeemParams.isDebtIssuance, - _redeemParams.setToken, - _redeemParams.amountSetToken - ); - uint256 totalWethReceived = 0; - for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalWethReceived = totalWethReceived.add(componentUnits[i]); - } else { - totalWethReceived = totalWethReceived.add(DEXAdapterV2.getAmountOut( - dexAdapter, - _redeemParams.componentSwapData[i], - componentUnits[i] - )); - } - } - return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); - } } diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 0bc9376a..5c47b131 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -256,23 +256,17 @@ if (process.env.INTEGRATIONTEST) { }); it("Can return ETH quantity required to issue legacy set token", async () => { - const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + 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 paymentInfo: PaymentInfo = { - token: addresses.tokens.USDC, - limitAmt: ether(0), - swapDataTokenToWeth: swapDataUsdcToWeth, - swapDataWethToToken: swapDataWethToUsdc, - }; - const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - expect(usdcRequired).to.eq(BigNumber.from("11063559168")); + 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.getIssueExactSetFromEth(issueParams); + 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); @@ -291,7 +285,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const wethEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); paymentInfo.limitAmt = wethEstimate.mul(1005).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); @@ -314,7 +308,7 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); @@ -336,7 +330,7 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.issueExactSetFromETH( issueParams, { - value: await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams), + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }, ); await setToken.approve(flashMintDex.address, setTokenAmount); @@ -344,26 +338,22 @@ if (process.env.INTEGRATIONTEST) { it("Can return ETH quantity received when redeeming legacy set token", async () => { const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); - console.log("ethReceived", ethReceivedEstimate); expect(ethReceivedEstimate).to.eq(BigNumber.from("3492695444625661021")); }); it("Can return USDC quantity received when redeeming legacy set token", async () => { - const usdcReceived = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); - console.log("usdcReceived", usdcReceived); - expect(usdcReceived).to.eq(BigNumber.from("11054123420")); + 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); - console.log("ethReceivedEstimate", ethReceivedEstimate.toString()); 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(); - console.log("eth received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gte(outputTokenBalanceBefore.add(minAmountOut)); }); @@ -397,7 +387,6 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataWethToUsdc, }; const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); - console.log("usdcReceivedEstimate", usdcReceivedEstimate); paymentInfo.limitAmt = usdcReceivedEstimate.mul(995).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); @@ -405,7 +394,6 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); - console.log("usdc received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); @@ -549,24 +537,18 @@ if (process.env.INTEGRATIONTEST) { }); it("Can return ETH quantity required to issue set token", async () => { - const ethRequired = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); + 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 paymentInfo: PaymentInfo = { - token: addresses.tokens.USDC, - limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - const usdcRequired = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - expect(usdcRequired).to.eq(BigNumber.from("26643397666")); + const usdcRequired = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + expect(usdcRequired).to.eq(BigNumber.from("26678902800")); }); it("Can issue set token from ETH", async () => { - const ethEstimate = await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams); - const maxEthIn = ethEstimate.mul(1005).div(1000); // 0.5% slippage + const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + const maxEthIn = ethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); @@ -584,8 +566,8 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - paymentInfo.limitAmt = wethEstimate.mul(1005).div(1000); // 0.5% slippage + const wethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + paymentInfo.limitAmt = wethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); await wethToken.deposit({ value: paymentInfo.limitAmt }); wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); @@ -606,8 +588,8 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataFromInputToken, swapDataWethToToken: swapDataToInputToken, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSetFromToken(issueParams, paymentInfo); - paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage + const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + paymentInfo.limitAmt = usdcRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); @@ -628,33 +610,25 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.issueExactSetFromETH( issueParams, { - value: await flashMintDex.callStatic.getIssueExactSetFromEth(issueParams), + 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.getRedeemExactSetForEth(redeemParams); - console.log("ethReceived", ethReceivedEstimate); + const ethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); expect(ethReceivedEstimate).to.eq(BigNumber.from("8423933102234975071")); }); it("Can return USDC quantity received when redeeming set token", async () => { - const paymentInfo: PaymentInfo = { - token: addresses.tokens.USDC, - limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, - }; - const usdcReceived = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - console.log("usdcReceived", usdcReceived); - expect(usdcReceived).to.eq(BigNumber.from("26643397666")); + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + expect(usdcReceivedEstimate).to.eq(BigNumber.from("26643397669")); }); it("Can redeem set token for ETH", async () => { - const ethEstimate = await flashMintDex.callStatic.getRedeemExactSetForEth(redeemParams); - const minAmountOut = ethEstimate.mul(995).div(1000); // 0.5% slippage + 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); @@ -671,8 +645,8 @@ if (process.env.INTEGRATIONTEST) { swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - paymentInfo.limitAmt = wethEstimate.mul(995).div(1000); // 0.5% slippage + const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + paymentInfo.limitAmt = wethReceivedEstimate.mul(995).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -687,11 +661,11 @@ if (process.env.INTEGRATIONTEST) { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; - const usdcEstimate = await flashMintDex.callStatic.getRedeemExactSetForToken(redeemParams, paymentInfo); - paymentInfo.limitAmt = usdcEstimate.mul(995).div(1000); // 0.5% slippage + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); + paymentInfo.limitAmt = usdcReceivedEstimate.mul(995).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); From 2aea7f81ec60ed8e224f2f93edf32052ab83fdd7 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 9 Aug 2024 15:03:30 -0400 Subject: [PATCH 40/61] refactor getIssueExactSet --- contracts/exchangeIssuance/FlashMintDex.sol | 66 ++++++++----------- .../integration/ethereum/flashMintDex.spec.ts | 3 - 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 14c99ed0..fed97f37 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -193,24 +193,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { external returns (uint256) { - (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( - _issueParams.issuanceModule, - _issueParams.isDebtIssuance, - _issueParams.setToken, - _issueParams.amountSetToken - ); uint256 totalEthNeeded = 0; - for (uint256 i = 0; i < components.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalEthNeeded = totalEthNeeded.add(componentUnits[i]); - } else { - totalEthNeeded += DEXAdapterV2.getAmountIn( - dexAdapter, - _issueParams.componentSwapData[i], - componentUnits[i] - ); - } + (,, uint256[] memory wethCosts) = _getWethCostsPerComponent(_issueParams); + + for (uint256 i = 0; i < wethCosts.length; i++) { + totalEthNeeded += wethCosts[i]; } return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalEthNeeded); } @@ -464,39 +451,44 @@ contract FlashMintDex is Ownable, ReentrancyGuard { internal returns (uint256 totalWethSold) { - uint256 componentAmountBought; + ( + address[] memory components, + uint256[] memory componentUnits, + uint256[] memory wethCosts + ) = _getWethCostsPerComponent(_issueParams); + + totalWethSold = 0; + for (uint256 i = 0; i < wethCosts.length; i++) { + // If the component is equal to WETH we don't have to trade + if (components[i] == address(WETH)) { + totalWethSold = totalWethSold.add(wethCosts[i]); + } else { + uint256 wethSpent = dexAdapter.swapTokensForExactTokens(componentUnits[i], wethCosts[i], _issueParams.componentSwapData[i]); + totalWethSold = totalWethSold.add(wethSpent); + } + } + } + function _getWethCostsPerComponent(IssueRedeemParams memory _issueParams) internal returns (address[] memory, uint256[] memory, uint256[] memory) { (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( _issueParams.issuanceModule, _issueParams.isDebtIssuance, _issueParams.setToken, _issueParams.amountSetToken ); - - uint256 wethBalanceBefore = IWETH(WETH).balanceOf(address(this)); + require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID SWAP DATA - NUMBER OF COMPONENTS"); + uint256[] memory wethCosts = new uint256[](components.length); for (uint256 i = 0; i < components.length; i++) { - address component = components[i]; - uint256 units = componentUnits[i]; - - // If the component is equal to WETH we don't have to trade - if (component == address(WETH)) { - totalWethSold = totalWethSold.add(units); - componentAmountBought = units; + if (components[i] == address(WETH)) { + wethCosts[i] = componentUnits[i]; } else { - uint256 componentBalanceBefore = IERC20(component).balanceOf(address(this)); - uint256 wethSellAmt = DEXAdapterV2.getAmountIn( - dexAdapter, + wethCosts[i] = dexAdapter.getAmountIn( _issueParams.componentSwapData[i], - units + componentUnits[i] ); - dexAdapter.swapTokensForExactTokens(units, wethSellAmt, _issueParams.componentSwapData[i]); - uint256 componentBalanceAfter = IERC20(component).balanceOf(address(this)); - componentAmountBought = componentBalanceAfter.sub(componentBalanceBefore); - require(componentAmountBought >= units, "FlashMint: UNDERBOUGHT COMPONENT"); } } - uint256 wethBalanceAfter = IWETH(WETH).balanceOf(address(this)); - totalWethSold = totalWethSold.add(wethBalanceBefore.sub(wethBalanceAfter)); + return (components, componentUnits, wethCosts); } /** diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 5c47b131..0b24a076 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -118,7 +118,6 @@ if (process.env.INTEGRATIONTEST) { context("When FlashMintDex contract is deployed", () => { let flashMintDex: FlashMintDex; - before(async () => { flashMintDex = await deployer.extensions.deployFlashMintDex( addresses.tokens.weth, @@ -366,7 +365,6 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataEmpty, }; const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); - console.log("wethReceivedEstimate", wethReceivedEstimate.toString()); paymentInfo.limitAmt = wethReceivedEstimate.mul(995).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); @@ -374,7 +372,6 @@ if (process.env.INTEGRATIONTEST) { await flashMintDex.redeemExactSetForToken(redeemParams, paymentInfo); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); const outputTokenBalanceAfter = await wethToken.balanceOf(owner.address); - console.log("weth received", outputTokenBalanceAfter.sub(outputTokenBalanceBefore).toString()); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenAmount)); expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); From 72bfb6ebb02bc108688896a570302c46ab37364e Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 9 Aug 2024 21:04:37 -0400 Subject: [PATCH 41/61] refactor getRedeemExactSet --- contracts/exchangeIssuance/FlashMintDex.sol | 77 +++++++++++---------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index fed97f37..9fb74bf1 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -235,7 +235,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSold, _issueParams.amountSetToken); } - /** * Issues an exact amount of SetTokens for given amount of ETH. * The excess amount of tokens is returned in an equivalent amount of ether. @@ -284,24 +283,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { external returns (uint256) { - (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( - _redeemParams.issuanceModule, - _redeemParams.isDebtIssuance, - _redeemParams.setToken, - _redeemParams.amountSetToken - ); uint256 totalWethReceived = 0; - for (uint256 i = 0; i < _redeemParams.componentSwapData.length; i++) { - // If the component is equal to WETH we don't have to trade - if (components[i] == address(WETH)) { - totalWethReceived = totalWethReceived.add(componentUnits[i]); - } else { - totalWethReceived = totalWethReceived.add(DEXAdapterV2.getAmountOut( - dexAdapter, - _redeemParams.componentSwapData[i], - componentUnits[i] - )); - } + (,, uint256[] memory wethReceived) = _getWethReceivedPerComponent(_redeemParams); + + for (uint256 i = 0; i < wethReceived.length; i++) { + totalWethReceived += wethReceived[i]; } return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); } @@ -431,9 +417,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return totalWethSold Amount of WETH used to buy components */ - function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) - internal - returns (uint256 totalWethSold) + function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) { totalWethSold = _buyComponentsWithWeth(_issueParams); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); @@ -447,10 +431,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return totalWethSold Total amount of WETH spent to buy components */ - function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) - internal - returns (uint256 totalWethSold) - { + function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) { ( address[] memory components, uint256[] memory componentUnits, @@ -459,7 +440,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { totalWethSold = 0; for (uint256 i = 0; i < wethCosts.length; i++) { - // If the component is equal to WETH we don't have to trade if (components[i] == address(WETH)) { totalWethSold = totalWethSold.add(wethCosts[i]); } else { @@ -469,14 +449,19 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } } - function _getWethCostsPerComponent(IssueRedeemParams memory _issueParams) internal returns (address[] memory, uint256[] memory, uint256[] memory) { + function _getWethCostsPerComponent(IssueRedeemParams memory _issueParams) + internal + returns (address[] memory, uint256[] memory, uint256[] memory) + { (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( _issueParams.issuanceModule, _issueParams.isDebtIssuance, _issueParams.setToken, _issueParams.amountSetToken ); - require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID SWAP DATA - NUMBER OF COMPONENTS"); + + require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); + uint256[] memory wethCosts = new uint256[](components.length); for (uint256 i = 0; i < components.length; i++) { if (components[i] == address(WETH)) { @@ -513,6 +498,27 @@ contract FlashMintDex is Ownable, ReentrancyGuard { function _sellComponentsForWeth(IssueRedeemParams memory _redeemParams) internal returns (uint256 totalWethBought) + { + ( + address[] memory components, + uint256[] memory componentUnits, + uint256[] memory wethReceived + ) = _getWethReceivedPerComponent(_redeemParams); + + totalWethBought = 0; + for (uint256 i = 0; i < wethReceived.length; i++) { + if (components[i] == address(WETH)) { + totalWethBought = totalWethBought.add(wethReceived[i]); + } else { + uint256 wethSpent = dexAdapter.swapExactTokensForTokens(componentUnits[i], wethReceived[i], _redeemParams.componentSwapData[i]); + totalWethBought = totalWethBought.add(wethSpent); + } + } + } + + function _getWethReceivedPerComponent(IssueRedeemParams memory _redeemParams) + internal + returns (address[] memory, uint256[] memory, uint256[] memory) { (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( _redeemParams.issuanceModule, @@ -521,24 +527,20 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _redeemParams.amountSetToken ); - require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID SWAP DATA"); + require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); - totalWethBought = 0; + uint256[] memory wethReceived = new uint256[](components.length); for (uint256 i = 0; i < components.length; i++) { - - // If the component is equal to the output token we don't have to trade if (components[i] == address(WETH)) { - totalWethBought = totalWethBought.add(componentUnits[i]); + wethReceived[i] = componentUnits[i]; } else { - uint256 wethBuyAmt = DEXAdapterV2.getAmountOut( - dexAdapter, + wethReceived[i] = dexAdapter.getAmountOut( _redeemParams.componentSwapData[i], componentUnits[i] ); - uint256 wethReceived = dexAdapter.swapExactTokensForTokens(componentUnits[i], wethBuyAmt, _redeemParams.componentSwapData[i]); - totalWethBought = totalWethBought.add(wethReceived); } } + return (components, componentUnits, wethReceived); } /** @@ -556,7 +558,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { else { (components, positions) = IBasicIssuanceModule(_issuanceModule).getRequiredComponentUnitsForIssue(_setToken, _amountSetToken); } - } /** From 79dd921c79f153fa9919df11a8be8e48d78ee37d Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Fri, 9 Aug 2024 21:32:42 -0400 Subject: [PATCH 42/61] reorder funcs add comments --- contracts/exchangeIssuance/FlashMintDex.sol | 178 ++++++++++-------- .../integration/ethereum/flashMintDex.spec.ts | 16 +- 2 files changed, 103 insertions(+), 91 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 9fb74bf1..754d6ddd 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -131,7 +131,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { receive() external payable { // required for weth.withdraw() to work properly - require(msg.sender == WETH, "FlashMint: Direct deposits not allowed"); + require(msg.sender == WETH, "FlashMint: DIRECT DEPOSITS NOT ALLOWED"); } /* ============ Public Functions ============ */ @@ -203,36 +203,30 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. - * The excess amount of tokens is returned in an equivalent amount of ether. - * - * @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 - * - * @return excessPaymentTokenAmt Amount of input token returned to the caller - */ - function issueExactSetFromToken(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo) + * 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 the amount of output tokens expected after performing redemption + */ + function getRedeemExactSet( + IssueRedeemParams memory _redeemParams, + DEXAdapterV2.SwapData memory _swapDataWethToOutputToken + ) external - isValidModule(_issueParams.issuanceModule) - nonReentrant - returns (uint256 excessPaymentTokenAmt) + returns (uint256) { - _paymentInfo.token.safeTransferFrom(msg.sender, address(this), _paymentInfo.limitAmt); - uint256 wethReceived = _swapPaymentTokenForWETH(_paymentInfo.token, _paymentInfo.limitAmt, _paymentInfo.swapDataTokenToWeth); - - uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); - require(totalEthSold <= wethReceived, "FlashMint: OVERSPENT WETH"); - // TODO: returnExcessPaymentToken() function - uint256 unusedWeth = wethReceived.sub(totalEthSold); + uint256 totalWethReceived = 0; + (,, uint256[] memory wethReceived) = _getWethReceivedPerComponent(_redeemParams); - if (unusedWeth > 0) { - excessPaymentTokenAmt = _swapWethForPaymentToken(unusedWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); - _paymentInfo.token.safeTransfer(msg.sender, excessPaymentTokenAmt); + for (uint256 i = 0; i < wethReceived.length; i++) { + totalWethReceived += wethReceived[i]; } - - uint256 paymentTokenSold = _paymentInfo.limitAmt.sub(excessPaymentTokenAmt); - - emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSold, _issueParams.amountSetToken); + return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); } /** @@ -252,53 +246,59 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IWETH(WETH).deposit{value: msg.value}(); - uint256 totalEthSold = _issueExactSetFromWeth(_issueParams); + uint256 ethUsedForIssuance = _issueExactSetFromWeth(_issueParams); - require(totalEthSold <= msg.value, "FlashMint: OVERSPENT ETH"); + require(ethUsedForIssuance <= msg.value, "FlashMint: OVERSPENT ETH"); - uint256 amountEthReturn = msg.value.sub(totalEthSold); - if (amountEthReturn > 0) { - IWETH(WETH).withdraw(amountEthReturn); - payable(msg.sender).sendValue(amountEthReturn); + uint256 ethReturned = msg.value.sub(ethUsedForIssuance); + if (ethReturned > 0) { + IWETH(WETH).withdraw(ethReturned); + payable(msg.sender).sendValue(ethReturned); } + uint256 ethSpent = msg.value.sub(ethReturned); - emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), totalEthSold, _issueParams.amountSetToken); + emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), ethSpent, _issueParams.amountSetToken); } /** - * 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 the amount of output tokens expected after performing redemption - */ - function getRedeemExactSet( - IssueRedeemParams memory _redeemParams, - DEXAdapterV2.SwapData memory _swapDataWethToOutputToken - ) + * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. + * The excess amount of tokens is returned in an equivalent amount of ether. + * + * @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 + * + * @return paymentTokenReturned Amount of input token returned to the caller + */ + function issueExactSetFromERC20(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo) external - returns (uint256) + isValidModule(_issueParams.issuanceModule) + nonReentrant + returns (uint256 paymentTokenReturned) { - uint256 totalWethReceived = 0; - (,, uint256[] memory wethReceived) = _getWethReceivedPerComponent(_redeemParams); + _paymentInfo.token.safeTransferFrom(msg.sender, address(this), _paymentInfo.limitAmt); + uint256 wethReceived = _swapPaymentTokenForWeth(_paymentInfo.token, _paymentInfo.limitAmt, _paymentInfo.swapDataTokenToWeth); - for (uint256 i = 0; i < wethReceived.length; i++) { - totalWethReceived += wethReceived[i]; + uint256 wethSpent = _issueExactSetFromWeth(_issueParams); + require(wethSpent <= wethReceived, "FlashMint: OVERSPENT WETH"); + uint256 leftoverWeth = wethReceived.sub(wethSpent); + + if (leftoverWeth > 0) { + paymentTokenReturned = _swapWethForPaymentToken(leftoverWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + _paymentInfo.token.safeTransfer(msg.sender, paymentTokenReturned); } - return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); + + uint256 paymentTokenSpent = _paymentInfo.limitAmt.sub(paymentTokenReturned); + + emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSpent, _issueParams.amountSetToken); } /** - * Redeems an exact amount of SetTokens for an ERC20 token. + * Redeems an exact amount of SetTokens for ETH. * The SetToken must be approved by the sender to this contract. * - * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance + * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance */ - function redeemExactSetForToken(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) + function redeemExactSetForETH(IssueRedeemParams memory _redeemParams, uint256 _minEthReceive) external isValidModule(_redeemParams.issuanceModule) nonReentrant @@ -306,24 +306,23 @@ contract FlashMintDex is Ownable, ReentrancyGuard { { _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - uint256 wethReceived = _sellComponentsForWeth(_redeemParams); - uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); - require(outputAmount >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + uint256 ethAmount = _sellComponentsForWeth(_redeemParams); + require(ethAmount >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); - _paymentInfo.token.safeTransfer(msg.sender, outputAmount); + IWETH(WETH).withdraw(ethAmount); + payable(msg.sender).sendValue(ethAmount); - emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputAmount); + emit FlashRedeem(msg.sender, _redeemParams.setToken, IERC20(ETH_ADDRESS), _redeemParams.amountSetToken, ethAmount); + return ethAmount; } /** - * Redeems an exact amount of SetTokens for ETH. + * 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 addresses, amounts, and swap data for issuance - * - * @return ethAmount The amount of ETH received. + * @param _redeemParams Struct containing token addresses, amounts, and swap data for issuance */ - function redeemExactSetForETH(IssueRedeemParams memory _redeemParams, uint256 _minEthReceive) + function redeemExactSetForERC20(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) external isValidModule(_redeemParams.issuanceModule) nonReentrant @@ -331,14 +330,13 @@ contract FlashMintDex is Ownable, ReentrancyGuard { { _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - uint256 ethAmount = _sellComponentsForWeth(_redeemParams); - require(ethAmount >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); + uint256 wethReceived = _sellComponentsForWeth(_redeemParams); + uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + require(outputAmount >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); - IWETH(WETH).withdraw(ethAmount); - payable(msg.sender).sendValue(ethAmount); + _paymentInfo.token.safeTransfer(msg.sender, outputAmount); - emit FlashRedeem(msg.sender, _redeemParams.setToken, IERC20(ETH_ADDRESS), _redeemParams.amountSetToken, ethAmount); - return ethAmount; + emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputAmount); } /* ============ Internal Functions ============ */ @@ -366,7 +364,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @return amountWethOut Amount of WETH received after the swap */ - function _swapPaymentTokenForWETH( + function _swapPaymentTokenForWeth( IERC20 _paymentToken, uint256 _paymentTokenAmount, DEXAdapterV2.SwapData memory _swapData @@ -415,11 +413,11 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalWethSold Amount of WETH used to buy components + * @return totalWethSpent Amount of WETH used to buy components */ - function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) + function _issueExactSetFromWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSpent) { - totalWethSold = _buyComponentsWithWeth(_issueParams); + totalWethSpent = _buyComponentsWithWeth(_issueParams); IBasicIssuanceModule(_issueParams.issuanceModule).issue(_issueParams.setToken, _issueParams.amountSetToken, msg.sender); } @@ -429,26 +427,33 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalWethSold Total amount of WETH spent to buy components + * @return totalWethSpent Total amount of WETH spent to buy components */ - function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSold) { + function _buyComponentsWithWeth(IssueRedeemParams memory _issueParams) internal returns (uint256 totalWethSpent) { ( address[] memory components, uint256[] memory componentUnits, uint256[] memory wethCosts ) = _getWethCostsPerComponent(_issueParams); - totalWethSold = 0; + totalWethSpent = 0; for (uint256 i = 0; i < wethCosts.length; i++) { if (components[i] == address(WETH)) { - totalWethSold = totalWethSold.add(wethCosts[i]); + totalWethSpent = totalWethSpent.add(wethCosts[i]); } else { - uint256 wethSpent = dexAdapter.swapTokensForExactTokens(componentUnits[i], wethCosts[i], _issueParams.componentSwapData[i]); - totalWethSold = totalWethSold.add(wethSpent); + uint256 wethSoldForComponent = dexAdapter.swapTokensForExactTokens(componentUnits[i], wethCosts[i], _issueParams.componentSwapData[i]); + totalWethSpent = totalWethSpent.add(wethSoldForComponent); } } } + /** + * Calculates the amount of WETH required to buy the components required for issuance. + * + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return Tuple of component addresses, units required for issuance, and WETH costs + */ function _getWethCostsPerComponent(IssueRedeemParams memory _issueParams) internal returns (address[] memory, uint256[] memory, uint256[] memory) @@ -516,6 +521,13 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } } + /** + * Calculates the amount of WETH received for selling off each component after redemption. + * + * @param _redeemParams Struct containing addresses, amounts, and swap data for redemption + * + * @return totalWethBought Tuple of component addresses, units to sell, and WETH received + */ function _getWethReceivedPerComponent(IssueRedeemParams memory _redeemParams) internal returns (address[] memory, uint256[] memory, uint256[] memory) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 0b24a076..3b0cab74 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -293,7 +293,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -317,7 +317,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -369,7 +369,7 @@ if (process.env.INTEGRATIONTEST) { 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.redeemExactSetForToken(redeemParams, paymentInfo); + 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)); @@ -388,7 +388,7 @@ if (process.env.INTEGRATIONTEST) { 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.redeemExactSetForToken(redeemParams, paymentInfo); + 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)); @@ -571,7 +571,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); const inputTokenBalanceAfter = await wethToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -595,7 +595,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromToken(issueParams, paymentInfo); + await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); const inputTokenBalanceAfter = await usdcToken.balanceOf(owner.address); const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); @@ -647,7 +647,7 @@ if (process.env.INTEGRATIONTEST) { 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.redeemExactSetForToken(redeemParams, paymentInfo); + 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)); @@ -666,7 +666,7 @@ if (process.env.INTEGRATIONTEST) { 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.redeemExactSetForToken(redeemParams, paymentInfo); + 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)); From ed4ff09c2e26114c68f8a1263fcb1f6dbe78f728 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 12 Aug 2024 14:56:04 -0400 Subject: [PATCH 43/61] cleanup --- .../integration/ethereum/flashMintDex.spec.ts | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 3b0cab74..c2fdb635 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -397,7 +397,7 @@ if (process.env.INTEGRATIONTEST) { }); }); - context("when setToken with a simple composition is deployed", () => { + context("when setToken is deployed on Index Protocol", () => { let setToken: SetToken; let issueParams: IssueRedeemParams; let redeemParams: IssueRedeemParams; @@ -443,20 +443,6 @@ if (process.env.INTEGRATIONTEST) { }, ]; - const swapDataFromInputToken = { - exchange: Exchange.UniV3, - fees: [500], - path: [addresses.tokens.USDC, addresses.tokens.weth], - pool: ADDRESS_ZERO, - }; - - const swapDataToInputToken = { - exchange: Exchange.UniV3, - fees: [500], - path: [addresses.tokens.weth, addresses.tokens.USDC], - pool: ADDRESS_ZERO, - }; - const componentSwapDataRedeem = [ { exchange: Exchange.UniV3, @@ -582,8 +568,8 @@ if (process.env.INTEGRATIONTEST) { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), - swapDataTokenToWeth: swapDataFromInputToken, - swapDataWethToToken: swapDataToInputToken, + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); paymentInfo.limitAmt = usdcRequiredEstimate.mul(1005).div(1000); // 0.5% slippage From a161fbbd799eec031f7218ccfdd8c858b1babb70 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 12 Aug 2024 15:27:31 -0400 Subject: [PATCH 44/61] invalid swap and settoken --- .../integration/ethereum/flashMintDex.spec.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index c2fdb635..4bf1ce4d 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -659,6 +659,43 @@ if (process.env.INTEGRATIONTEST) { expect(outputTokenBalanceAfter).to.gt(outputTokenBalanceBefore.add(paymentInfo.limitAmt)); }); }); + + context("When invalid unputs are given", () => { + let invalidIssueParams: IssueRedeemParams; + beforeEach(async () => { + // reset invalidIssueParams each test + 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( + issueParams, + { + 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(issueParams, swapDataEmpty); + const notEnoughEth = ethRequiredEstimate.div(2); + + await expect( + flashMintDex.issueExactSetFromETH(issueParams, { value: notEnoughEth }), + ).to.be.revertedWith("STF"); + }); + }); }); }); }); From 2a50c86abd81c50b77d3c7499fa6dff6a8db6b49 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 12 Aug 2024 15:45:52 -0400 Subject: [PATCH 45/61] test for revert on invalid issuance --- .../integration/ethereum/flashMintDex.spec.ts | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 4bf1ce4d..8b8ef98d 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -660,7 +660,7 @@ if (process.env.INTEGRATIONTEST) { }); }); - context("When invalid unputs are given", () => { + context.only("When invalid unputs are given", () => { let invalidIssueParams: IssueRedeemParams; beforeEach(async () => { // reset invalidIssueParams each test @@ -679,7 +679,7 @@ if (process.env.INTEGRATIONTEST) { await expect( flashMintDex.issueExactSetFromETH( - issueParams, + invalidIssueParams, { value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }, @@ -688,13 +688,38 @@ if (process.env.INTEGRATIONTEST) { }); it("should revert when not enough ETH is sent for issuance", async () => { - const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(invalidIssueParams, swapDataEmpty); const notEnoughEth = ethRequiredEstimate.div(2); await expect( - flashMintDex.issueExactSetFromETH(issueParams, { value: notEnoughEth }), + flashMintDex.issueExactSetFromETH(invalidIssueParams, { value: notEnoughEth }), ).to.be.revertedWith("STF"); }); + + it("should revert when not enough ERC20 is sent for issuance", async () => { + const paymentInfo: PaymentInfo = { + token: addresses.tokens.USDC, + limitAmt: ether(0), + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, + }; + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); + const usdc = IERC20__factory.connect(paymentInfo.token, owner.wallet); + usdc.approve(flashMintDex.address, usdcEstimate); + paymentInfo.limitAmt = usdcEstimate.div(2); + await expect( + flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo), + ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + // it("should revert when minimum ETH is not received during redemption", async () => { + + // const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); + // paymentInfo.limitAmt = usdcEstimate.div(2); + // await expect( + // flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo), + // ).to.be.revertedWith("STF"); + // }); }); }); }); From 63102def2c2b164d469e39aec21feb4a6c24f2bf Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 12 Aug 2024 15:59:48 -0400 Subject: [PATCH 46/61] test for eth received in redemption --- .../integration/ethereum/flashMintDex.spec.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 8b8ef98d..2e2a9b78 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -697,29 +697,36 @@ if (process.env.INTEGRATIONTEST) { }); it("should revert when not enough ERC20 is sent for issuance", async () => { - const paymentInfo: PaymentInfo = { + 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: ether(0), + limitAmt: usdcEstimate.div(2), swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); - const usdc = IERC20__factory.connect(paymentInfo.token, owner.wallet); - usdc.approve(flashMintDex.address, usdcEstimate); - paymentInfo.limitAmt = usdcEstimate.div(2); await expect( - flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo), - ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + flashMintDex.issueExactSetFromERC20(issueParams, paymentInfoNotEnoughUsdc), + ).to.be.revertedWith("STF"); }); - // it("should revert when minimum ETH is not received during redemption", async () => { - - // const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); - // paymentInfo.limitAmt = usdcEstimate.div(2); - // await expect( - // flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo), - // ).to.be.revertedWith("STF"); - // }); + it("should revert when minimum ETH is not received during redemption", async () => { + const testRedeemParams = { ...redeemParams }; + const setToken = SetToken__factory.connect(testRedeemParams.setToken, owner.wallet); + setToken.approve(flashMintDex.address, testRedeemParams.amountSetToken); + await flashMintDex.issueExactSetFromETH(issueParams, { + value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), + }); + const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(testRedeemParams, swapDataEmpty); + const minAmountOutTooHigh = ethEstimate.mul(2); + await expect( + flashMintDex.redeemExactSetForETH(testRedeemParams, minAmountOutTooHigh), + ).to.be.revertedWith("FlashMint: INSUFFICIENT WETH RECEIVED"); + }); }); }); }); From fb1b5d7cb5809732694479cda559082ce74f8b9a Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 12 Aug 2024 16:08:36 -0400 Subject: [PATCH 47/61] test for min received usdc for redemption --- .../integration/ethereum/flashMintDex.spec.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 2e2a9b78..deaeb4a1 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -715,18 +715,32 @@ if (process.env.INTEGRATIONTEST) { }); it("should revert when minimum ETH is not received during redemption", async () => { - const testRedeemParams = { ...redeemParams }; - const setToken = SetToken__factory.connect(testRedeemParams.setToken, owner.wallet); - setToken.approve(flashMintDex.address, testRedeemParams.amountSetToken); + const setToken = SetToken__factory.connect(redeemParams.setToken, owner.wallet); + setToken.approve(flashMintDex.address, redeemParams.amountSetToken); await flashMintDex.issueExactSetFromETH(issueParams, { value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }); - const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(testRedeemParams, swapDataEmpty); + const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); const minAmountOutTooHigh = ethEstimate.mul(2); await expect( - flashMintDex.redeemExactSetForETH(testRedeemParams, minAmountOutTooHigh), + 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"); + }); }); }); }); From 266aed9ba2db5220ce67cc332afadb79c0d95388 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 10:15:40 -0400 Subject: [PATCH 48/61] tests for invalid issuance module and set token test for revert on eth direct deposit --- contracts/exchangeIssuance/FlashMintDex.sol | 17 +++-- .../integration/ethereum/flashMintDex.spec.ts | 63 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 754d6ddd..55301a50 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -96,6 +96,15 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _; } + 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" + ); + _; + } + constructor( address _weth, IController _setController, @@ -238,7 +247,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { function issueExactSetFromETH(IssueRedeemParams memory _issueParams) external payable - isValidModule(_issueParams.issuanceModule) + isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) nonReentrant returns (uint256) { @@ -271,7 +280,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { */ function issueExactSetFromERC20(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo) external - isValidModule(_issueParams.issuanceModule) + isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) nonReentrant returns (uint256 paymentTokenReturned) { @@ -300,7 +309,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { */ function redeemExactSetForETH(IssueRedeemParams memory _redeemParams, uint256 _minEthReceive) external - isValidModule(_redeemParams.issuanceModule) + isValidModuleAndSet(_redeemParams.issuanceModule, address(_redeemParams.setToken)) nonReentrant returns (uint256) { @@ -324,7 +333,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { */ function redeemExactSetForERC20(IssueRedeemParams memory _redeemParams, PaymentInfo memory _paymentInfo) external - isValidModule(_redeemParams.issuanceModule) + isValidModuleAndSet(_redeemParams.issuanceModule, address(_redeemParams.setToken)) nonReentrant returns (uint256) { diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index deaeb4a1..c1c7b8de 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -80,10 +80,9 @@ const swapDataWethToUsdc = { }; if (process.env.INTEGRATIONTEST) { - describe.only("FlashMintDex - Integration Test", async () => { + describe("FlashMintDex - Integration Test", async () => { let owner: Account; let deployer: DeployHelper; - let legacySetTokenCreator: SetTokenCreator; let setTokenCreator: SetTokenCreator; let legacyBasicIssuanceModule: IBasicIssuanceModule; @@ -166,6 +165,12 @@ if (process.env.INTEGRATIONTEST) { ); }); + 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; @@ -660,10 +665,9 @@ if (process.env.INTEGRATIONTEST) { }); }); - context.only("When invalid unputs are given", () => { + context("When invalid inputs are given", () => { let invalidIssueParams: IssueRedeemParams; beforeEach(async () => { - // reset invalidIssueParams each test invalidIssueParams = { ...issueParams }; }); @@ -741,6 +745,57 @@ if (process.env.INTEGRATIONTEST) { 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, { + 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) + ).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"); + }); }); }); }); From 8416908e425371eae1e686409992466d589a20d3 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 11:22:46 -0400 Subject: [PATCH 49/61] test for returning excess input token --- contracts/exchangeIssuance/FlashMintDex.sol | 43 +++++++++++-------- .../integration/ethereum/flashMintDex.spec.ts | 23 +++++++--- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 55301a50..d9679ba8 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -243,13 +243,15 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * The excess amount of tokens is returned in an equivalent amount of ether. * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * + * @return ethSpent Amount of ETH spent */ function issueExactSetFromETH(IssueRedeemParams memory _issueParams) external payable isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) nonReentrant - returns (uint256) + returns (uint256 ethSpent) { require(msg.value > 0, "FlashMint: NO ETH SENT"); @@ -264,7 +266,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { IWETH(WETH).withdraw(ethReturned); payable(msg.sender).sendValue(ethReturned); } - uint256 ethSpent = msg.value.sub(ethReturned); + ethSpent = msg.value.sub(ethReturned); emit FlashMint(msg.sender, _issueParams.setToken, IERC20(ETH_ADDRESS), ethSpent, _issueParams.amountSetToken); } @@ -276,13 +278,13 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @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 * - * @return paymentTokenReturned Amount of input token returned to the caller + * @return paymentTokenSpent Amount of input token spent */ function issueExactSetFromERC20(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo) external isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) nonReentrant - returns (uint256 paymentTokenReturned) + returns (uint256 paymentTokenSpent) { _paymentInfo.token.safeTransferFrom(msg.sender, address(this), _paymentInfo.limitAmt); uint256 wethReceived = _swapPaymentTokenForWeth(_paymentInfo.token, _paymentInfo.limitAmt, _paymentInfo.swapDataTokenToWeth); @@ -290,13 +292,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 wethSpent = _issueExactSetFromWeth(_issueParams); require(wethSpent <= wethReceived, "FlashMint: OVERSPENT WETH"); uint256 leftoverWeth = wethReceived.sub(wethSpent); + uint256 paymentTokenReturned = 0; if (leftoverWeth > 0) { paymentTokenReturned = _swapWethForPaymentToken(leftoverWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); _paymentInfo.token.safeTransfer(msg.sender, paymentTokenReturned); } - uint256 paymentTokenSpent = _paymentInfo.limitAmt.sub(paymentTokenReturned); + paymentTokenSpent = _paymentInfo.limitAmt.sub(paymentTokenReturned); emit FlashMint(msg.sender, _issueParams.setToken, _paymentInfo.token, paymentTokenSpent, _issueParams.amountSetToken); } @@ -306,46 +309,50 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * 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) + returns (uint256 ethReceived) { _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); - uint256 ethAmount = _sellComponentsForWeth(_redeemParams); - require(ethAmount >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); + ethReceived = _sellComponentsForWeth(_redeemParams); + require(ethReceived >= _minEthReceive, "FlashMint: INSUFFICIENT WETH RECEIVED"); - IWETH(WETH).withdraw(ethAmount); - payable(msg.sender).sendValue(ethAmount); + IWETH(WETH).withdraw(ethReceived); + payable(msg.sender).sendValue(ethReceived); - emit FlashRedeem(msg.sender, _redeemParams.setToken, IERC20(ETH_ADDRESS), _redeemParams.amountSetToken, ethAmount); - return ethAmount; + 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 + * @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) + returns (uint256 outputTokenReceived) { _redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule); uint256 wethReceived = _sellComponentsForWeth(_redeemParams); - uint256 outputAmount = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); - require(outputAmount >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); + outputTokenReceived = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken); + require(outputTokenReceived >= _paymentInfo.limitAmt, "FlashMint: INSUFFICIENT OUTPUT AMOUNT"); - _paymentInfo.token.safeTransfer(msg.sender, outputAmount); + _paymentInfo.token.safeTransfer(msg.sender, outputTokenReceived); - emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputAmount); + emit FlashRedeem(msg.sender, _redeemParams.setToken, _paymentInfo.token, _redeemParams.amountSetToken, outputTokenReceived); } /* ============ Internal Functions ============ */ diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index c1c7b8de..a64653c9 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -534,20 +534,27 @@ if (process.env.INTEGRATIONTEST) { expect(usdcRequired).to.eq(BigNumber.from("26678902800")); }); - it("Can issue set token from ETH", async () => { + it.only("Can issue set token from ETH", async () => { const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); const maxEthIn = ethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - await flashMintDex.issueExactSetFromETH(issueParams, { value: maxEthIn }); + const tx = await flashMintDex.issueExactSetFromETH(issueParams, { 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.gt(ethBalanceBefore.sub(maxEthIn)); + + // Check that excess ETH was returned to caller + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed; + const gasPrice = tx.gasPrice; + const ethUsedForGas = gasUsed.mul(gasPrice); + expect(ethBalanceBefore.sub(ethRequiredEstimate).sub(ethUsedForGas)).to.eq(ethBalanceAfter); }); - it("Can issue set token from WETH", async () => { + it.only("Can issue set token from WETH", async () => { const paymentInfo: PaymentInfo = { token: addresses.tokens.weth, limitAmt: ether(0), @@ -567,9 +574,12 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + + // Check that excess WETH was returned to caller + expect(inputTokenBalanceBefore.sub(wethRequiredEstimate)).to.eq(inputTokenBalanceAfter); }); - it("Can issue set token from USDC", async () => { + it.only("Can issue set token from USDC", async () => { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), @@ -590,7 +600,10 @@ if (process.env.INTEGRATIONTEST) { 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)); + expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + + // Check that excess USDC was returned to caller + expect(inputTokenBalanceBefore.sub(usdcRequiredEstimate)).to.eq(inputTokenBalanceAfter); }); describe("When set token has been issued", () => { From 294842bbec261708dd09dce7dda898a43a543d8e Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 12:11:49 -0400 Subject: [PATCH 50/61] cleanup --- contracts/exchangeIssuance/FlashMintDex.sol | 10 ++++----- .../integration/ethereum/flashMintDex.spec.ts | 21 ++++--------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index d9679ba8..7c88990e 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -240,10 +240,10 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Issues an exact amount of SetTokens for given amount of ETH. - * The excess amount of tokens is returned in an equivalent amount of ether. + * Any leftover ether is returned to the caller. * * @param _issueParams Struct containing addresses, amounts, and swap data for issuance - * + * * @return ethSpent Amount of ETH spent */ function issueExactSetFromETH(IssueRedeemParams memory _issueParams) @@ -273,7 +273,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Issues an exact amount of SetTokens for given amount of input ERC20 tokens. - * The excess amount of tokens is returned in an equivalent amount of ether. + * Any leftover value is swapped back to the payment token and returned to the caller. * * @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 @@ -309,7 +309,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * 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) @@ -335,7 +335,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * 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) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index a64653c9..e263ea63 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -534,27 +534,20 @@ if (process.env.INTEGRATIONTEST) { expect(usdcRequired).to.eq(BigNumber.from("26678902800")); }); - it.only("Can issue set token from ETH", async () => { + it("Can issue set token from ETH", async () => { const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); const maxEthIn = ethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - const tx = await flashMintDex.issueExactSetFromETH(issueParams, { value: maxEthIn }); + await flashMintDex.issueExactSetFromETH(issueParams, { 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.gt(ethBalanceBefore.sub(maxEthIn)); - - // Check that excess ETH was returned to caller - const receipt = await tx.wait(); - const gasUsed = receipt.gasUsed; - const gasPrice = tx.gasPrice; - const ethUsedForGas = gasUsed.mul(gasPrice); - expect(ethBalanceBefore.sub(ethRequiredEstimate).sub(ethUsedForGas)).to.eq(ethBalanceAfter); }); - it.only("Can issue set token from WETH", async () => { + it("Can issue set token from WETH", async () => { const paymentInfo: PaymentInfo = { token: addresses.tokens.weth, limitAmt: ether(0), @@ -574,12 +567,9 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); - - // Check that excess WETH was returned to caller - expect(inputTokenBalanceBefore.sub(wethRequiredEstimate)).to.eq(inputTokenBalanceAfter); }); - it.only("Can issue set token from USDC", async () => { + it("Can issue set token from USDC", async () => { const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, limitAmt: ether(0), @@ -601,9 +591,6 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceAfter = await setToken.balanceOf(owner.address); expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.add(setTokenAmount)); expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); - - // Check that excess USDC was returned to caller - expect(inputTokenBalanceBefore.sub(usdcRequiredEstimate)).to.eq(inputTokenBalanceAfter); }); describe("When set token has been issued", () => { From dec07ea10ea6410bbc9f12db5c3cfd3c69623d69 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 14:35:21 -0400 Subject: [PATCH 51/61] better assignment ordering --- .../integration/ethereum/flashMintDex.spec.ts | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index e263ea63..2c8b79be 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -283,14 +283,13 @@ if (process.env.INTEGRATIONTEST) { }); 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: ether(0), + limitAmt: wethEstimate.mul(1005).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); - paymentInfo.limitAmt = wethEstimate.mul(1005).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); await wethToken.deposit({ value: paymentInfo.limitAmt }); @@ -306,14 +305,13 @@ if (process.env.INTEGRATIONTEST) { }); it("Can issue set token from USDC", async () => { + const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, - limitAmt: ether(0), + limitAmt: usdcEstimate.mul(1005).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); - paymentInfo.limitAmt = usdcEstimate.mul(1005).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); @@ -363,14 +361,13 @@ if (process.env.INTEGRATIONTEST) { }); 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: ether(0), + limitAmt: wethReceivedEstimate.mul(995).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); - paymentInfo.limitAmt = wethReceivedEstimate.mul(995).div(1000); // 0.5% slippage const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -382,14 +379,13 @@ if (process.env.INTEGRATIONTEST) { }); it("Can redeem set token for USDC", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, - limitAmt: ether(0), + limitAmt: usdcReceivedEstimate.mul(995).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); - paymentInfo.limitAmt = usdcReceivedEstimate.mul(995).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -548,14 +544,14 @@ if (process.env.INTEGRATIONTEST) { }); it("Can issue set token from WETH", async () => { + const wethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); const paymentInfo: PaymentInfo = { token: addresses.tokens.weth, - limitAmt: ether(0), + limitAmt: wethRequiredEstimate.mul(1005).div(1000), // 0.5% slippage, swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); - paymentInfo.limitAmt = wethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); await wethToken.deposit({ value: paymentInfo.limitAmt }); wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); @@ -570,14 +566,13 @@ if (process.env.INTEGRATIONTEST) { }); it("Can issue set token from USDC", async () => { + const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, - limitAmt: ether(0), + limitAmt: usdcRequiredEstimate.mul(1005).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataUsdcToWeth); - paymentInfo.limitAmt = usdcRequiredEstimate.mul(1005).div(1000); // 0.5% slippage const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const whaleSigner = await impersonateAccount(addresses.whales.USDC); @@ -627,14 +622,14 @@ if (process.env.INTEGRATIONTEST) { }); it("Can redeem set token for WETH", async () => { + const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); const paymentInfo: PaymentInfo = { token: addresses.tokens.weth, - limitAmt: ether(0), + limitAmt: wethReceivedEstimate.mul(995).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataEmpty, swapDataWethToToken: swapDataEmpty, }; - const wethReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); - paymentInfo.limitAmt = wethReceivedEstimate.mul(995).div(1000); // 0.5% slippage + const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await wethToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); @@ -646,14 +641,14 @@ if (process.env.INTEGRATIONTEST) { }); it("Can redeem set token for USDC", async () => { + const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); const paymentInfo: PaymentInfo = { token: addresses.tokens.USDC, - limitAmt: ether(0), + limitAmt: usdcReceivedEstimate.mul(995).div(1000), // 0.5% slippage swapDataTokenToWeth: swapDataUsdcToWeth, swapDataWethToToken: swapDataWethToUsdc, }; - const usdcReceivedEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataWethToUsdc); - paymentInfo.limitAmt = usdcReceivedEstimate.mul(995).div(1000); // 0.5% slippage + const usdcToken = IERC20__factory.connect(paymentInfo.token, owner.wallet); const outputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); const setTokenBalanceBefore = await setToken.balanceOf(owner.address); From 4b38df64dbb67395f3a15be3c56032f4b6d8a736 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 15:05:50 -0400 Subject: [PATCH 52/61] cleanup for pr --- test/integration/arbitrum/flashMintLeveragedExtended.spec.ts | 2 +- test/integration/ethereum/addresses.ts | 2 +- test/integration/ethereum/flashMintDex.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/arbitrum/flashMintLeveragedExtended.spec.ts b/test/integration/arbitrum/flashMintLeveragedExtended.spec.ts index 0c3e1247..abfc7247 100644 --- a/test/integration/arbitrum/flashMintLeveragedExtended.spec.ts +++ b/test/integration/arbitrum/flashMintLeveragedExtended.spec.ts @@ -35,7 +35,7 @@ type SwapData = { }; if (process.env.INTEGRATIONTEST) { - describe.only("FlashMintLeveragedExtended - Integration Test", async () => { + describe("FlashMintLeveragedExtended - Integration Test", async () => { const addresses = PRODUCTION_ADDRESSES; let owner: Account; let deployer: DeployHelper; diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index e1cd525c..eb9b3086 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -46,7 +46,7 @@ export const PRODUCTION_ADDRESSES = { }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", - dai: "0xBF293D5138a2a1BA407B43672643434C43827179", + dai: "0x075e72a5edf65f0a5f44699c7654c1a76941ddc8", weth: "0x2f0b23f53734252bda2277357e97e1517d6b042a", USDC: "0x55fe002aeff02f77364de339a1292923a15844b8", }, diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 2c8b79be..e5f6258a 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -80,7 +80,7 @@ const swapDataWethToUsdc = { }; if (process.env.INTEGRATIONTEST) { - describe("FlashMintDex - Integration Test", async () => { + describe.only("FlashMintDex - Integration Test", async () => { let owner: Account; let deployer: DeployHelper; let legacySetTokenCreator: SetTokenCreator; From c90f6ea0f262642d374618c613909b3ec81f33cc Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 13 Aug 2024 15:07:05 -0400 Subject: [PATCH 53/61] cleanup for pr --- test/integration/ethereum/flashMintHyETHV2.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 89632065e88e573dbb352c891a01a1464238eaf8 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Thu, 15 Aug 2024 12:39:21 -0400 Subject: [PATCH 54/61] no need to calc exact component prices when issuing/redeeming --- contracts/exchangeIssuance/FlashMintDex.sol | 50 ++++++++++----------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 7c88990e..f023a294 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -446,20 +446,18 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @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, - uint256[] memory wethCosts - ) = _getWethCostsPerComponent(_issueParams); + (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 < wethCosts.length; i++) { - if (components[i] == address(WETH)) { - totalWethSpent = totalWethSpent.add(wethCosts[i]); - } else { - uint256 wethSoldForComponent = dexAdapter.swapTokensForExactTokens(componentUnits[i], wethCosts[i], _issueParams.componentSwapData[i]); - totalWethSpent = totalWethSpent.add(wethSoldForComponent); - } + for (uint256 i = 0; i < components.length; i++) { + uint256 wethSold = dexAdapter.swapTokensForExactTokens(componentUnits[i], type(uint256).max, _issueParams.componentSwapData[i]); + totalWethSpent = totalWethSpent.add(wethSold); } } @@ -514,26 +512,24 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * * @param _redeemParams Struct containing addresses, amounts, and swap data for issuance * - * @return totalWethBought Total amount of WETH received after liquidating all SetToken components + * @return totalWethReceived Total amount of WETH received after liquidating all SetToken components */ function _sellComponentsForWeth(IssueRedeemParams memory _redeemParams) internal - returns (uint256 totalWethBought) + returns (uint256 totalWethReceived) { - ( - address[] memory components, - uint256[] memory componentUnits, - uint256[] memory wethReceived - ) = _getWethReceivedPerComponent(_redeemParams); + (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"); - totalWethBought = 0; - for (uint256 i = 0; i < wethReceived.length; i++) { - if (components[i] == address(WETH)) { - totalWethBought = totalWethBought.add(wethReceived[i]); - } else { - uint256 wethSpent = dexAdapter.swapExactTokensForTokens(componentUnits[i], wethReceived[i], _redeemParams.componentSwapData[i]); - totalWethBought = totalWethBought.add(wethSpent); - } + totalWethReceived = 0; + for (uint256 i = 0; i < components.length; i++) { + uint256 wethBought = dexAdapter.swapExactTokensForTokens(componentUnits[i], 0, _redeemParams.componentSwapData[i]); + totalWethReceived = totalWethReceived.add(wethBought); } } From a40e46a80cc717df7da4e277c9768e0091ad83bf Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 19 Aug 2024 11:46:43 -0400 Subject: [PATCH 55/61] reuse deployed dexadapterv2 library; add natspec to constructor --- contracts/exchangeIssuance/FlashMintDex.sol | 10 ++++++++-- test/integration/ethereum/addresses.ts | 1 + test/integration/ethereum/flashMintDex.spec.ts | 1 + utils/deploys/deployExtensions.ts | 6 ++---- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index f023a294..2d249e97 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -105,8 +105,14 @@ contract FlashMintDex is Ownable, ReentrancyGuard { _; } + /** + * 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( - address _weth, IController _setController, IController _indexController, DEXAdapterV2.Addresses memory _dexAddresses @@ -116,7 +122,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { setController = _setController; indexController = _indexController; dexAdapter = _dexAddresses; - WETH = _weth; + WETH = _dexAddresses.weth; } /* ============ External Functions ============ */ diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index eb9b3086..cd6fa87d 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -84,6 +84,7 @@ export const PRODUCTION_ADDRESSES = { eEth1226: "0x7d372819240D14fB477f17b964f95F33BeB4c704", }, }, + dexAdapterV2: "0x88858930B3F1946A5C41a5deD7B5335431d5dE8D", }, set: { controller: "0xa4c8d221d8BB851f83aadd0223a8900A6921A349", diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index e5f6258a..2ca7aac9 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -126,6 +126,7 @@ if (process.env.INTEGRATIONTEST) { addresses.dexes.uniV3.quoter, addresses.dexes.curve.calculator, addresses.dexes.curve.addressProvider, + addresses.dexes.dexAdapterV2, addresses.set.controller, addresses.setFork.controller, ); diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index c48f4463..480e35f5 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -492,11 +492,10 @@ export default class DeployExtensions { uniswapV3QuoterAddress: Address, curveCalculatorAddress: Address, curveAddressProviderAddress: Address, + dexAdapterV2Address: Address, setControllerAddress: Address, indexControllerAddress: Address, ) { - const dexAdapter = await this.deployDEXAdapterV2(); - const linkId = convertLibraryNameToLinkId( "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", ); @@ -504,12 +503,11 @@ export default class DeployExtensions { return await new FlashMintDex__factory( // @ts-ignore { - [linkId]: dexAdapter.address, + [linkId]: dexAdapterV2Address, }, // @ts-ignore this._deployerSigner, ).deploy( - wethAddress, setControllerAddress, indexControllerAddress, { From 9b1708b0167d1b5c97272c17f7775b3c417c015c Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 19 Aug 2024 12:20:00 -0400 Subject: [PATCH 56/61] refactor getter functions --- contracts/exchangeIssuance/FlashMintDex.sol | 54 ++++++++------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 2d249e97..c3f3b49b 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -199,7 +199,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @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 totalEthNeeded Amount of input tokens required to perfrom the issuance + * @return Amount of input tokens required to perform the issuance */ function getIssueExactSet( IssueRedeemParams memory _issueParams, @@ -208,13 +208,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { external returns (uint256) { - uint256 totalEthNeeded = 0; - (,, uint256[] memory wethCosts) = _getWethCostsPerComponent(_issueParams); - - for (uint256 i = 0; i < wethCosts.length; i++) { - totalEthNeeded += wethCosts[i]; - } - return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalEthNeeded); + uint256 totalWethNeeded = _getWethCostsForIssue(_issueParams); + return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalWethNeeded); } /** @@ -226,7 +221,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { * @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 the amount of output tokens expected after performing redemption + * @return Amount of output tokens expected after performing redemption */ function getRedeemExactSet( IssueRedeemParams memory _redeemParams, @@ -235,12 +230,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { external returns (uint256) { - uint256 totalWethReceived = 0; - (,, uint256[] memory wethReceived) = _getWethReceivedPerComponent(_redeemParams); - - for (uint256 i = 0; i < wethReceived.length; i++) { - totalWethReceived += wethReceived[i]; - } + uint256 totalWethReceived = _getWethReceivedForRedeem(_redeemParams); return dexAdapter.getAmountOut(_swapDataWethToOutputToken, totalWethReceived); } @@ -468,15 +458,15 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Calculates the amount of WETH required to buy the components required for issuance. + * Calculates the amount of WETH required to buy all components required for issuance. * - * @param _issueParams Struct containing addresses, amounts, and swap data for issuance + * @param _issueParams Struct containing addresses, amounts, and swap data for issuance * - * @return Tuple of component addresses, units required for issuance, and WETH costs + * @return totalWethCosts Amount of WETH needed to swap into component units required for issuance */ - function _getWethCostsPerComponent(IssueRedeemParams memory _issueParams) + function _getWethCostsForIssue(IssueRedeemParams memory _issueParams) internal - returns (address[] memory, uint256[] memory, uint256[] memory) + returns (uint256 totalWethCosts) { (address[] memory components, uint256[] memory componentUnits) = getRequiredIssuanceComponents( _issueParams.issuanceModule, @@ -487,18 +477,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { require(components.length == _issueParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); - uint256[] memory wethCosts = new uint256[](components.length); + totalWethCosts = 0; for (uint256 i = 0; i < components.length; i++) { if (components[i] == address(WETH)) { - wethCosts[i] = componentUnits[i]; + totalWethCosts += componentUnits[i]; } else { - wethCosts[i] = dexAdapter.getAmountIn( + totalWethCosts += dexAdapter.getAmountIn( _issueParams.componentSwapData[i], componentUnits[i] ); } } - return (components, componentUnits, wethCosts); } /** @@ -540,15 +529,15 @@ contract FlashMintDex is Ownable, ReentrancyGuard { } /** - * Calculates the amount of WETH received for selling off each component after redemption. + * Calculates the amount of WETH received for selling off all components after redemption. * - * @param _redeemParams Struct containing addresses, amounts, and swap data for redemption + * @param _redeemParams Struct containing addresses, amounts, and swap data for redemption * - * @return totalWethBought Tuple of component addresses, units to sell, and WETH received + * @return totalWethReceived Amount of WETH received after swapping all component tokens */ - function _getWethReceivedPerComponent(IssueRedeemParams memory _redeemParams) + function _getWethReceivedForRedeem(IssueRedeemParams memory _redeemParams) internal - returns (address[] memory, uint256[] memory, uint256[] memory) + returns (uint256 totalWethReceived) { (address[] memory components, uint256[] memory componentUnits) = getRequiredRedemptionComponents( _redeemParams.issuanceModule, @@ -559,18 +548,17 @@ contract FlashMintDex is Ownable, ReentrancyGuard { require(components.length == _redeemParams.componentSwapData.length, "FlashMint: INVALID NUMBER OF COMPONENTS IN SWAP DATA"); - uint256[] memory wethReceived = new uint256[](components.length); + totalWethReceived = 0; for (uint256 i = 0; i < components.length; i++) { if (components[i] == address(WETH)) { - wethReceived[i] = componentUnits[i]; + totalWethReceived += componentUnits[i]; } else { - wethReceived[i] = dexAdapter.getAmountOut( + totalWethReceived += dexAdapter.getAmountOut( _redeemParams.componentSwapData[i], componentUnits[i] ); } } - return (components, componentUnits, wethReceived); } /** From 562f2d619273072aa6b5889add120e07e6d688b1 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 19 Aug 2024 13:59:16 -0400 Subject: [PATCH 57/61] add contract-level notice and reference FlashMint SDK --- contracts/exchangeIssuance/FlashMintDex.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index c3f3b49b..c16bab8c 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -34,6 +34,10 @@ 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. + * 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; From dc7a9432898a6771d2324cd6ca00307d97dc51ff Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Mon, 19 Aug 2024 15:36:36 -0400 Subject: [PATCH 58/61] remove require for overspent eth; enforce no external positions --- contracts/exchangeIssuance/FlashMintDex.sol | 4 ++-- test/integration/ethereum/flashMintDex.spec.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index c16bab8c..416178f7 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -259,8 +259,6 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 ethUsedForIssuance = _issueExactSetFromWeth(_issueParams); - require(ethUsedForIssuance <= msg.value, "FlashMint: OVERSPENT ETH"); - uint256 ethReturned = msg.value.sub(ethUsedForIssuance); if (ethReturned > 0) { IWETH(WETH).withdraw(ethReturned); @@ -456,6 +454,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { 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); } @@ -527,6 +526,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { 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); } diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index 2ca7aac9..fb586ce0 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -712,6 +712,13 @@ if (process.env.INTEGRATIONTEST) { await expect( flashMintDex.issueExactSetFromERC20(issueParams, paymentInfoNotEnoughUsdc), ).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), + ).to.be.revertedWith("FlashMint: OVERSPENT WETH"); }); it("should revert when minimum ETH is not received during redemption", async () => { From 7b4605b146d079d3739cfdf798011ee3bc8d24b3 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Tue, 20 Aug 2024 13:00:06 -0400 Subject: [PATCH 59/61] Add minimum threshold for returning unused funds when issuing --- contracts/exchangeIssuance/FlashMintDex.sol | 28 +-- .../integration/ethereum/flashMintDex.spec.ts | 165 +++++++++++++----- 2 files changed, 140 insertions(+), 53 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 416178f7..666b134e 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -240,13 +240,15 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /** * Issues an exact amount of SetTokens for given amount of ETH. - * Any leftover ether is returned to the caller. + * 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 _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 + * @return ethSpent Amount of ETH spent */ - function issueExactSetFromETH(IssueRedeemParams memory _issueParams) + function issueExactSetFromETH(IssueRedeemParams memory _issueParams, uint256 _minEthRefund) external payable isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) @@ -259,26 +261,28 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 ethUsedForIssuance = _issueExactSetFromWeth(_issueParams); - uint256 ethReturned = msg.value.sub(ethUsedForIssuance); - if (ethReturned > 0) { - IWETH(WETH).withdraw(ethReturned); - payable(msg.sender).sendValue(ethReturned); + uint256 leftoverETH = msg.value.sub(ethUsedForIssuance); + if (leftoverETH > _minEthRefund) { + IWETH(WETH).withdraw(leftoverETH); + payable(msg.sender).sendValue(leftoverETH); } - ethSpent = msg.value.sub(ethReturned); + 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. - * Any leftover value is swapped back to the payment token and returned to the caller. + * 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) + function issueExactSetFromERC20(IssueRedeemParams memory _issueParams, PaymentInfo memory _paymentInfo, uint256 _minRefundValueInWeth) external isValidModuleAndSet(_issueParams.issuanceModule, address(_issueParams.setToken)) nonReentrant @@ -292,7 +296,7 @@ contract FlashMintDex is Ownable, ReentrancyGuard { uint256 leftoverWeth = wethReceived.sub(wethSpent); uint256 paymentTokenReturned = 0; - if (leftoverWeth > 0) { + if (leftoverWeth > _minRefundValueInWeth) { paymentTokenReturned = _swapWethForPaymentToken(leftoverWeth, _paymentInfo.token, _paymentInfo.swapDataWethToToken); _paymentInfo.token.safeTransfer(msg.sender, paymentTokenReturned); } diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index fb586ce0..b12c56ea 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -15,6 +15,7 @@ import { SetTokenCreator__factory, FlashMintDex, IERC20__factory, + IWETH, IWETH__factory, IBasicIssuanceModule, IBasicIssuanceModule__factory, @@ -276,7 +277,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const ethBalanceBefore = await owner.wallet.getBalance(); - await flashMintDex.issueExactSetFromETH(issueParams, { value: maxEthIn }); + 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)); @@ -298,7 +299,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await wethToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); + 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)); @@ -321,7 +322,7 @@ if (process.env.INTEGRATIONTEST) { const setTokenBalanceBefore = await setToken.balanceOf(owner.address); const inputTokenBalanceBefore = await usdcToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); + 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)); @@ -332,6 +333,7 @@ if (process.env.INTEGRATIONTEST) { beforeEach(async () => { await flashMintDex.issueExactSetFromETH( issueParams, + 0, { value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }, @@ -531,68 +533,150 @@ if (process.env.INTEGRATIONTEST) { expect(usdcRequired).to.eq(BigNumber.from("26678902800")); }); - it("Can issue set token from ETH", async () => { - const ethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); - const maxEthIn = ethRequiredEstimate.mul(1005).div(1000); // 0.5% slippage + 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; - const setTokenBalanceBefore = await setToken.balanceOf(owner.address); - const ethBalanceBefore = await owner.wallet.getBalance(); - await flashMintDex.issueExactSetFromETH(issueParams, { 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.gt(ethBalanceBefore.sub(maxEthIn)); + 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 WETH", async () => { - const wethRequiredEstimate = await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty); + 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.weth, - limitAmt: wethRequiredEstimate.mul(1005).div(1000), // 0.5% slippage, - swapDataTokenToWeth: swapDataEmpty, - swapDataWethToToken: swapDataEmpty, + token: addresses.tokens.USDC, + limitAmt: usdcRequiredEstimate.mul(1005).div(1000), // 0.5% slippage + swapDataTokenToWeth: swapDataUsdcToWeth, + swapDataWethToToken: swapDataWethToUsdc, }; - const wethToken = IWETH__factory.connect(paymentInfo.token, owner.wallet); - await wethToken.deposit({ value: paymentInfo.limitAmt }); - wethToken.approve(flashMintDex.address, paymentInfo.limitAmt); - + 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 wethToken.balanceOf(owner.address); - await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); - const inputTokenBalanceAfter = await wethToken.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", async () => { + 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); - await flashMintDex.issueExactSetFromERC20(issueParams, paymentInfo); + 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.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); + 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), }, @@ -602,12 +686,12 @@ if (process.env.INTEGRATIONTEST) { 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("8423933102234975071")); + 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("26643397669")); + expect(usdcReceivedEstimate).to.eq(BigNumber.from("26650292996")); }); it("Can redeem set token for ETH", async () => { @@ -680,6 +764,7 @@ if (process.env.INTEGRATIONTEST) { await expect( flashMintDex.issueExactSetFromETH( invalidIssueParams, + 0, { value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }, @@ -692,7 +777,7 @@ if (process.env.INTEGRATIONTEST) { const notEnoughEth = ethRequiredEstimate.div(2); await expect( - flashMintDex.issueExactSetFromETH(invalidIssueParams, { value: notEnoughEth }), + flashMintDex.issueExactSetFromETH(invalidIssueParams, 0, { value: notEnoughEth }), ).to.be.revertedWith("STF"); }); @@ -710,21 +795,21 @@ if (process.env.INTEGRATIONTEST) { swapDataWethToToken: swapDataWethToUsdc, }; await expect( - flashMintDex.issueExactSetFromERC20(issueParams, paymentInfoNotEnoughUsdc), + 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), + 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, { + await flashMintDex.issueExactSetFromETH(issueParams, 0, { value: await flashMintDex.callStatic.getIssueExactSet(issueParams, swapDataEmpty), }); const ethEstimate = await flashMintDex.callStatic.getRedeemExactSet(redeemParams, swapDataEmpty); @@ -752,9 +837,7 @@ if (process.env.INTEGRATIONTEST) { it("issueExactSetFromETH should revert when incompatible set token is provided", async () => { invalidIssueParams.setToken = addresses.tokens.dpi; await expect( - flashMintDex.issueExactSetFromETH(invalidIssueParams, { - value: ether(1), - }), + flashMintDex.issueExactSetFromETH(invalidIssueParams, 0, { value: ether(1) }), ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); }); @@ -772,7 +855,7 @@ if (process.env.INTEGRATIONTEST) { }; invalidIssueParams.issuanceModule = addresses.set.basicIssuanceModule; await expect( - flashMintDex.issueExactSetFromERC20(invalidIssueParams, paymentInfo) + flashMintDex.issueExactSetFromERC20(invalidIssueParams, paymentInfo, 0) ).to.be.revertedWith("FlashMint: INVALID ISSUANCE MODULE OR SET TOKEN"); }); From 0440755118704ca3a9c46af5db15277511b776e3 Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 21 Aug 2024 10:27:37 -0400 Subject: [PATCH 60/61] add note about not requiring off-chain quotes --- contracts/exchangeIssuance/FlashMintDex.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 666b134e..967f7911 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -36,7 +36,8 @@ 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. + * 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 { From de2602274d2541d67479352b546af526a643d41e Mon Sep 17 00:00:00 2001 From: Edward Kim Date: Wed, 21 Aug 2024 12:03:31 -0400 Subject: [PATCH 61/61] update struct comment --- contracts/exchangeIssuance/FlashMintDex.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/exchangeIssuance/FlashMintDex.sol b/contracts/exchangeIssuance/FlashMintDex.sol index 967f7911..16d43583 100644 --- a/contracts/exchangeIssuance/FlashMintDex.sol +++ b/contracts/exchangeIssuance/FlashMintDex.sol @@ -62,8 +62,8 @@ contract FlashMintDex is Ownable, ReentrancyGuard { /* ============ Structs ============ */ struct IssueRedeemParams { - ISetToken setToken; // The address of the SetToken to be issued - uint256 amountSetToken; // The amount of SetTokens to issue + 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