diff --git a/buidler.config.ts b/buidler.config.ts index d0657ab0..d21f5293 100644 --- a/buidler.config.ts +++ b/buidler.config.ts @@ -8,7 +8,7 @@ usePlugin('buidler-typechain'); usePlugin('solidity-coverage'); usePlugin('@nomiclabs/buidler-waffle'); usePlugin('@nomiclabs/buidler-etherscan'); -usePlugin('buidler-gas-reporter'); +//usePlugin('buidler-gas-reporter'); const DEFAULT_BLOCK_GAS_LIMIT = 10000000; const DEFAULT_GAS_PRICE = 10; diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 43bfb554..cc676666 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -63,7 +63,7 @@ interface ILendingPool { * @param reserve the address of the reserve * @param user the address of the user executing the swap **/ - event Swap(address indexed reserve, address indexed user, uint256 timestamp); + event Swap(address indexed reserve, address indexed user); /** * @dev emitted when a user enables a reserve as collateral @@ -91,12 +91,14 @@ interface ILendingPool { * @param reserve the address of the reserve * @param amount the amount requested * @param totalFee the total fee on the amount + * @param referralCode the referral code of the caller **/ event FlashLoan( address indexed target, address indexed reserve, uint256 amount, - uint256 totalFee + uint256 totalFee, + uint16 referralCode ); /** * @dev these events are not emitted directly by the LendingPool @@ -105,21 +107,6 @@ interface ILendingPool { * This allows to have the events in the generated ABI for LendingPool. **/ - /** - * @dev emitted when a borrow fee is liquidated - * @param collateral the address of the collateral being liquidated - * @param reserve the address of the reserve - * @param user the address of the user being liquidated - * @param feeLiquidated the total fee liquidated - * @param liquidatedCollateralForFee the amount of collateral received by the protocol in exchange for the fee - **/ - event OriginationFeeLiquidated( - address indexed collateral, - address indexed reserve, - address indexed user, - uint256 feeLiquidated, - uint256 liquidatedCollateralForFee - ); /** * @dev emitted when a borrower is liquidated * @param collateral the address of the collateral being liquidated @@ -238,12 +225,15 @@ interface ILendingPool { * @param receiver The address of the contract receiving the funds. The receiver should implement the IFlashLoanReceiver interface. * @param reserve the address of the principal reserve * @param amount the amount requested for this flashloan + * @param params a bytes array to be sent to the flashloan executor + * @param referralCode the referral code of the caller **/ function flashLoan( address receiver, address reserve, uint256 amount, - bytes calldata params + bytes calldata params, + uint16 referralCode ) external; /** diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 77f04893..0066076d 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -108,7 +108,6 @@ contract LendingPool is VersionedInitializable, ILendingPool { //transfer to the aToken contract IERC20(asset).safeTransferFrom(msg.sender, address(aToken), amount); - //solium-disable-next-line emit Deposit(asset, msg.sender, amount, referralCode); } @@ -152,7 +151,6 @@ contract LendingPool is VersionedInitializable, ILendingPool { aToken.burn(msg.sender, msg.sender, amountToWithdraw); - //solium-disable-next-line emit Withdraw(asset, msg.sender, amount); } @@ -320,9 +318,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { emit Swap( asset, - msg.sender, - //solium-disable-next-line - block.timestamp + msg.sender ); } @@ -441,6 +437,15 @@ contract LendingPool is VersionedInitializable, ILendingPool { } } + struct FlashLoanLocalVars{ + uint256 amountFee; + uint256 amountPlusFee; + uint256 amountPlusFeeInETH; + IFlashLoanReceiver receiver; + address aTokenAddress; + address oracle; + } + /** * @dev allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts @@ -453,40 +458,77 @@ contract LendingPool is VersionedInitializable, ILendingPool { address receiverAddress, address asset, uint256 amount, - bytes calldata params + bytes calldata params, + uint16 referralCode ) external override { ReserveLogic.ReserveData storage reserve = _reserves[asset]; + FlashLoanLocalVars memory vars; - address aTokenAddress = reserve.aTokenAddress; + vars.aTokenAddress = reserve.aTokenAddress; //calculate amount fee - uint256 amountFee = amount.mul(FLASHLOAN_FEE_TOTAL).div(10000); + vars.amountFee = amount.mul(FLASHLOAN_FEE_TOTAL).div(10000); - require(amountFee > 0, 'The requested amount is too small for a FlashLoan.'); + require(vars.amountFee > 0, 'The requested amount is too small for a FlashLoan.'); //get the FlashLoanReceiver instance - IFlashLoanReceiver receiver = IFlashLoanReceiver(receiverAddress); + vars.receiver = IFlashLoanReceiver(receiverAddress); //transfer funds to the receiver - IAToken(aTokenAddress).transferUnderlyingTo(receiverAddress, amount); + IAToken(vars.aTokenAddress).transferUnderlyingTo(receiverAddress, amount); //execute action of the receiver - receiver.executeOperation(asset, amount, amountFee, params); + vars.receiver.executeOperation(asset, amount, vars.amountFee, params); - //transfer from the receiver the amount plus the fee - IERC20(asset).safeTransferFrom(receiverAddress, aTokenAddress, amount.add(amountFee)); - - //compounding the cumulated interest + //compounding the cumulated interest reserve.updateCumulativeIndexesAndTimestamp(); - //compounding the received fee into the reserve - reserve.cumulateToLiquidityIndex(IERC20(aTokenAddress).totalSupply(), amountFee); + vars.amountPlusFee = amount.add(vars.amountFee); - //refresh interest rates - reserve.updateInterestRates(asset, amountFee, 0); + //transfer from the receiver the amount plus the fee + try IERC20(asset).transferFrom(receiverAddress, vars.aTokenAddress, vars.amountPlusFee) { + //if the transfer succeeded, the executor has repaid the flashloans. + //the fee is compounded into the reserve + reserve.cumulateToLiquidityIndex(IERC20(vars.aTokenAddress).totalSupply(), vars.amountFee); + //refresh interest rates + reserve.updateInterestRates(asset, vars.amountFee, 0); + emit FlashLoan(receiverAddress, asset, amount, vars.amountFee, referralCode); + } + catch(bytes memory reason){ - //solium-disable-next-line - emit FlashLoan(receiverAddress, asset, amount, amountFee); + //if the transfer didn't succeed, the executor either didn't return the funds, or didn't approve the transfer. + //we check if the caller has enough collateral to open a variable rate loan. If it does, then debt is mint to msg.sender + vars.oracle = addressesProvider.getPriceOracle(); + vars.amountPlusFeeInETH = IPriceOracleGetter(vars.oracle) + .getAssetPrice(asset) + .mul(vars.amountPlusFee) + .div(10**reserve.configuration.getDecimals()); //price is in ether + + ValidationLogic.validateBorrow( + reserve, + asset, + vars.amountPlusFee, + vars.amountPlusFeeInETH, + uint256(ReserveLogic.InterestRateMode.VARIABLE), + MAX_STABLE_RATE_BORROW_SIZE_PERCENT, + _reserves, + _usersConfig[msg.sender], + reservesList, + vars.oracle + ); + + IVariableDebtToken(reserve.variableDebtTokenAddress).mint(msg.sender, vars.amountPlusFee); + //refresh interest rates + reserve.updateInterestRates(asset, vars.amountFee, 0); + emit Borrow( + asset, + msg.sender, + vars.amountPlusFee, + uint256(ReserveLogic.InterestRateMode.VARIABLE), + reserve.currentVariableBorrowRate, + referralCode + ); + } } /** diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index abba4592..dd3ef25a 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -62,10 +62,6 @@ library ValidationLogic { ) external view { require(amount > 0, 'Amount must be greater than 0'); - uint256 currentAvailableLiquidity = IERC20(reserveAddress).balanceOf(address(aTokenAddress)); - - require(currentAvailableLiquidity >= amount, '4'); - require(amount <= userBalance, 'User cannot withdraw more than the available balance'); require( @@ -150,11 +146,6 @@ library ValidationLogic { 'Invalid interest rate mode selected' ); - //check that the amount is available in the reserve - vars.availableLiquidity = IERC20(reserveAddress).balanceOf(address(reserve.aTokenAddress)); - - require(vars.availableLiquidity >= amount, '7'); - ( vars.userCollateralBalanceETH, vars.userBorrowBalanceETH, diff --git a/helpers/types.ts b/helpers/types.ts index 625da1f4..08ca3320 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -63,6 +63,7 @@ export enum ProtocolErrors { INVALID_HF = 'Invalid health factor', USER_DID_NOT_BORROW_SPECIFIED = 'User did not borrow the specified currency', THE_COLLATERAL_CHOSEN_CANNOT_BE_LIQUIDATED = 'The collateral chosen cannot be liquidated', + COLLATERAL_BALANCE_IS_0 = 'The collateral balance is 0' } export type tEthereumAddress = string; diff --git a/test/flashloan.spec.ts b/test/flashloan.spec.ts index cbbda7d6..dfda3085 100644 --- a/test/flashloan.spec.ts +++ b/test/flashloan.spec.ts @@ -1,18 +1,20 @@ -import {TestEnv, makeSuite} from './helpers/make-suite'; -import {APPROVAL_AMOUNT_LENDING_POOL, oneRay} from '../helpers/constants'; -import {convertToCurrencyDecimals, getMockFlashLoanReceiver} from '../helpers/contracts-helpers'; -import {ethers} from 'ethers'; -import {MockFlashLoanReceiver} from '../types/MockFlashLoanReceiver'; -import {ProtocolErrors} from '../helpers/types'; +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { APPROVAL_AMOUNT_LENDING_POOL, oneRay } from '../helpers/constants'; +import { convertToCurrencyDecimals, getMockFlashLoanReceiver, getContract } from '../helpers/contracts-helpers'; +import { ethers } from 'ethers'; +import { MockFlashLoanReceiver } from '../types/MockFlashLoanReceiver'; +import { ProtocolErrors, eContractid } from '../helpers/types'; import BigNumber from 'bignumber.js'; +import { VariableDebtToken } from '../types/VariableDebtToken'; -const {expect} = require('chai'); +const { expect } = require('chai'); makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; const { TRANSFER_AMOUNT_EXCEEDS_BALANCE, TOO_SMALL_FLASH_LOAN, + COLLATERAL_BALANCE_IS_0, } = ProtocolErrors; before(async () => { @@ -20,7 +22,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('Deposits ETH into the reserve', async () => { - const {pool, weth} = testEnv; + const { pool, weth } = testEnv; const amountToDeposit = ethers.utils.parseEther('1'); await weth.mint(amountToDeposit); @@ -31,13 +33,14 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('Takes ETH flashloan, returns the funds correctly', async () => { - const {pool, deployer, weth} = testEnv; + const { pool, deployer, weth } = testEnv; await pool.flashLoan( _mockFlashLoanReceiver.address, weth.address, ethers.utils.parseEther('0.8'), - '0x10' + '0x10', + '0' ); ethers.utils.parseUnits('10000'); @@ -57,17 +60,15 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('Takes an ETH flashloan as big as the available liquidity', async () => { - const {pool, weth} = testEnv; + const { pool, weth } = testEnv; const reserveDataBefore = await pool.getReserveData(weth.address); - - console.log("Total liquidity is ", reserveDataBefore.availableLiquidity.toString()); - const txResult = await pool.flashLoan( _mockFlashLoanReceiver.address, weth.address, '1000720000000000000', - '0x10' + '0x10', + '0' ); const reserveData = await pool.getReserveData(weth.address); @@ -84,83 +85,122 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { expect(currentLiquidityIndex.toString()).to.be.equal('1001620648000000000000000000'); }); - it('Takes WETH flashloan, does not return the funds (revert expected)', async () => { - const {pool, deployer, weth} = testEnv; - - // move funds to the MockFlashLoanReceiver contract to pay the fee - + it('Takes WETH flashloan, does not return the funds. Caller does not have any collateral (revert expected)', async () => { + const { pool, weth, users } = testEnv; + const caller = users[1]; await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await expect( - pool.flashLoan( + pool + .connect(caller.signer) + .flashLoan( + _mockFlashLoanReceiver.address, + weth.address, + ethers.utils.parseEther('0.8'), + '0x10', + '0' + ) + ).to.be.revertedWith(COLLATERAL_BALANCE_IS_0); + }); + + it('Caller deposits 1000 DAI as collateral, Takes WETH flashloan, does not return the funds. A loan for caller is created', async () => { + const { dai, pool, weth, users } = 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, '0'); + + await _mockFlashLoanReceiver.setFailExecutionTransfer(true); + + await pool + .connect(caller.signer) + .flashLoan( _mockFlashLoanReceiver.address, weth.address, ethers.utils.parseEther('0.8'), - '0x10' - ) - ).to.be.revertedWith(TRANSFER_AMOUNT_EXCEEDS_BALANCE); + '0x10', + '0' + ); + const {variableDebtTokenAddress} = await pool.getReserveTokensAddresses(weth.address); + + const wethDebtToken = await getContract(eContractid.VariableDebtToken, variableDebtTokenAddress); + + const callerDebt = await wethDebtToken.balanceOf(caller.address); + + expect(callerDebt.toString()).to.be.equal('800720000000000000', 'Invalid user debt'); }); it('tries to take a very small flashloan, which would result in 0 fees (revert expected)', async () => { - const {pool, weth} = testEnv; + const { pool, weth } = testEnv; await expect( pool.flashLoan( _mockFlashLoanReceiver.address, weth.address, '1', //1 wei loan - '0x10' + '0x10', + '0' ) ).to.be.revertedWith(TOO_SMALL_FLASH_LOAN); }); it('tries to take a flashloan that is bigger than the available liquidity (revert expected)', async () => { - const {pool, weth} = testEnv; + const { pool, weth } = testEnv; await expect( pool.flashLoan( _mockFlashLoanReceiver.address, weth.address, '1004415000000000000', //slightly higher than the available liquidity - '0x10' + '0x10', + '0' ), TRANSFER_AMOUNT_EXCEEDS_BALANCE ).to.be.revertedWith(TRANSFER_AMOUNT_EXCEEDS_BALANCE); }); it('tries to take a flashloan using a non contract address as receiver (revert expected)', async () => { - const {pool, deployer, weth} = testEnv; + const { pool, deployer, weth } = testEnv; - await expect(pool.flashLoan(deployer.address, weth.address, '1000000000000000000', '0x10')).to - .be.reverted; + await expect(pool.flashLoan(deployer.address, weth.address, '1000000000000000000', '0x10', '0')) + .to.be.reverted; }); - it('Deposits DAI into the reserve', async () => { - const {dai, pool} = testEnv; + it('Deposits USDC into the reserve', async () => { + const { usdc, pool } = testEnv; - await dai.mint(await convertToCurrencyDecimals(dai.address, '1000')); + await usdc.mint(await convertToCurrencyDecimals(usdc.address, '1000')); - await dai.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await usdc.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + const amountToDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); - await pool.deposit(dai.address, amountToDeposit, '0'); + await pool.deposit(usdc.address, amountToDeposit, '0'); }); - it('Takes out a 500 DAI flashloan, returns the funds correctly', async () => { - const {dai, pool, deployer: depositor} = testEnv; + it('Takes out a 500 USDC flashloan, returns the funds correctly', async () => { + const { usdc, pool, deployer: depositor } = testEnv; await _mockFlashLoanReceiver.setFailExecutionTransfer(false); + const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); + await pool.flashLoan( _mockFlashLoanReceiver.address, - dai.address, - ethers.utils.parseEther('500'), - '0x10' + usdc.address, + flashloanAmount, + '0x10', + '0' ); - const reserveData = await pool.getReserveData(dai.address); - const userData = await pool.getUserReserveData(dai.address, depositor.address); + const reserveData = await pool.getReserveData(usdc.address); + const userData = await pool.getUserReserveData(usdc.address, depositor.address); const totalLiquidity = reserveData.availableLiquidity .add(reserveData.totalBorrowsStable) @@ -170,7 +210,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const currentLiquidityIndex = reserveData.liquidityIndex.toString(); const currentUserBalance = userData.currentATokenBalance.toString(); - const expectedLiquidity = ethers.utils.parseEther('1000.450'); + const expectedLiquidity = await convertToCurrencyDecimals(usdc.address,'1000.450'); expect(totalLiquidity).to.be.equal(expectedLiquidity, 'Invalid total liquidity'); expect(currentLiqudityRate).to.be.equal('0', 'Invalid liquidity rate'); @@ -181,19 +221,59 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { expect(currentUserBalance.toString()).to.be.equal(expectedLiquidity, 'Invalid user balance'); }); - it('Takes out a 500 DAI flashloan, does not return the funds (revert expected)', async () => { - const {dai, pool} = testEnv; + it('Takes out a 500 USDC flashloan, does not return the funds. Caller does not have any collateral (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.flashLoan( + pool + .connect(caller.signer) + .flashLoan( + _mockFlashLoanReceiver.address, + usdc.address, + flashloanAmount, + '0x10', + '0' + ) + ).to.be.revertedWith(COLLATERAL_BALANCE_IS_0); + }); + + it('Caller deposits 5 ETH as collateral, Takes a USDC flashloan, does not return the funds. A loan for caller is created', async () => { + const { usdc, pool, weth, users } = 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, '0'); + + await _mockFlashLoanReceiver.setFailExecutionTransfer(true); + + const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); + + await pool + .connect(caller.signer) + .flashLoan( _mockFlashLoanReceiver.address, - dai.address, - ethers.utils.parseEther('500'), - '0x10' - ), - TRANSFER_AMOUNT_EXCEEDS_BALANCE - ).to.be.revertedWith(TRANSFER_AMOUNT_EXCEEDS_BALANCE); + usdc.address, + flashloanAmount, + '0x10', + '0' + ); + const {variableDebtTokenAddress} = await pool.getReserveTokensAddresses(usdc.address); + + const usdcDebtToken = await getContract(eContractid.VariableDebtToken, variableDebtTokenAddress); + + const callerDebt = await usdcDebtToken.balanceOf(caller.address); + + expect(callerDebt.toString()).to.be.equal('500450000', 'Invalid user debt'); }); });