diff --git a/contracts/exchangeIssuance/FlashMintLeveraged.sol b/contracts/exchangeIssuance/FlashMintLeveraged.sol index 6d652b23..6f431a78 100644 --- a/contracts/exchangeIssuance/FlashMintLeveraged.sol +++ b/contracts/exchangeIssuance/FlashMintLeveraged.sol @@ -347,6 +347,7 @@ contract FlashMintLeveraged is ReentrancyGuard, IFlashLoanRecipient{ DEXAdapter.SwapData memory _swapDataInputToken ) external + virtual nonReentrant { _initiateIssuance( @@ -374,6 +375,7 @@ contract FlashMintLeveraged is ReentrancyGuard, IFlashLoanRecipient{ DEXAdapter.SwapData memory _swapDataInputToken ) external + virtual payable nonReentrant { diff --git a/contracts/exchangeIssuance/FlashMintLeveragedExtended.sol b/contracts/exchangeIssuance/FlashMintLeveragedExtended.sol index 82ead8c9..e919271b 100644 --- a/contracts/exchangeIssuance/FlashMintLeveragedExtended.sol +++ b/contracts/exchangeIssuance/FlashMintLeveragedExtended.sol @@ -28,6 +28,7 @@ import { ISetToken } from "../interfaces/ISetToken.sol"; import { IWETH } from "../interfaces/IWETH.sol"; + /** * @title FlashMintLeveragedExtended * @author Index Coop @@ -61,6 +62,76 @@ contract FlashMintLeveragedExtended is FlashMintLeveraged { { } + /** + * Trigger issuance of set token paying with any arbitrary ERC20 token + * + * @param _setToken Set token to issue + * @param _setAmount Amount to issue + * @param _inputToken Input token to pay with + * @param _maxAmountInputToken Maximum amount of input token to spend + * @param _swapDataDebtForCollateral Data (token addresses and fee levels) to describe the swap path from Debt to collateral token + * @param _swapDataInputToken Data (token addresses and fee levels) to describe the swap path from input to collateral token + */ + function issueExactSetFromERC20( + ISetToken _setToken, + uint256 _setAmount, + address _inputToken, + uint256 _maxAmountInputToken, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + external + override + nonReentrant + { + uint256 inputTokenBalanceBefore = IERC20(_inputToken).balanceOf(address(this)); + IERC20(_inputToken).transferFrom(msg.sender, address(this), _maxAmountInputToken); + _initiateIssuance( + _setToken, + _setAmount, + _inputToken, + _maxAmountInputToken, + _swapDataDebtForCollateral, + _swapDataInputToken + ); + uint256 amountToReturn = IERC20(_inputToken).balanceOf(address(this)).sub(inputTokenBalanceBefore); + IERC20(_inputToken).transfer(msg.sender, amountToReturn); + } + + /** + * Trigger issuance of set token paying with Eth + * + * @param _setToken Set token to issue + * @param _setAmount Amount to issue + * @param _swapDataDebtForCollateral Data (token addresses and fee levels) to describe the swap path from Debt to collateral token + * @param _swapDataInputToken Data (token addresses and fee levels) to describe the swap path from eth to collateral token + */ + function issueExactSetFromETH( + ISetToken _setToken, + uint256 _setAmount, + DEXAdapter.SwapData memory _swapDataDebtForCollateral, + DEXAdapter.SwapData memory _swapDataInputToken + ) + external + override + payable + nonReentrant + { + uint256 inputTokenBalanceBefore = IERC20(addresses.weth).balanceOf(address(this)); + IWETH(addresses.weth).deposit{value: msg.value}(); + _initiateIssuance( + _setToken, + _setAmount, + DEXAdapter.ETH_ADDRESS, + msg.value, + _swapDataDebtForCollateral, + _swapDataInputToken + ); + uint256 amountToReturn = IERC20(addresses.weth).balanceOf(address(this)).sub(inputTokenBalanceBefore); + IWETH(addresses.weth).withdraw(amountToReturn); + msg.sender.transfer(amountToReturn); + } + function issueSetFromExactERC20( ISetToken _setToken, uint256 _minSetAmount, diff --git a/test/integration/ethereum/flashMintLeveragedExtended.spec.ts b/test/integration/ethereum/flashMintLeveragedExtended.spec.ts index 4133d1bb..aeb48bc8 100644 --- a/test/integration/ethereum/flashMintLeveragedExtended.spec.ts +++ b/test/integration/ethereum/flashMintLeveragedExtended.spec.ts @@ -1,7 +1,7 @@ import "module-alias/register"; import { Account, Address } from "@utils/types"; import DeployHelper from "@utils/deploys"; -import { getAccounts, getWaffleExpect } from "@utils/index"; +import { getAccounts, getWaffleExpect, preciseMul } from "@utils/index"; import { setBlockNumber } from "@utils/test/testingUtils"; import { ethers } from "hardhat"; import { BigNumber, utils } from "ethers"; @@ -214,12 +214,135 @@ if (process.env.INTEGRATIONTEST) { ["collateralToken", "WETH", "ETH"].forEach(inputTokenName => { describe(`When input/output token is ${inputTokenName}`, () => { - let subjectMinSetAmount: BigNumber; let amountIn: BigNumber; beforeEach(async () => { amountIn = ether(2); }); + describe( + inputTokenName === "ETH" ? "issueExactSetFromETH" : "#issueExactSetFromERC20", + () => { + let subjectSetAmount: BigNumber; + let swapDataDebtToCollateral: SwapData; + let swapDataInputToken: SwapData; + + let inputToken: StandardTokenMock | IWETH; + + let subjectSetToken: Address; + let subjectMaxAmountIn: BigNumber; + let subjectInputToken: Address; + + beforeEach(async () => { + subjectSetAmount = ether(1); + swapDataDebtToCollateral = { + path: [addresses.tokens.weth, addresses.tokens.rETH], + fees: [500], + pool: ADDRESS_ZERO, + exchange: Exchange.UniV3, + }; + + swapDataInputToken = { + path: [], + fees: [], + pool: ADDRESS_ZERO, + exchange: Exchange.None, + }; + + if (inputTokenName === "collateralToken") { + inputToken = rEth; + } else { + swapDataInputToken = swapDataDebtToCollateral; + + if (inputTokenName === "WETH") { + inputToken = weth; + await weth.deposit({ value: amountIn }); + } + } + + let inputTokenBalance: BigNumber; + if (inputTokenName === "ETH") { + subjectMaxAmountIn = amountIn; + } else { + inputTokenBalance = await inputToken.balanceOf(owner.address); + subjectMaxAmountIn = inputTokenBalance; + await inputToken.approve(flashMintLeveraged.address, subjectMaxAmountIn); + subjectInputToken = inputToken.address; + } + subjectSetToken = setToken.address; + }); + + async function subject() { + if (inputTokenName === "ETH") { + return flashMintLeveraged.issueExactSetFromETH( + subjectSetToken, + subjectSetAmount, + swapDataDebtToCollateral, + swapDataInputToken, + { value: subjectMaxAmountIn }, + ); + } + return flashMintLeveraged.issueExactSetFromERC20( + subjectSetToken, + subjectSetAmount, + subjectInputToken, + subjectMaxAmountIn, + swapDataDebtToCollateral, + swapDataInputToken, + ); + } + + async function subjectQuote() { + return flashMintLeveraged.callStatic.getIssueExactSet( + subjectSetToken, + subjectSetAmount, + swapDataDebtToCollateral, + swapDataInputToken, + ); + } + + it("should issue the correct amount of tokens", async () => { + const setBalancebefore = await setToken.balanceOf(owner.address); + await subject(); + const setBalanceAfter = await setToken.balanceOf(owner.address); + const setObtained = setBalanceAfter.sub(setBalancebefore); + expect(setObtained).to.eq(subjectSetAmount); + }); + + it("should spend less than specified max amount", async () => { + const inputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + expect(inputSpent.gt(0)).to.be.true; + expect(inputSpent.lte(subjectMaxAmountIn)).to.be.true; + }); + + it("should quote the correct input amount", async () => { + const inputBalanceBefore = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + await subject(); + const inputBalanceAfter = + inputTokenName === "ETH" + ? await owner.wallet.getBalance() + : await inputToken.balanceOf(owner.address); + const inputSpent = inputBalanceBefore.sub(inputBalanceAfter); + + const quotedInputAmount = await subjectQuote(); + + expect(quotedInputAmount).to.gt(preciseMul(inputSpent, ether(0.99))); + expect(quotedInputAmount).to.lt(preciseMul(inputSpent, ether(1.01))); + }); + }, + ); + describe( inputTokenName === "ETH" ? "issueSetFromExactETH" : "#issueSetFromExactERC20", () => { @@ -227,6 +350,7 @@ if (process.env.INTEGRATIONTEST) { let swapDataInputToken: SwapData; let inputToken: StandardTokenMock | IWETH; + let subjectMinSetAmount: BigNumber; let subjectSetToken: Address; let subjectAmountIn: BigNumber;