diff --git a/contracts/exchangeIssuance/FlashMintNAV.sol b/contracts/exchangeIssuance/FlashMintNAV.sol new file mode 100644 index 00000000..1d5f49bf --- /dev/null +++ b/contracts/exchangeIssuance/FlashMintNAV.sol @@ -0,0 +1,412 @@ +/* + Copyright 2024 Index Cooperative + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental ABIEncoderV2; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +import { INAVIssuanceModule } from "../interfaces/INAVIssuanceModule.sol"; +import { ISetValuer } from "../interfaces/ISetValuer.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 FlashMintNAV + * @author Index Cooperative + * @notice Part of a family of FlashMint contracts that allows users to issue and redeem SetTokens with a single input/output token (ETH/ERC20). + * Allows the caller to combine a DEX swap and a SetToken issuance or redemption in a single transaction. + * Supports SetTokens that use a NAV Issuance Module, and does not require use of off-chain APIs for swap quotes. + * The SetToken must be configured with a reserve asset that has liquidity on the exchanges supported by the DEXAdapterV2 library. + * + * See the FlashMint SDK for integrating any FlashMint contract (https://github.com/IndexCoop/flash-mint-sdk). + */ +contract FlashMintNAV is Ownable, ReentrancyGuard { + using DEXAdapterV2 for DEXAdapterV2.Addresses; + using Address for address payable; + using SafeMath for uint256; + using PreciseUnitMath for uint256; + using SafeERC20 for IERC20; + using SafeERC20 for ISetToken; + + /* ============ Constants ============== */ + + // Placeholder address to identify ETH where it is treated as if it was an ERC20 token + address constant public ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /* ============ Immutables ============ */ + + address public immutable WETH; + IController public immutable setController; + INAVIssuanceModule public immutable navIssuanceModule; + + /* ============ State Variables ============ */ + + DEXAdapterV2.Addresses public dexAdapter; + + /* ============ 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 _amountSetIssued, // The amount of SetTokens received by the recipient + uint256 _amountInputToken // The amount of input tokens used for issuance + ); + + 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 + ); + + /** + * Initializes the contract with controller, issuance module, and DEXAdapterV2 library addresses. + * + * @param _setController Address of the protocol controller contract + * @param _navIssuanceModule NAV Issuance Module used to issue and redeem SetTokens + * @param _dexAddresses Struct containing addresses for the DEXAdapterV2 library + */ + constructor( + IController _setController, + INAVIssuanceModule _navIssuanceModule, + DEXAdapterV2.Addresses memory _dexAddresses + ) + public + { + setController = _setController; + navIssuanceModule = _navIssuanceModule; + dexAdapter = _dexAddresses; + WETH = _dexAddresses.weth; + } + + /* ============ External Functions ============ */ + + /** + * Withdraw tokens to selected address if they end up in the contract + * + * @param _tokens Addresses of tokens to withdraw, specifiy ETH_ADDRESS to withdraw ETH + * @param _to Address to send the tokens to + */ + function withdrawTokens( + IERC20[] calldata _tokens, + address payable _to + ) external payable onlyOwner { + for (uint256 i = 0; i < _tokens.length; i++) { + if (address(_tokens[i]) == DEXAdapterV2.ETH_ADDRESS) { + _to.sendValue(address(this).balance); + } else { + _tokens[i].safeTransfer(_to, _tokens[i].balanceOf(address(this))); + } + } + } + + receive() external payable { + // required for weth.withdraw() to work properly + require(msg.sender == WETH, "FlashMint: DIRECT DEPOSITS NOT ALLOWED"); + } + + /** + * Runs all the necessary approval functions required before issuing or redeeming + * a SetToken through the NAV Issuance Module. This function needs to be called + * before this smart contract is used with any particular SetToken, and again + * whenever a new reserve asset is added. + * + * @param _setToken Address of the SetToken being initialized + */ + function approveSetToken(ISetToken _setToken) external { + address[] memory reserveAssets = navIssuanceModule.getReserveAssets(address(_setToken)); + for (uint256 i = 0; i < reserveAssets.length; i++) { + _safeApprove(IERC20(reserveAssets[i]), address(navIssuanceModule), type(uint256).max); + } + _safeApprove(IERC20(_setToken), address(navIssuanceModule), type(uint256).max); + } + + /** + * Gets the amount of Set Token expected to be issued given an exact quantity of input token. + * 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 _setToken Address of the Set Token to be issued + * @param _inputToken Address of the token used to pay for issuance. Use WETH address if input token is ETH. + * @param _inputTokenAmount Exact amount of input token to spend + * @param _reserveAssetSwapData Swap data to trade input token for reserve asset. Use empty swap data if input token is the reserve asset. + * + * @return Amount of SetTokens expected to be issued + */ + function getIssueAmount( + ISetToken _setToken, + address _inputToken, + uint256 _inputTokenAmount, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + returns (uint256) + { + address reserveAsset = _getAndValidateReserveAsset(_setToken, _inputToken, _reserveAssetSwapData.path, true); + uint256 reserveAssetReceived = dexAdapter.getAmountOut(_reserveAssetSwapData, _inputTokenAmount); + + return navIssuanceModule.getExpectedSetTokenIssueQuantity( + _setToken, + reserveAsset, + reserveAssetReceived + ); + } + + /** + * Gets the amount of output token expected to be received after redeeming a given quantity of Set Token + * for reserve asset and then swapping the reserve asset for output token. + * 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 _setToken Address of the Set Token to be redeemed + * @param _setTokenAmount Amount of Set Token to be redeemed + * @param _outputToken Address of the token to send to caller after redemption + * @param _reserveAssetSwapData Swap data to trade reserve asset for output token + * + * @return Amount of output tokens expected to be sent to caller + */ + function getRedeemAmountOut( + ISetToken _setToken, + uint256 _setTokenAmount, + address _outputToken, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + returns (uint256) + { + address reserveAsset = _getAndValidateReserveAsset(_setToken, _outputToken, _reserveAssetSwapData.path, false); + uint256 reserveAssetReceived = navIssuanceModule.getExpectedReserveRedeemQuantity( + _setToken, + reserveAsset, + _setTokenAmount + ); + + return dexAdapter.getAmountOut(_reserveAssetSwapData, reserveAssetReceived); + } + + /** + * Issues a minimum amount of SetTokens for an exact amount of ETH. + * + * @param _setToken Address of the SetToken to be issued + * @param _minSetTokenAmount Minimum amount of SetTokens to be issued + * @param _reserveAssetSwapData Swap data to trade WETH for reserve asset + */ + function issueSetFromExactETH( + ISetToken _setToken, + uint256 _minSetTokenAmount, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + payable + nonReentrant + { + require(msg.value > 0, "FlashMint: NO ETH SENT"); + IWETH(WETH).deposit{value: msg.value}(); + address reserveAsset = _getAndValidateReserveAsset(_setToken, WETH, _reserveAssetSwapData.path, true); + uint256 reserveAssetReceived = dexAdapter.swapExactTokensForTokens(msg.value, 0, _reserveAssetSwapData); + uint256 setTokenBalanceBefore = _setToken.balanceOf(msg.sender); + + navIssuanceModule.issue( + _setToken, + reserveAsset, + reserveAssetReceived, + _minSetTokenAmount, + msg.sender + ); + + uint256 setTokenIssued = _setToken.balanceOf(msg.sender).sub(setTokenBalanceBefore); + emit FlashMint(msg.sender, _setToken, IERC20(ETH_ADDRESS), setTokenIssued, msg.value); + } + + /** + * Issues a minimum amount of SetTokens for an exact amount of ERC20. + * + * @param _setToken Address of the SetToken to issue + * @param _minSetTokenAmount Minimum amount of SetTokens to issue + * @param _inputToken Address of token used to pay for issuance + * @param _inputTokenAmount Amount of input token to spend + * @param _reserveAssetSwapData Swap data to trade input token for reserve asset. Can use empty swap data if input token is reserve asset. + */ + function issueSetFromExactERC20( + ISetToken _setToken, + uint256 _minSetTokenAmount, + IERC20 _inputToken, + uint256 _inputTokenAmount, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + nonReentrant + { + address reserveAsset = _getAndValidateReserveAsset(_setToken, address(_inputToken), _reserveAssetSwapData.path, true); + _inputToken.safeTransferFrom(msg.sender, address(this), _inputTokenAmount); + uint256 reserveAssetReceived = dexAdapter.swapExactTokensForTokens(_inputTokenAmount, 0, _reserveAssetSwapData); + uint256 setTokenBalanceBefore = _setToken.balanceOf(msg.sender); + + navIssuanceModule.issue( + _setToken, + reserveAsset, + reserveAssetReceived, + _minSetTokenAmount, + msg.sender + ); + + uint256 setTokenIssued = _setToken.balanceOf(msg.sender).sub(setTokenBalanceBefore); + emit FlashMint(msg.sender, _setToken, IERC20(ETH_ADDRESS), setTokenIssued, _inputTokenAmount); + } + + /** + * 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 to redeem + * @param _setTokenAmount Amount of SetTokens to redeem + * @param _minEthAmount Minimum amount of ETH to be received by caller + * @param _reserveAssetSwapData Swap data to trade reserve asset for WETH + */ + function redeemExactSetForETH( + ISetToken _setToken, + uint256 _setTokenAmount, + uint256 _minEthAmount, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + nonReentrant + { + address reserveAsset = _getAndValidateReserveAsset(_setToken, WETH, _reserveAssetSwapData.path, false); + uint256 reserveAssetBalanceBefore = IERC20(reserveAsset).balanceOf(address(this)); + _setToken.safeTransferFrom(msg.sender, address(this), _setTokenAmount); + + navIssuanceModule.redeem( + _setToken, + reserveAsset, + _setTokenAmount, + 0, + address(this) + ); + + uint256 reserveAssetReceived = IERC20(reserveAsset).balanceOf(address(this)).sub(reserveAssetBalanceBefore); + uint256 wethReceived = dexAdapter.swapExactTokensForTokens(reserveAssetReceived, 0, _reserveAssetSwapData); + require(wethReceived >= _minEthAmount, "FlashMint: NOT ENOUGH ETH RECEIVED"); + + IWETH(WETH).withdraw(wethReceived); + payable(msg.sender).sendValue(wethReceived); + + emit FlashRedeem(msg.sender, _setToken, IERC20(ETH_ADDRESS), _setTokenAmount, wethReceived); + } + + /** + * 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 to redeem + * @param _setTokenAmount Amount of SetTokens to redeem + * @param _outputToken Address of the token to be received by caller + * @param _minOutputTokenAmount Minimum amount of output token to be received by caller + * @param _reserveAssetSwapData Swap data to trade reserve asset for output token. Can use empty swap data if output token is reserve asset. + */ + function redeemExactSetForERC20( + ISetToken _setToken, + uint256 _setTokenAmount, + IERC20 _outputToken, + uint256 _minOutputTokenAmount, + DEXAdapterV2.SwapData memory _reserveAssetSwapData + ) + external + nonReentrant + { + address reserveAsset = _getAndValidateReserveAsset(_setToken, address(_outputToken), _reserveAssetSwapData.path, false); + uint256 reserveAssetBalanceBefore = IERC20(reserveAsset).balanceOf(address(this)); + _setToken.safeTransferFrom(msg.sender, address(this), _setTokenAmount); + + navIssuanceModule.redeem( + _setToken, + reserveAsset, + _setTokenAmount, + 0, + address(this) + ); + + uint256 reserveAssetReceived = IERC20(reserveAsset).balanceOf(address(this)).sub(reserveAssetBalanceBefore); + uint256 outputTokenReceived = dexAdapter.swapExactTokensForTokens(reserveAssetReceived, 0, _reserveAssetSwapData); + require(outputTokenReceived >= _minOutputTokenAmount, "FlashMint: NOT ENOUGH OUTPUT TOKEN RECEIVED"); + + _outputToken.safeTransfer(msg.sender, outputTokenReceived); + + emit FlashRedeem(msg.sender, _setToken, _outputToken, _setTokenAmount, outputTokenReceived); + } + + /* ============ Internal Functions ============ */ + + /** + * Validates the reserve asset and swap path for issuance or redemption. + * + * @param _setToken Address of SetToken being issued or redeemed + * @param _paymentToken Address of input token if issuance, or output token if redemption + * @param _path Array of token addresses representing the swap path + * @param _isIssuance bool indicating issuance or redemption + */ + function _getAndValidateReserveAsset( + ISetToken _setToken, + address _paymentToken, + address[] memory _path, + bool _isIssuance + ) + internal + view + returns(address) + { + address reserveAsset; + if (_path.length > 0) { + if (_isIssuance) { + require(_path[0] == _paymentToken, "FLASHMINT: FIRST ADDRESS IN SWAP PATH MUST BE INPUT TOKEN"); + reserveAsset = _path[_path.length - 1]; + } else { + require(_path[_path.length - 1] == address(_paymentToken), "FLASHMINT: LAST ADDRESS IN SWAP PATH MUST BE OUTPUT TOKEN"); + reserveAsset = _path[0]; + } + } else { + reserveAsset = address(_paymentToken); + } + require(navIssuanceModule.isReserveAsset(_setToken, reserveAsset), "FLASHMINT: INVALID RESERVE ASSET"); + return reserveAsset; + } + + /** + * 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); + } + } +} diff --git a/contracts/interfaces/INAVIssuanceHook.sol b/contracts/interfaces/INAVIssuanceHook.sol new file mode 100644 index 00000000..c880a42c --- /dev/null +++ b/contracts/interfaces/INAVIssuanceHook.sol @@ -0,0 +1,39 @@ +/* + Copyright 2020 Set Labs Inc. + + 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; + +import { ISetToken } from "./ISetToken.sol"; + +interface INAVIssuanceHook { + function invokePreIssueHook( + ISetToken _setToken, + address _reserveAsset, + uint256 _reserveAssetQuantity, + address _sender, + address _to + ) + external; + + function invokePreRedeemHook( + ISetToken _setToken, + uint256 _redeemQuantity, + address _sender, + address _to + ) + external; +} diff --git a/contracts/interfaces/INAVIssuanceModule.sol b/contracts/interfaces/INAVIssuanceModule.sol new file mode 100644 index 00000000..e889c5c0 --- /dev/null +++ b/contracts/interfaces/INAVIssuanceModule.sol @@ -0,0 +1,57 @@ +/* + Copyright 2024 Index Coop + + 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; + +import { ISetToken } from "./ISetToken.sol"; + +interface INAVIssuanceModule { + function issue( + ISetToken _setToken, + address _reserveAsset, + uint256 _reserveAssetQuantity, + uint256 _minSetTokenReceiveQuantity, + address _to + ) external; + + function redeem( + ISetToken _setToken, + address _reserveAsset, + uint256 _setTokenQuantity, + uint256 _minReserveReceiveQuantity, + address _to + ) external; + + function isReserveAsset( + ISetToken _setToken, + address _asset + ) external view returns(bool); + + function getReserveAssets(address _setToken) external view returns (address[] memory); + + function getExpectedSetTokenIssueQuantity( + ISetToken _setToken, + address _reserveAsset, + uint256 _reserveAssetQuantity + ) external view returns (uint256); + + function getExpectedReserveRedeemQuantity( + ISetToken _setToken, + address _reserveAsset, + uint256 _setTokenQuantity + ) external view returns (uint256); +} diff --git a/test/integration/arbitrum/withdrawTokens.spec.ts b/test/integration/arbitrum/withdrawTokens.spec.ts index 2ff87240..51df64fe 100644 --- a/test/integration/arbitrum/withdrawTokens.spec.ts +++ b/test/integration/arbitrum/withdrawTokens.spec.ts @@ -4,7 +4,7 @@ import { impersonateAccount, setBlockNumber } from "@utils/test/testingUtils"; import { WithdrawTokens__factory } from "../../../typechain"; if (process.env.INTEGRATIONTEST) { - describe.only("WithdrawTokens", function () { + describe("WithdrawTokens", function () { const deployerAddress = "0x37e6365d4f6aE378467b0e24c9065Ce5f06D70bF"; let deployerSigner: Signer; diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index cd6fa87d..70b8fdf2 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -43,6 +43,7 @@ export const PRODUCTION_ADDRESSES = { osEth: "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38", comp: "0xc00e94Cb662C3520282E6f5717214004A7f26888", dpi: "0x1494CA1F11D487c2bBe4543E90080AeBa4BA3C2b", + usdt: "0xdAC17F958D2ee523a2206206994597C13D831ec7", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", diff --git a/test/integration/ethereum/flashMintDex.spec.ts b/test/integration/ethereum/flashMintDex.spec.ts index b12c56ea..cfd27bb3 100644 --- a/test/integration/ethereum/flashMintDex.spec.ts +++ b/test/integration/ethereum/flashMintDex.spec.ts @@ -81,7 +81,7 @@ 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; diff --git a/test/integration/ethereum/flashMintNAV.spec.ts b/test/integration/ethereum/flashMintNAV.spec.ts new file mode 100644 index 00000000..f283d686 --- /dev/null +++ b/test/integration/ethereum/flashMintNAV.spec.ts @@ -0,0 +1,548 @@ +import "module-alias/register"; +import { Account, Address, CustomOracleNAVIssuanceSettings } from "@utils/types"; +import DeployHelper from "@utils/deploys"; +import { getAccounts, getWaffleExpect } from "@utils/index"; +import { addSnapshotBeforeRestoreAfterEach, setBlockNumber } from "@utils/test/testingUtils"; +import { ethers } from "hardhat"; +import { BigNumber } from "ethers"; +import { + SetToken, + ERC4626Oracle, + FlashMintNAV, + IERC20, + IERC20__factory, +} from "../../../typechain"; +import { PRODUCTION_ADDRESSES } from "./addresses"; +import { ADDRESS_ZERO, MAX_UINT_256, ZERO } from "@utils/constants"; +import { ether, usdc } from "@utils/index"; +import { impersonateAccount } from "./utils"; +import { SetFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +enum Exchange { + None, + Sushiswap, + Quickswap, + UniV3, + Curve, +} + +type SwapData = { + path: Address[]; + fees: number[]; + pool: Address; + exchange: Exchange; +}; + +const addresses = PRODUCTION_ADDRESSES; + +const tokenAddresses = { + usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + aEthUSDC: "0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c", + cUSDCv3: "0xc3d688B66703497DAA19211EEdff47f25384cdc3", + aUSDC: "0xBcca60bB61934080951369a648Fb03DF4F96263C", + gtUSDC: "0xdd0f28e19C1780eb6396170735D45153D261490d", +}; + +const whales = { + usdc: "0x075e72a5eDf65F0A5f44699c7654C1a76941Ddc8", + justin_sun: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", // aEthUSDC + wan_liang: "0xCcb12611039c7CD321c0F23043c841F1d97287A5", // cUSDCv3 + mane_lee: "0xBF370B6E9d97D928497C2f2d72FD74f4D9ca5825", // aUSDC + morpho_seeding: "0x6ABfd6139c7C3CC270ee2Ce132E309F59cAaF6a2", // gtUSDC, + weth: "0x4F4495243837681061C4743b74B3eEdf548D56A5", + dai: "0x3B5fb9d9da3546e9CE6E5AA3CCEca14C8D20041e", + usdt: "0xEEA81C4416d71CeF071224611359F6F99A4c4294", +}; + +const swapDataEmpty: SwapData = { + exchange: Exchange.None, + fees: [], + path: [], + pool: ADDRESS_ZERO, +}; + +const swapDataUsdcToWeth: SwapData = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.USDC, addresses.tokens.weth], + pool: ADDRESS_ZERO, +}; + +const swapDataWethToUsdc = { + exchange: Exchange.UniV3, + fees: [500], + path: [addresses.tokens.weth, addresses.tokens.USDC], + pool: ADDRESS_ZERO, +}; + +if (process.env.INTEGRATIONTEST) { + describe.only("FlashMintNAV - Integration Test", async () => { + let owner: Account; + let deployer: DeployHelper; + let setV2Setup: SetFixture; + let erc4626Oracle: ERC4626Oracle; + let setToken: SetToken; + let flashMintNAV: FlashMintNAV; + let usdc_erc20: IERC20; + let aEthUSDC_erc20: IERC20; + let cUSDCv3_erc20: IERC20; + let aUSDC_erc20: IERC20; + let gtUSDC_erc20: IERC20; + + setBlockNumber(20528609, true); + + before(async () => { + [owner] = await getAccounts(); + + // Sytem setup + deployer = new DeployHelper(owner.wallet); + setV2Setup = new SetFixture(ethers.provider, owner.address); + await setV2Setup.initialize(); + + // Token setup + usdc_erc20 = IERC20__factory.connect(addresses.tokens.USDC, owner.wallet); + aEthUSDC_erc20 = IERC20__factory.connect(tokenAddresses.aEthUSDC, owner.wallet); + cUSDCv3_erc20 = IERC20__factory.connect(tokenAddresses.cUSDCv3, owner.wallet); + aUSDC_erc20 = IERC20__factory.connect(tokenAddresses.aUSDC, owner.wallet); + gtUSDC_erc20 = IERC20__factory.connect(tokenAddresses.gtUSDC, owner.wallet); + + // Oracle setup + await setV2Setup.priceOracle.editMasterQuoteAsset(tokenAddresses.usdc); + + const preciseUnitOracle = await deployer.setV2.deployPreciseUnitOracle("Rebasing USDC Oracle"); + await setV2Setup.priceOracle.addAdapter(preciseUnitOracle.address); + await setV2Setup.priceOracle.addPair(tokenAddresses.usdc, tokenAddresses.usdc, preciseUnitOracle.address); + await setV2Setup.priceOracle.addPair(tokenAddresses.aEthUSDC, tokenAddresses.usdc, preciseUnitOracle.address); + await setV2Setup.priceOracle.addPair(tokenAddresses.cUSDCv3, tokenAddresses.usdc, preciseUnitOracle.address); + await setV2Setup.priceOracle.addPair(tokenAddresses.aUSDC, tokenAddresses.usdc, preciseUnitOracle.address); + erc4626Oracle = await deployer.setV2.deployERC4626Oracle( + tokenAddresses.gtUSDC, + usdc(1), + "gtUSDC - USDC Calculated Oracle", + ); + await setV2Setup.priceOracle.addAdapter(erc4626Oracle.address); + await setV2Setup.priceOracle.addPair(tokenAddresses.gtUSDC, tokenAddresses.usdc, erc4626Oracle.address); + + // SetToken setup + setToken = await setV2Setup.createSetToken( + [ + tokenAddresses.usdc, + tokenAddresses.aEthUSDC, + tokenAddresses.cUSDCv3, + tokenAddresses.aUSDC, + tokenAddresses.gtUSDC, + ], + [ + usdc(20), + usdc(20), + usdc(20), + usdc(20), + ether(20), + ], + [ + setV2Setup.debtIssuanceModuleV3.address, + setV2Setup.rebasingComponentModule.address, + setV2Setup.navIssuanceModule.address, + ] + ); + + // Initialize Modules + await setV2Setup.debtIssuanceModuleV3.initialize( + setToken.address, + ZERO, + ZERO, + ZERO, + owner.address, + ADDRESS_ZERO + ); + + await setV2Setup.rebasingComponentModule.initialize( + setToken.address, + [tokenAddresses.aEthUSDC, tokenAddresses.cUSDCv3, tokenAddresses.aUSDC] + ); + + const navIssuanceSettings = { + managerIssuanceHook: setV2Setup.rebasingComponentModule.address, + managerRedemptionHook: setV2Setup.rebasingComponentModule.address, + setValuer: ADDRESS_ZERO, + reserveAssets: [tokenAddresses.usdc], + feeRecipient: owner.address, + managerFees: [ether(0.001), ether(0.002)], + maxManagerFee: ether(0.02), + premiumPercentage: ether(0.01), + maxPremiumPercentage: ether(0.1), + minSetTokenSupply: ether(5), + } as CustomOracleNAVIssuanceSettings; + + await setV2Setup.navIssuanceModule.initialize( + setToken.address, + navIssuanceSettings + ); + + // Issue initial units via the debt issuance module V3 + const justin_sun = await impersonateAccount(whales.justin_sun); + const wan_liang = await impersonateAccount(whales.wan_liang); + const mane_lee = await impersonateAccount(whales.mane_lee); + const morpho_seeding = await impersonateAccount(whales.morpho_seeding); + await usdc_erc20.connect(justin_sun).transfer(owner.address, usdc(1000)); + await aEthUSDC_erc20.connect(justin_sun).transfer(owner.address, usdc(10000)); + await cUSDCv3_erc20.connect(wan_liang).transfer(owner.address, usdc(10000)); + await aUSDC_erc20.connect(mane_lee).transfer(owner.address, usdc(10000)); + await gtUSDC_erc20.connect(morpho_seeding).transfer(owner.address, ether(10000)); + await usdc_erc20.connect(owner.wallet).approve(setV2Setup.debtIssuanceModuleV3.address, MAX_UINT_256); + await aEthUSDC_erc20.connect(owner.wallet).approve(setV2Setup.debtIssuanceModuleV3.address, MAX_UINT_256); + await cUSDCv3_erc20.connect(owner.wallet).approve(setV2Setup.debtIssuanceModuleV3.address, MAX_UINT_256); + await aUSDC_erc20.connect(owner.wallet).approve(setV2Setup.debtIssuanceModuleV3.address, MAX_UINT_256); + await gtUSDC_erc20.connect(owner.wallet).approve(setV2Setup.debtIssuanceModuleV3.address, MAX_UINT_256); + await setV2Setup.debtIssuanceModuleV3.connect(owner.wallet).issue(setToken.address, ether(10), owner.address); + + // Deploy FlashMintNAV + flashMintNAV = await deployer.extensions.deployFlashMintNAV( + addresses.tokens.weth, + addresses.dexes.uniV2.router, + addresses.dexes.sushiswap.router, + addresses.dexes.uniV3.router, + addresses.dexes.uniV3.quoter, + addresses.dexes.curve.calculator, + addresses.dexes.curve.addressProvider, + addresses.dexes.dexAdapterV2, + addresses.setFork.controller, + setV2Setup.navIssuanceModule.address, + ); + + await flashMintNAV.approveSetToken(setToken.address); + await setToken.connect(owner.wallet).approve(flashMintNAV.address, MAX_UINT_256); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#issue", () => { + let subjectInputToken: Address; + let subjectInputTokenAmount: BigNumber; + let subjectMinSetTokenOut: BigNumber; + let subjectSwapData: SwapData; + + async function subjectQuote() { + return await flashMintNAV.callStatic.getIssueAmount( + setToken.address, + subjectInputToken, + subjectInputTokenAmount, + subjectSwapData + ); + } + + context("issueSetFromExactETH", async () => { + subjectInputToken = addresses.tokens.weth; + subjectInputTokenAmount = ether(1); + subjectSwapData = swapDataWethToUsdc; + + async function subject() { + return await flashMintNAV.issueSetFromExactETH( + setToken.address, + subjectMinSetTokenOut, + subjectSwapData, + { value: subjectInputTokenAmount } + ); + } + + it("can estimate the amount of SetToken issued for a given amount of ETH", async () => { + const setTokenOutEstimate = await subjectQuote(); + expect(setTokenOutEstimate).to.eq(BigNumber.from("25460056235206711599")); + }); + + it("should issue SetToken with ETH", async () => { + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(995).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.gte(setTokenBalanceBefore.add(subjectMinSetTokenOut)); + }); + + it("should revert if less than minSetTokenAmount received", async () => { + subjectInputTokenAmount = ether(0.01); + await expect(subject()).to.be.revertedWith("Must be greater than min SetToken"); + }); + }); + + context("issueSetFromExactERC20", async () => { + let subjectWhale: Address; + + async function getInputTokens() { + const whaleSigner = await impersonateAccount(subjectWhale); + const erc20 = IERC20__factory.connect(subjectInputToken, owner.wallet); + await erc20.connect(whaleSigner).transfer(owner.address, subjectInputTokenAmount); + await erc20.approve(flashMintNAV.address, subjectInputTokenAmount); + } + + async function subject() { + return await flashMintNAV.issueSetFromExactERC20( + setToken.address, + subjectMinSetTokenOut, + subjectInputToken, + subjectInputTokenAmount, + subjectSwapData + ); + } + + it("can estimate the amount of SetToken issued for a given amount of ERC20", async () => { + subjectInputToken = addresses.tokens.weth; + subjectInputTokenAmount = ether(1); + subjectSwapData = swapDataWethToUsdc; + + const setTokenOutEstimate = await subjectQuote(); + expect(setTokenOutEstimate).to.eq(BigNumber.from("25460056235206711599")); + }); + + it("should issue SetToken with USDC (reserve asset)", async () => { + subjectInputToken = addresses.tokens.USDC; + subjectInputTokenAmount = usdc(100); + subjectWhale = whales.usdc; + subjectSwapData = swapDataEmpty; + + await getInputTokens(); + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(995).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.gte(setTokenBalanceBefore.add(subjectMinSetTokenOut)); + }); + + it("should issue SetToken with WETH", async () => { + subjectInputToken = addresses.tokens.weth; + subjectInputTokenAmount = ether(1); + subjectWhale = whales.weth; + subjectSwapData = swapDataWethToUsdc; + + await getInputTokens(); + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(995).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.gte(setTokenBalanceBefore.add(subjectMinSetTokenOut)); + }); + + it("should issue SetToken with DAI", async () => { + subjectInputToken = addresses.tokens.dai; + subjectInputTokenAmount = ether(1000); + subjectWhale = whales.dai; + subjectSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.dai, addresses.tokens.USDC], + pool: ADDRESS_ZERO, + }; + + await getInputTokens(); + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(995).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.gte(setTokenBalanceBefore.add(subjectMinSetTokenOut)); + }); + + it("should issue SetToken with USDT", async () => { + subjectInputToken = addresses.tokens.usdt; + subjectInputTokenAmount = usdc(1000); // USDT and USDC both have 6 decimals + subjectWhale = whales.usdt; + subjectSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.usdt, addresses.tokens.USDC], + pool: ADDRESS_ZERO, + }; + + await getInputTokens(); + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(995).div(1000); // 0.5% slippage + + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.gte(setTokenBalanceBefore.add(subjectMinSetTokenOut)); + }); + + it("should revert if less than minSetTokenAmount received", async () => { + subjectInputToken = addresses.tokens.USDC; + subjectInputTokenAmount = usdc(100); + subjectSwapData = swapDataEmpty; + subjectWhale = whales.usdc; + + await getInputTokens(); + const setTokenOutEstimate = await subjectQuote(); + subjectMinSetTokenOut = setTokenOutEstimate.mul(1005).div(1000); // 0.5% too high + await expect(subject()).to.be.revertedWith("Must be greater than min SetToken"); + }); + }); + }); + + describe("#redeem", () => { + let subjectOutputToken: Address; + let subjectMinOutputTokenAmount: BigNumber; + let subjectSwapData: SwapData; + + const setTokenRedeemAmount = ether(23); + const ethAmountIn = ether(1); + + beforeEach(async () => { + await flashMintNAV.issueSetFromExactETH( + setToken.address, + setTokenRedeemAmount, + swapDataWethToUsdc, + { value: ethAmountIn } + ); + }); + + async function subjectQuote() { + return await flashMintNAV.callStatic.getRedeemAmountOut( + setToken.address, + setTokenRedeemAmount, + subjectOutputToken, + subjectSwapData + ); + } + + context("redeemExactSetForETH", async () => { + subjectOutputToken = addresses.tokens.weth; + subjectSwapData = swapDataUsdcToWeth; + + async function subject() { + return await flashMintNAV.redeemExactSetForETH( + setToken.address, + setTokenRedeemAmount, + subjectMinOutputTokenAmount, + subjectSwapData + ); + } + + it("can estimate the amount of output token received for redeeming a given amount of Set Token", async () => { + const outputTokenAmount = await subjectQuote(); + expect(outputTokenAmount).to.eq(BigNumber.from("881862431628006214")); + }); + + it("should redeem SetToken for ETH", async () => { + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(995).div(1000); // 0.5% slippage + + const ethBalanceBefore = await owner.wallet.getBalance(); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const ethBalanceAfter = await owner.wallet.getBalance(); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenRedeemAmount)); + expect(ethBalanceAfter).to.gte(ethBalanceBefore.add(subjectMinOutputTokenAmount)); + }); + + it("should revert if less than minEthAmount received", async () => { + const ethOutEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = ethOutEstimate.mul(1005).div(1000); // 0.5% too high + await expect(subject()).to.be.revertedWith("FlashMint: NOT ENOUGH ETH RECEIVED"); + }); + }); + + context("redeemExactSetForERC20", async () => { + async function subject() { + return await flashMintNAV.redeemExactSetForERC20( + setToken.address, + setTokenRedeemAmount, + subjectOutputToken, + subjectMinOutputTokenAmount, + subjectSwapData + ); + } + + it("should redeem SetToken for USDC (reserve asset)", async () => { + subjectOutputToken = addresses.tokens.USDC; + subjectSwapData = swapDataEmpty; + + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(995).div(1000); // 0.5% slippage + + const usdcBalanceBefore = await usdc_erc20.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const usdcBalanceAfter = await usdc_erc20.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenRedeemAmount)); + expect(usdcBalanceAfter).to.gte(usdcBalanceBefore.add(subjectMinOutputTokenAmount)); + }); + + it("should redeem SetToken for WETH", async () => { + subjectOutputToken = addresses.tokens.weth; + subjectSwapData = swapDataUsdcToWeth; + + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(995).div(1000); // 0.5% slippage + const wethToken = IERC20__factory.connect(subjectOutputToken, owner.wallet); + + const wethBalanceBefore = await wethToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const wethBalanceAfter = await wethToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenRedeemAmount)); + expect(wethBalanceAfter).to.gte(wethBalanceBefore.add(subjectMinOutputTokenAmount)); + }); + + it("should redeem SetToken for DAI", async () => { + subjectOutputToken = addresses.tokens.dai; + subjectSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.USDC, addresses.tokens.dai], + pool: ADDRESS_ZERO, + }; + + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(995).div(1000); // 0.5% slippage + const daiToken = IERC20__factory.connect(subjectOutputToken, owner.wallet); + + const daiBalanceBefore = await daiToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const daiBalanceAfter = await daiToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenRedeemAmount)); + expect(daiBalanceAfter).to.gte(daiBalanceBefore.add(subjectMinOutputTokenAmount)); + }); + + it("should redeem SetToken for USDT", async () => { + subjectOutputToken = addresses.tokens.usdt; + subjectSwapData = { + exchange: Exchange.UniV3, + fees: [100], + path: [addresses.tokens.USDC, addresses.tokens.usdt], + pool: ADDRESS_ZERO, + }; + + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(995).div(1000); // 0.5% slippage + const usdtToken = IERC20__factory.connect(subjectOutputToken, owner.wallet); + + const usdtBalanceBefore = await usdtToken.balanceOf(owner.address); + const setTokenBalanceBefore = await setToken.balanceOf(owner.address); + await subject(); + const setTokenBalanceAfter = await setToken.balanceOf(owner.address); + const usdtBalanceAfter = await usdtToken.balanceOf(owner.address); + expect(setTokenBalanceAfter).to.eq(setTokenBalanceBefore.sub(setTokenRedeemAmount)); + expect(usdtBalanceAfter).to.gte(usdtBalanceBefore.add(subjectMinOutputTokenAmount)); + }); + + it("should revert if less than minOutputTokenAmount received", async () => { + const outputAmountEstimate = await subjectQuote(); + subjectMinOutputTokenAmount = outputAmountEstimate.mul(1005).div(1000); // 0.5% too high + await expect(subject()).to.be.revertedWith("FlashMint: NOT ENOUGH OUTPUT TOKEN RECEIVED"); + }); + }); + }); + }); +} diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index a27e360a..d2bd7bee 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -8,6 +8,7 @@ export { BaseManager } from "../../typechain/BaseManager"; export { BaseManagerV2 } from "../../typechain/BaseManagerV2"; export { ChainlinkAggregatorV3Mock } from "../../typechain/ChainlinkAggregatorV3Mock"; export { ConstantPriceAdapter } from "../../typechain/ConstantPriceAdapter"; +export { CustomOracleNavIssuanceModule } from "../../typechain/CustomOracleNavIssuanceModule"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { BasicIssuanceModule } from "../../typechain/BasicIssuanceModule"; diff --git a/utils/deploys/deployExtensions.ts b/utils/deploys/deployExtensions.ts index f8b51c4b..4d314749 100644 --- a/utils/deploys/deployExtensions.ts +++ b/utils/deploys/deployExtensions.ts @@ -56,6 +56,7 @@ 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 { FlashMintNAV__factory } from "../../typechain/factories/FlashMintNAV__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"; @@ -486,6 +487,7 @@ export default class DeployExtensions { swapTarget, ); } + public async deployFlashMintDex( wethAddress: Address, quickRouterAddress: Address, @@ -524,6 +526,44 @@ export default class DeployExtensions { ); } + public async deployFlashMintNAV( + wethAddress: Address, + quickRouterAddress: Address, + sushiRouterAddress: Address, + uniV3RouterAddress: Address, + uniswapV3QuoterAddress: Address, + curveCalculatorAddress: Address, + curveAddressProviderAddress: Address, + dexAdapterV2Address: Address, + indexControllerAddress: Address, + navIssuanceModuleAddress: Address + ) { + const linkId = convertLibraryNameToLinkId( + "contracts/exchangeIssuance/DEXAdapterV2.sol:DEXAdapterV2", + ); + + return await new FlashMintNAV__factory( + // @ts-ignore + { + [linkId]: dexAdapterV2Address, + }, + // @ts-ignore + this._deployerSigner, + ).deploy( + indexControllerAddress, + navIssuanceModuleAddress, + { + quickRouter: quickRouterAddress, + sushiRouter: sushiRouterAddress, + uniV3Router: uniV3RouterAddress, + uniV3Quoter: uniswapV3QuoterAddress, + curveAddressProvider: curveAddressProviderAddress, + curveCalculator: curveCalculatorAddress, + weth: wethAddress, + }, + ); + } + public async deployFlashMintNotional( wethAddress: Address, setControllerAddress: Address, diff --git a/utils/fixtures/setFixture.ts b/utils/fixtures/setFixture.ts index 9e7ee07b..098e53f5 100644 --- a/utils/fixtures/setFixture.ts +++ b/utils/fixtures/setFixture.ts @@ -16,6 +16,7 @@ import { IntegrationRegistry, OracleMock, PriceOracle, + RebasingComponentModule, SetToken, SetTokenCreator, SetValuer, @@ -66,6 +67,7 @@ export class SetFixture { public wrapModule: WrapModule; public wrapModuleV2: WrapModuleV2; public slippageIssuanceModule: SlippageIssuanceModule; + public rebasingComponentModule: RebasingComponentModule; public weth: WETH9; public usdc: StandardTokenMock; @@ -106,6 +108,8 @@ export class SetFixture { this.airdropModule = await this._deployer.setV2.deployAirdropModule(this.controller.address); this.slippageIssuanceModule = await this._deployer.setV2.deploySlippageIssuanceModule(this.controller.address); this.debtIssuanceModuleV3 = await this._deployer.setV2.deployDebtIssuanceModuleV3(this.controller.address, 10); + this.rebasingComponentModule = await this._deployer.setV2.deployRebasingComponentModule(this.controller.address); + await this.initializeStandardComponents(); @@ -153,6 +157,7 @@ export class SetFixture { this.slippageIssuanceModule.address, this.navIssuanceModule.address, this.debtIssuanceModuleV3.address, + this.rebasingComponentModule.address, ]; await this.controller.initialize(