diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index b893c16b..450def2d 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -99,6 +99,8 @@ interface ILendingPool { /** * @dev emitted when a flashloan is executed * @param target the address of the flashLoanReceiver + * @param mode Type of the debt to open if the flash loan is not returned. 0 -> Don't open any debt, just revert, 1 -> stable, 2 -> variable + * @param onBehalfOf the address incurring the debt, if borrow mode is not 0 * @param assets the address of the assets being flashborrowed * @param amounts the amount requested * @param premiums the total fee on the amount @@ -107,6 +109,7 @@ interface ILendingPool { event FlashLoan( address indexed target, uint256 mode, + address indexed onBehalfOf, address[] assets, uint256[] amounts, uint256[] premiums, @@ -295,6 +298,7 @@ interface ILendingPool { address[] calldata assets, uint256[] calldata amounts, uint256 mode, + address onBehalfOf, bytes calldata params, uint16 referralCode ) external; diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 1684247e..539b6ac6 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -503,6 +503,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param assets The addresss of the assets being flashborrowed * @param amounts The amounts requested for this flashloan for each asset * @param mode Type of the debt to open if the flash loan is not returned. 0 -> Don't open any debt, just revert, 1 -> stable, 2 -> variable + * @param onBehalfOf If mode is not 0, then the address to take the debt onBehalfOf. The onBehalfOf address must already have approved `msg.sender` to incur the debt on their behalf. * @param params Variadic packed params to pass to the receiver as extra information * @param referralCode Referral code of the flash loan **/ @@ -511,6 +512,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address[] calldata assets, uint256[] calldata amounts, uint256 mode, + address onBehalfOf, bytes calldata params, uint16 referralCode ) external override { @@ -568,13 +570,23 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage vars.currentAmountPlusPremium ); } else { + if (msg.sender != onBehalfOf) { + address debtToken = _reserves[vars.currentAsset].getDebtTokenAddress(mode); + + _borrowAllowance[debtToken][onBehalfOf][msg + .sender] = _borrowAllowance[debtToken][onBehalfOf][msg.sender].sub( + vars.currentAmount, + Errors.BORROW_ALLOWANCE_ARE_NOT_ENOUGH + ); + } + //if the user didn't choose to return the funds, the system checks if there //is enough collateral and eventually open a position _executeBorrow( ExecuteBorrowParams( vars.currentAsset, msg.sender, - msg.sender, + onBehalfOf, vars.currentAmount, mode, vars.currentATokenAddress, @@ -583,7 +595,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ) ); } - emit FlashLoan(receiverAddress, mode, assets, amounts, premiums, referralCode); + emit FlashLoan(receiverAddress, mode, onBehalfOf, assets, amounts, premiums, referralCode); } } diff --git a/helpers/types.ts b/helpers/types.ts index 8abd4bcc..81ac6661 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -91,6 +91,7 @@ export enum ProtocolErrors { REQUESTED_AMOUNT_TOO_SMALL = '25', // 'The requested amount is too small for a FlashLoan.' INCONSISTENT_PROTOCOL_ACTUAL_BALANCE = '26', // 'The actual balance of the protocol is inconsistent' CALLER_NOT_LENDING_POOL_CONFIGURATOR = '27', // 'The actual balance of the protocol is inconsistent' + BORROW_ALLOWANCE_ARE_NOT_ENOUGH = '54', // User borrows on behalf, but allowance are too small INVALID_FLASH_LOAN_EXECUTOR_RETURN = '60', // The flash loan received returned 0 (EOA) // require error messages - aToken diff --git a/test/flashloan.spec.ts b/test/flashloan.spec.ts index 305d674f..fc977fcf 100644 --- a/test/flashloan.spec.ts +++ b/test/flashloan.spec.ts @@ -22,6 +22,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { SAFEERC20_LOWLEVEL_CALL, IS_PAUSED, INVALID_FLASH_LOAN_EXECUTOR_RETURN, + BORROW_ALLOWANCE_ARE_NOT_ENOUGH, } = ProtocolErrors; before(async () => { @@ -48,6 +49,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], [ethers.utils.parseEther('0.8')], 0, + _mockFlashLoanReceiver.address, '0x10', '0' ); @@ -77,6 +79,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], ['1000720000000000000'], 0, + _mockFlashLoanReceiver.address, '0x10', '0' ); @@ -108,6 +111,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], [ethers.utils.parseEther('0.8')], 0, + caller.address, '0x10', '0' ) @@ -128,6 +132,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], [ethers.utils.parseEther('0.8')], 0, + caller.address, '0x10', '0' ) @@ -148,6 +153,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], [ethers.utils.parseEther('0.8')], 4, + caller.address, '0x10', '0' ) @@ -176,6 +182,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [weth.address], [ethers.utils.parseEther('0.8')], 2, + caller.address, '0x10', '0' ); @@ -194,14 +201,16 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('tries to take a flashloan that is bigger than the available liquidity (revert expected)', async () => { - const {pool, weth} = testEnv; + const {pool, weth, users} = testEnv; + const caller = users[1]; await expect( - pool.flashLoan( + pool.connect(caller.signer).flashLoan( _mockFlashLoanReceiver.address, [weth.address], ['1004415000000000000'], //slightly higher than the available liquidity 2, + caller.address, '0x10', '0' ), @@ -210,10 +219,11 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); 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, users} = testEnv; + const caller = users[1]; await expect( - pool.flashLoan(deployer.address, [weth.address], ['1000000000000000000'], 2, '0x10', '0') + pool.flashLoan(deployer.address, [weth.address], ['1000000000000000000'], 2, caller.address, '0x10', '0') ).to.be.reverted; }); @@ -242,6 +252,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [usdc.address], [flashloanAmount], 0, + _mockFlashLoanReceiver.address, '0x10', '0' ); @@ -284,6 +295,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { [usdc.address], [flashloanAmount], 2, + caller.address, '0x10', '0' ) @@ -309,7 +321,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await pool .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, [usdc.address], [flashloanAmount], 2, '0x10', '0'); + .flashLoan(_mockFlashLoanReceiver.address, [usdc.address], [flashloanAmount], 2, caller.address, '0x10', '0'); const {variableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses( usdc.address ); @@ -344,7 +356,15 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await expect( pool .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, [weth.address], [flashAmount], 0, '0x10', '0') + .flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [flashAmount], + 0, + caller.address, + '0x10', + '0' + ) ).to.be.revertedWith(SAFEERC20_LOWLEVEL_CALL); }); @@ -359,7 +379,15 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await pool .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, [weth.address], [flashAmount], 1, '0x10', '0'); + .flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [flashAmount], + 1, + caller.address, + '0x10', + '0' + ); const {stableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses(weth.address); @@ -372,4 +400,82 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { 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(BORROW_ALLOWANCE_ARE_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'); + + // Deposited for onBehalfOf user already, delegate borrow allowance + await pool + .connect(onBehalfOf.signer) + .delegateBorrowAllowance(weth.address, caller.address, 1, 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 getContract( + eContractid.VariableDebtToken, + stableDebtTokenAddress + ); + + const onBehalfOfDebt = await wethDebtToken.balanceOf(onBehalfOf.address); + + expect(onBehalfOfDebt.toString()).to.be.equal( + '800000000000000000', + 'Invalid onBehalfOf user debt' + ); + }); }); diff --git a/test/pausable-functions.spec.ts b/test/pausable-functions.spec.ts index 2bf8be21..7a24da2d 100644 --- a/test/pausable-functions.spec.ts +++ b/test/pausable-functions.spec.ts @@ -187,7 +187,7 @@ makeSuite('Pausable Pool', (testEnv: TestEnv) => { await expect( pool .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, [weth.address], [flashAmount], 1, '0x10', '0') + .flashLoan(_mockFlashLoanReceiver.address, [weth.address], [flashAmount], 1, caller.address, '0x10', '0') ).revertedWith(IS_PAUSED); // Unpause pool