import BigNumber from 'bignumber.js'; import { TestEnv, makeSuite } from './helpers/make-suite'; import { APPROVAL_AMOUNT_LENDING_POOL, MAX_UINT_AMOUNT, oneRay } from '../../helpers/constants'; import { convertToCurrencyDecimals, getContract } from '../../helpers/contracts-helpers'; import { ethers } from 'ethers'; import { MockFlashLoanReceiver } from '../../types/MockFlashLoanReceiver'; import { ProtocolErrors, eContractid } from '../../helpers/types'; import { VariableDebtToken } from '../../types/VariableDebtToken'; import { StableDebtToken } from '../../types/StableDebtToken'; import { getMockFlashLoanReceiver, getStableDebtToken, getVariableDebtToken, } from '../../helpers/contracts-getters'; const { expect } = require('chai'); makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; const { VL_COLLATERAL_BALANCE_IS_0, TRANSFER_AMOUNT_EXCEEDS_BALANCE, LP_INVALID_FLASHLOAN_MODE, SAFEERC20_LOWLEVEL_CALL, LP_INVALID_FLASH_LOAN_EXECUTOR_RETURN, LP_BORROW_ALLOWANCE_NOT_ENOUGH, } = ProtocolErrors; const TOTAL_PREMIUM = 9; const PREMIUM_TO_PROTOCOL = 3; const PREMIUM_TO_LP = TOTAL_PREMIUM - PREMIUM_TO_PROTOCOL; before(async () => { _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); }); it('Configurator sets total premium = 9 bps, premium to protocol = 3 bps', async () => { const { configurator, pool } = testEnv; await configurator.updateFlashloanPremiumTotal(TOTAL_PREMIUM); await configurator.updateFlashloanPremiumToProtocol(PREMIUM_TO_PROTOCOL); expect(await pool.FLASHLOAN_PREMIUM_TOTAL()).to.be.equal(TOTAL_PREMIUM); expect(await pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()).to.be.equal(PREMIUM_TO_PROTOCOL); }); it('Deposits WETH into the reserve', async () => { const { pool, weth, aave, dai } = testEnv; const userAddress = await pool.signer.getAddress(); const amountToDeposit = ethers.utils.parseEther('1'); await weth.mint(amountToDeposit); await weth.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); await aave.mint(amountToDeposit); await aave.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.deposit(aave.address, amountToDeposit, userAddress, '0'); await dai.mint(amountToDeposit); await dai.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.deposit(dai.address, amountToDeposit, userAddress, '0'); }); it('Takes WETH + Dai flash loan with mode = 0, returns the funds correctly', async () => { const { pool, helpersContract, weth, aWETH, dai, aDai } = testEnv; const wethFlashBorrowedAmount = ethers.utils.parseEther('0.8'); const daiFlashBorrowedAmount = ethers.utils.parseEther('0.3'); const wethTotalFees = new BigNumber( wethFlashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString() ); const wethFeesToProtocol = wethFlashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); const wethFeesToLp = wethFlashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); const daiTotalFees = new BigNumber( daiFlashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString() ); const daiFeesToProtocol = daiFlashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); const daiFeesToLp = daiFlashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); const wethLiquidityIndexAdded = wethFeesToLp .mul(ethers.BigNumber.from(10).pow(27)) .div((await aWETH.totalSupply()).toString()); const daiLiquidityIndexAdded = daiFeesToLp .mul(ethers.BigNumber.from(10).pow(27)) .div((await aDai.totalSupply()).toString()); let wethReserveData = await helpersContract.getReserveData(weth.address); let daiReserveData = await helpersContract.getReserveData(dai.address); const wethLiquidityIndexBefore = wethReserveData.liquidityIndex; const daiLiquidityIndexBefore = daiReserveData.liquidityIndex; const wethTotalLiquidityBefore = new BigNumber(wethReserveData.availableLiquidity.toString()) .plus(wethReserveData.totalStableDebt.toString()) .plus(wethReserveData.totalVariableDebt.toString()); const daiTotalLiquidityBefore = new BigNumber(daiReserveData.availableLiquidity.toString()) .plus(daiReserveData.totalStableDebt.toString()) .plus(daiReserveData.totalVariableDebt.toString()); const wethReservesBefore = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); const daiReservesBefore = await aDai.balanceOf(await aDai.RESERVE_TREASURY_ADDRESS()); await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address, dai.address], [wethFlashBorrowedAmount, daiFlashBorrowedAmount], [0, 0], _mockFlashLoanReceiver.address, '0x10', '0' ); await pool.mintToTreasury([weth.address, dai.address]); wethReserveData = await helpersContract.getReserveData(weth.address); daiReserveData = await helpersContract.getReserveData(dai.address); const wethCurrentLiquidityRate = wethReserveData.liquidityRate; const wethCurrentLiquidityIndex = wethReserveData.liquidityIndex; const daiCurrentLiquidityRate = daiReserveData.liquidityRate; const daiCurrentLiquidityIndex = daiReserveData.liquidityIndex; const wethTotalLiquidityAfter = new BigNumber(wethReserveData.availableLiquidity.toString()) .plus(wethReserveData.totalStableDebt.toString()) .plus(wethReserveData.totalVariableDebt.toString()); const daiTotalLiquidityAfter = new BigNumber(daiReserveData.availableLiquidity.toString()) .plus(daiReserveData.totalStableDebt.toString()) .plus(daiReserveData.totalVariableDebt.toString()); const wethReservesAfter = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); const daiReservesAfter = await aDai.balanceOf(await aDai.RESERVE_TREASURY_ADDRESS()); expect(wethTotalLiquidityBefore.plus(wethTotalFees).toString()).to.be.equal( wethTotalLiquidityAfter.toString() ); expect(wethCurrentLiquidityRate.toString()).to.be.equal('0'); expect(wethCurrentLiquidityIndex.toString()).to.be.equal( wethLiquidityIndexBefore.add(wethLiquidityIndexAdded.toString()).toString() ); expect(wethReservesAfter).to.be.equal(wethReservesBefore.add(wethFeesToProtocol)); expect(daiTotalLiquidityBefore.plus(daiTotalFees).toString()).to.be.equal( daiTotalLiquidityAfter.toString() ); expect(daiCurrentLiquidityRate.toString()).to.be.equal('0'); expect(daiCurrentLiquidityIndex.toString()).to.be.equal( daiLiquidityIndexBefore.add(daiLiquidityIndexAdded.toString()).toString() ); expect(daiReservesAfter).to.be.equal(daiReservesBefore.add(daiFeesToProtocol)); }); it('Takes an authorized AAVE flash loan with mode = 0, returns the funds correctly', async () => { const { pool, helpersContract, aave, configurator, users: [, , , authorizedUser], } = testEnv; await configurator.authorizeFlashBorrower(authorizedUser.address); const flashBorrowedAmount = ethers.utils.parseEther('0.8'); const totalFees = new BigNumber(0); let reserveData = await helpersContract.getReserveData(aave.address); const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); await pool .connect(authorizedUser.signer) .flashLoan( _mockFlashLoanReceiver.address, [aave.address], [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); await pool.mintToTreasury([aave.address]); reserveData = await helpersContract.getReserveData(aave.address); const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); expect(totalLiquidityBefore.plus(totalFees).toString()).to.be.equal( totalLiquidityAfter.toString() ); }); it('Takes an ETH flashloan with mode = 0 as big as the available liquidity', async () => { const { pool, helpersContract, weth, aWETH } = testEnv; let reserveData = await helpersContract.getReserveData(weth.address); const totalLiquidityBefore = reserveData.availableLiquidity .add(reserveData.totalStableDebt) .add(reserveData.totalVariableDebt); const flashBorrowedAmount = totalLiquidityBefore; const totalFees = new BigNumber(flashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString()); const feesToProtocol = flashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); const feesToLp = flashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); const liquidityIndexBefore = reserveData.liquidityIndex; const liquidityIndexAdded = feesToLp .mul(ethers.BigNumber.from(10).pow(27)) .div((await aWETH.totalSupply()).toString()) .mul(liquidityIndexBefore) .div(ethers.BigNumber.from(10).pow(27)); const reservesBefore = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); const txResult = await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address], [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); await pool.mintToTreasury([weth.address]); reserveData = await helpersContract.getReserveData(weth.address); const currentLiquidityRate = reserveData.liquidityRate; const currentLiquidityIndex = reserveData.liquidityIndex; const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); const reservesAfter = await aWETH.balanceOf(await aWETH.RESERVE_TREASURY_ADDRESS()); expect(new BigNumber(totalLiquidityBefore.toString()).plus(totalFees).toString()).to.be.equal( totalLiquidityAfter.toString() ); expect(currentLiquidityRate.toString()).to.be.equal('0'); expect(currentLiquidityIndex.toString()).to.be.equal( liquidityIndexBefore.add(liquidityIndexAdded.toString()).toString() ); expect( reservesAfter.sub(feesToProtocol).mul(liquidityIndexBefore).div(currentLiquidityIndex) ).to.be.equal(reservesBefore); }); it('Takes WETH flashloan, does not return the funds with mode = 0. (revert expected)', async () => { const { pool, weth, users } = testEnv; const caller = users[1]; await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [ethers.utils.parseEther('0.8')], [0], caller.address, '0x10', '0' ) ).to.be.revertedWith(SAFEERC20_LOWLEVEL_CALL); }); it('Takes WETH flashloan, simulating a receiver as EOA (revert expected)', async () => { const { pool, weth, users } = testEnv; const caller = users[1]; await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await _mockFlashLoanReceiver.setSimulateEOA(true); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [ethers.utils.parseEther('0.8')], [0], caller.address, '0x10', '0' ) ).to.be.revertedWith(LP_INVALID_FLASH_LOAN_EXECUTOR_RETURN); }); it('Takes a WETH flashloan with an invalid mode. (revert expected)', async () => { const { pool, weth, users } = testEnv; const caller = users[1]; await _mockFlashLoanReceiver.setSimulateEOA(false); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [ethers.utils.parseEther('0.8')], [4], caller.address, '0x10', '0' ) ).to.be.reverted; }); it('Caller deposits 1000 DAI as collateral, Takes WETH flashloan with mode = 2, does not return the funds. A variable loan for caller is created', async () => { const { dai, pool, weth, users, helpersContract } = testEnv; const caller = users[1]; await dai.connect(caller.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); await dai.connect(caller.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, caller.address, '0'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); let reserveData = await helpersContract.getReserveData(weth.address); let totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); await pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [ethers.utils.parseEther('0.8')], [2], caller.address, '0x10', '0' ); const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); reserveData = await helpersContract.getReserveData(weth.address); const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); expect(totalLiquidityAfter.toString()).to.be.equal( ethers.BigNumber.from(totalLiquidityBefore.toString()) ); const wethDebtToken = await getVariableDebtToken(variableDebtTokenAddress); const callerDebt = await wethDebtToken.balanceOf(caller.address); expect(callerDebt.toString()).to.be.equal('800000000000000000', 'Invalid user debt'); // repays debt for later, so no interest accrue await weth.connect(caller.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); await weth.connect(caller.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.connect(caller.signer).repay(weth.address, MAX_UINT_AMOUNT, 2, caller.address); }); it('tries to take a flashloan that is bigger than the available liquidity (revert expected)', async () => { const { pool, weth, users } = testEnv; const caller = users[1]; await expect( pool.connect(caller.signer).flashLoan( _mockFlashLoanReceiver.address, [weth.address], ['1004415000000000000'], //slightly higher than the available liquidity [2], caller.address, '0x10', '0' ), TRANSFER_AMOUNT_EXCEEDS_BALANCE ).to.be.revertedWith(SAFEERC20_LOWLEVEL_CALL); }); it('tries to take a flashloan using a non contract address as receiver (revert expected)', async () => { const { pool, deployer, weth, users } = testEnv; const caller = users[1]; await expect( pool.flashLoan( deployer.address, [weth.address], ['1000000000000000000'], [2], caller.address, '0x10', '0' ) ).to.be.reverted; }); it('Deposits USDC into the reserve', async () => { const { usdc, pool } = testEnv; const userAddress = await pool.signer.getAddress(); await usdc.mint(await convertToCurrencyDecimals(usdc.address, '1000')); await usdc.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const amountToDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); await pool.deposit(usdc.address, amountToDeposit, userAddress, '0'); }); it('Takes out a 500 USDC flashloan, returns the funds correctly', async () => { const { usdc, aUsdc, pool, helpersContract, deployer: depositor } = testEnv; await _mockFlashLoanReceiver.setFailExecutionTransfer(false); const flashBorrowedAmount = await convertToCurrencyDecimals(usdc.address, '500'); const totalFees = new BigNumber(flashBorrowedAmount.mul(TOTAL_PREMIUM).div(10000).toString()); const feesToProtocol = flashBorrowedAmount.mul(PREMIUM_TO_PROTOCOL).div(10000); const feesToLp = flashBorrowedAmount.mul(PREMIUM_TO_LP).div(10000); const liquidityIndexAdded = feesToLp .mul(ethers.BigNumber.from(10).pow(27)) .div((await aUsdc.totalSupply()).toString()); let reserveData = await helpersContract.getReserveData(usdc.address); const liquidityIndexBefore = reserveData.liquidityIndex; const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); const reservesBefore = await aUsdc.balanceOf(await aUsdc.RESERVE_TREASURY_ADDRESS()); await pool.flashLoan( _mockFlashLoanReceiver.address, [usdc.address], [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); await pool.mintToTreasury([usdc.address]); reserveData = await helpersContract.getReserveData(usdc.address); const currentLiquidityRate = reserveData.liquidityRate; const currentLiquidityIndex = reserveData.liquidityIndex; const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); const reservesAfter = await aUsdc.balanceOf(await aUsdc.RESERVE_TREASURY_ADDRESS()); expect(totalLiquidityBefore.plus(totalFees).toString()).to.be.equal( totalLiquidityAfter.toString() ); expect(currentLiquidityRate.toString()).to.be.equal('0'); expect(currentLiquidityIndex.toString()).to.be.equal( liquidityIndexBefore.add(liquidityIndexAdded.toString()).toString() ); expect(reservesAfter).to.be.equal(reservesBefore.add(feesToProtocol)); }); it('Takes out a 500 USDC flashloan with mode = 0, does not return the funds. (revert expected)', async () => { const { usdc, pool, users } = testEnv; const caller = users[2]; const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [usdc.address], [flashloanAmount], [2], caller.address, '0x10', '0' ) ).to.be.revertedWith(VL_COLLATERAL_BALANCE_IS_0); }); it('Caller deposits 5 WETH as collateral, Takes a USDC flashloan with mode = 2, does not return the funds. A loan for caller is created', async () => { const { usdc, pool, weth, users, helpersContract } = testEnv; const caller = users[2]; await weth.connect(caller.signer).mint(await convertToCurrencyDecimals(weth.address, '5')); await weth.connect(caller.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const amountToDeposit = await convertToCurrencyDecimals(weth.address, '5'); await pool.connect(caller.signer).deposit(weth.address, amountToDeposit, caller.address, '0'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); await pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [usdc.address], [flashloanAmount], [2], caller.address, '0x10', '0' ); const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( usdc.address ); const usdcDebtToken = await getVariableDebtToken(variableDebtTokenAddress); const callerDebt = await usdcDebtToken.balanceOf(caller.address); expect(callerDebt.toString()).to.be.equal('500000000', 'Invalid user debt'); }); it('Caller deposits 1000 DAI as collateral, Takes a WETH flashloan with mode = 0, does not approve the transfer of the funds', async () => { const { dai, pool, weth, users } = testEnv; const caller = users[3]; await dai.connect(caller.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); await dai.connect(caller.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, caller.address, '0'); const flashAmount = ethers.utils.parseEther('0.8'); await _mockFlashLoanReceiver.setFailExecutionTransfer(false); await _mockFlashLoanReceiver.setAmountToApprove(flashAmount.div(2)); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [flashAmount], [0], caller.address, '0x10', '0' ) ).to.be.revertedWith(SAFEERC20_LOWLEVEL_CALL); }); it('Caller takes a WETH flashloan with mode = 1', async () => { const { dai, pool, weth, users, helpersContract } = testEnv; const caller = users[3]; const flashAmount = ethers.utils.parseEther('0.8'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [flashAmount], [1], caller.address, '0x10', '0' ); const { stableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const wethDebtToken = await getStableDebtToken(stableDebtTokenAddress); const callerDebt = await wethDebtToken.balanceOf(caller.address); expect(callerDebt.toString()).to.be.equal('800000000000000000', 'Invalid user debt'); }); it('Caller takes a WETH flashloan with mode = 1 onBehalfOf user without allowance', async () => { const { dai, pool, weth, users, helpersContract } = testEnv; const caller = users[5]; const onBehalfOf = users[4]; // Deposit 1000 dai for onBehalfOf user await dai.connect(onBehalfOf.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); await dai.connect(onBehalfOf.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); await pool .connect(onBehalfOf.signer) .deposit(dai.address, amountToDeposit, onBehalfOf.address, '0'); const flashAmount = ethers.utils.parseEther('0.8'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await expect( pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [flashAmount], [1], onBehalfOf.address, '0x10', '0' ) ).to.be.revertedWith(LP_BORROW_ALLOWANCE_NOT_ENOUGH); }); it('Caller takes a WETH flashloan with mode = 1 onBehalfOf user with allowance. A loan for onBehalfOf is creatd.', async () => { const { dai, pool, weth, users, helpersContract } = testEnv; const caller = users[5]; const onBehalfOf = users[4]; const flashAmount = ethers.utils.parseEther('0.8'); const reserveData = await pool.getReserveData(weth.address); const stableDebtToken = await getStableDebtToken(reserveData.stableDebtTokenAddress); // Deposited for onBehalfOf user already, delegate borrow allowance await stableDebtToken.connect(onBehalfOf.signer).approveDelegation(caller.address, flashAmount); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await pool .connect(caller.signer) .flashLoan( _mockFlashLoanReceiver.address, [weth.address], [flashAmount], [1], onBehalfOf.address, '0x10', '0' ); const { stableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const wethDebtToken = await getStableDebtToken(stableDebtTokenAddress); const onBehalfOfDebt = await wethDebtToken.balanceOf(onBehalfOf.address); expect(onBehalfOfDebt.toString()).to.be.equal( '800000000000000000', 'Invalid onBehalfOf user debt' ); }); });