diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index bcebbde8..86e58f61 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -471,4 +471,17 @@ interface ILendingPool { function updateFlashBorrowerAuthorization(address flashBorrower, bool authorized) external; function isFlashBorrowerAuthorized(address flashBorrower) external view returns (bool); + + function updateFlashloanPremiums( + uint256 flashLoanPremiumTotal, + uint256 flashLoanPremiumToProtocol + ) external; + + function MAX_STABLE_RATE_BORROW_SIZE_PERCENT() external view returns (uint256); + + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint256); + + function FLASHLOAN_PREMIUM_TO_PROTOCOL() external view returns (uint256); + + function MAX_NUMBER_RESERVES() external view returns (uint256); } diff --git a/contracts/interfaces/ILendingPoolConfigurator.sol b/contracts/interfaces/ILendingPoolConfigurator.sol index 312aebed..634bde26 100644 --- a/contracts/interfaces/ILendingPoolConfigurator.sol +++ b/contracts/interfaces/ILendingPoolConfigurator.sol @@ -233,6 +233,18 @@ interface ILendingPoolConfigurator { **/ event RiskAdminUnregistered(address indexed admin); + /** + * @dev Emitted when a the total premium on flashloans is updated + * @param flashloanPremiumTotal the new premium + **/ + event FlashloanPremiumTotalUpdated(uint256 flashloanPremiumTotal); + + /** + * @dev Emitted when a the part of the premium that goes to protoco lis updated + * @param flashloanPremiumToProtocol the new premium + **/ + event FlashloanPremiumToProcolUpdated(uint256 flashloanPremiumToProtocol); + /** * @dev Initializes reserves in batch * @param input The array of reserves initialization parameters @@ -410,4 +422,19 @@ interface ILendingPoolConfigurator { * @param asset the address of the reserve to drop **/ function dropReserve(address asset) external; + + /** + * @dev Updates the total flash loan premium + * flash loan premium consist in 2 parts + * - A part is sent to aToken holders as extra balance + * - A part is collected by the protocol reserves + * @param flashloanPremiumTotal total premium in bps + */ + function updateFlashloanPremiumTotal(uint256 flashloanPremiumTotal) external; + + /** + * @dev Updates the flash loan premium collected by protocol reserves + * @param flashloanPremiumToProtocol part of the premium sent to protocol + */ + function updateFlashloanPremiumToProtocol(uint256 flashloanPremiumToProtocol) external; } diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index a5283d11..fbf9a2b0 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -90,6 +90,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _maxStableRateBorrowSizePercent = 2500; _flashLoanPremiumTotal = 9; _maxNumberOfReserves = 128; + _flashLoanPremiumToProtocol = 0; } /** @@ -438,12 +439,14 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address currentAsset; address currentATokenAddress; uint256 currentAmount; - uint256 currentPremium; + uint256 currentPremiumToLP; + uint256 currentPremiumToProtocol; uint256 currentAmountPlusPremium; address debtToken; address[] aTokenAddresses; - uint256[] premiums; + uint256[] totalPremiums; uint256 flashloanPremiumTotal; + uint256 flashloanPremiumToProtocol; } /** @@ -475,30 +478,35 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage FlashLoanLocalVars memory vars; vars.aTokenAddresses = new address[](assets.length); - vars.premiums = new uint256[](assets.length); + vars.totalPremiums = new uint256[](assets.length); ValidationLogic.validateFlashloan(assets, amounts, _reserves); vars.receiver = IFlashLoanReceiver(receiverAddress); - vars.flashloanPremiumTotal = _authorizedFlashBorrowers[msg.sender] ? 0 : _flashLoanPremiumTotal; + (vars.flashloanPremiumTotal, vars.flashloanPremiumToProtocol) = _authorizedFlashBorrowers[ + msg.sender + ] + ? (0, 0) + : (_flashLoanPremiumTotal, _flashLoanPremiumToProtocol); for (vars.i = 0; vars.i < assets.length; vars.i++) { vars.aTokenAddresses[vars.i] = _reserves[assets[vars.i]].aTokenAddress; - vars.premiums[vars.i] = amounts[vars.i].percentMul(vars.flashloanPremiumTotal); + vars.totalPremiums[vars.i] = amounts[vars.i].percentMul(vars.flashloanPremiumTotal); IAToken(vars.aTokenAddresses[vars.i]).transferUnderlyingTo(receiverAddress, amounts[vars.i]); } require( - vars.receiver.executeOperation(assets, amounts, vars.premiums, msg.sender, params), + vars.receiver.executeOperation(assets, amounts, vars.totalPremiums, msg.sender, params), Errors.LP_INVALID_FLASH_LOAN_EXECUTOR_RETURN ); for (vars.i = 0; vars.i < assets.length; vars.i++) { vars.currentAsset = assets[vars.i]; vars.currentAmount = amounts[vars.i]; - vars.currentPremium = vars.premiums[vars.i]; vars.currentATokenAddress = vars.aTokenAddresses[vars.i]; - vars.currentAmountPlusPremium = vars.currentAmount.add(vars.currentPremium); + vars.currentAmountPlusPremium = vars.currentAmount.add(vars.totalPremiums[vars.i]); + vars.currentPremiumToProtocol = amounts[vars.i].percentMul(vars.flashloanPremiumToProtocol); + vars.currentPremiumToLP = vars.totalPremiums[vars.i].sub(vars.currentPremiumToProtocol); if (DataTypes.InterestRateMode(modes[vars.i]) == DataTypes.InterestRateMode.NONE) { DataTypes.ReserveData storage reserve = _reserves[vars.currentAsset]; @@ -507,7 +515,10 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage reserve.updateState(reserveCache); reserve.cumulateToLiquidityIndex( IERC20(vars.currentATokenAddress).totalSupply(), - vars.currentPremium + vars.currentPremiumToLP + ); + reserve.accruedToTreasury = reserve.accruedToTreasury.add( + vars.currentPremiumToProtocol.rayDiv(reserve.liquidityIndex) ); reserve.updateInterestRates( reserveCache, @@ -541,7 +552,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage msg.sender, vars.currentAsset, vars.currentAmount, - vars.currentPremium, + vars.totalPremiums[vars.i], referralCode ); } @@ -734,21 +745,28 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage /** * @dev Returns the percentage of available liquidity that can be borrowed at once at stable rate */ - function MAX_STABLE_RATE_BORROW_SIZE_PERCENT() public view returns (uint256) { + function MAX_STABLE_RATE_BORROW_SIZE_PERCENT() public view override returns (uint256) { return _maxStableRateBorrowSizePercent; } /** - * @dev Returns the fee on flash loans + * @dev Returns the total fee on flash loans */ - function FLASHLOAN_PREMIUM_TOTAL() public view returns (uint256) { + function FLASHLOAN_PREMIUM_TOTAL() public view override returns (uint256) { return _flashLoanPremiumTotal; } + /** + * @dev Returns the part of the flashloan fees sent to protocol + */ + function FLASHLOAN_PREMIUM_TO_PROTOCOL() public view override returns (uint256) { + return _flashLoanPremiumToProtocol; + } + /** * @dev Returns the maximum number of reserves supported to be listed in this LendingPool */ - function MAX_NUMBER_RESERVES() public view returns (uint256) { + function MAX_NUMBER_RESERVES() public view override returns (uint256) { return _maxNumberOfReserves; } @@ -884,6 +902,11 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } } + /** + * @dev Authorizes/Unauthorizes a flash borrower. Authorized borrowers pay no flash loan premium + * @param flashBorrower address of the flash borrower + * @param authorized `true` to authorize, `false` to unauthorize + */ function updateFlashBorrowerAuthorization(address flashBorrower, bool authorized) external override @@ -892,10 +915,31 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _authorizedFlashBorrowers[flashBorrower] = authorized; } + /** + * @dev Returns whether a flashborrower is authorized (pays no premium) + * @param flashBorrower address of the flash borrower + * @return `true` if authorized, `false` if not + */ function isFlashBorrowerAuthorized(address flashBorrower) external view override returns (bool) { return _authorizedFlashBorrowers[flashBorrower]; } + /** + * @dev Updates flash loan premiums + * flash loan premium consist in 2 parts + * - A part is sent to aToken holders as extra balance + * - A part is collected by the protocol reserves + * @param flashLoanPremiumTotal total premium in bps + * @param flashLoanPremiumToProtocol part of the premium sent to protocol + */ + function updateFlashloanPremiums( + uint256 flashLoanPremiumTotal, + uint256 flashLoanPremiumToProtocol + ) external override onlyLendingPoolConfigurator { + _flashLoanPremiumTotal = flashLoanPremiumTotal; + _flashLoanPremiumToProtocol = flashLoanPremiumToProtocol; + } + struct ExecuteBorrowParams { address asset; address user; diff --git a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol index 29c725cf..3c699179 100644 --- a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol @@ -508,6 +508,26 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur return _riskAdmins[admin]; } + /// @inheritdoc ILendingPoolConfigurator + function updateFlashloanPremiumTotal(uint256 flashloanPremiumTotal) + external + override + onlyPoolAdmin + { + _pool.updateFlashloanPremiums(flashloanPremiumTotal, _pool.FLASHLOAN_PREMIUM_TO_PROTOCOL()); + emit FlashloanPremiumTotalUpdated(flashloanPremiumTotal); + } + + /// @inheritdoc ILendingPoolConfigurator + function updateFlashloanPremiumToProtocol(uint256 flashloanPremiumToProtocol) + external + override + onlyPoolAdmin + { + _pool.updateFlashloanPremiums(_pool.FLASHLOAN_PREMIUM_TOTAL(), flashloanPremiumToProtocol); + emit FlashloanPremiumToProcolUpdated(flashloanPremiumToProtocol); + } + function _initTokenWithProxy(address implementation, bytes memory initParams) internal returns (address) diff --git a/contracts/protocol/lendingpool/LendingPoolStorage.sol b/contracts/protocol/lendingpool/LendingPoolStorage.sol index 4f37e658..b8516bbf 100644 --- a/contracts/protocol/lendingpool/LendingPoolStorage.sol +++ b/contracts/protocol/lendingpool/LendingPoolStorage.sol @@ -31,4 +31,6 @@ contract LendingPoolStorage { uint256 internal _maxNumberOfReserves; mapping(address => bool) _authorizedFlashBorrowers; + + uint256 internal _flashLoanPremiumToProtocol; } diff --git a/test-suites/test-aave/flashloan.spec.ts b/test-suites/test-aave/flashloan.spec.ts index 911c4adc..91e162a5 100644 --- a/test-suites/test-aave/flashloan.spec.ts +++ b/test-suites/test-aave/flashloan.spec.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js'; import { TestEnv, makeSuite } from './helpers/make-suite'; -import { APPROVAL_AMOUNT_LENDING_POOL, oneRay } from '../../helpers/constants'; +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'; @@ -32,7 +32,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { }); it('Deposits WETH into the reserve', async () => { - const { pool, weth } = testEnv; + const { pool, weth, aave } = testEnv; const userAddress = await pool.signer.getAddress(); const amountToDeposit = ethers.utils.parseEther('1'); @@ -41,52 +41,124 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { 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'); }); - it('Takes WETH flashloan with mode = 0, returns the funds correctly', async () => { - const { pool, helpersContract, weth } = testEnv; + it('Takes WETH flash loan with mode = 0, returns the funds correctly', async () => { + const { pool, helpersContract, weth, aWETH } = testEnv; + + const flashBorrowedAmount = ethers.utils.parseEther('0.8'); + const fees = new BigNumber(flashBorrowedAmount.mul(9).div(10000).toString()); + + let reserveData = await helpersContract.getReserveData(weth.address); + + const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address], - [ethers.utils.parseEther('0.8')], + [flashBorrowedAmount], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); - ethers.utils.parseUnits('10000'); + await pool.mintToTreasury([weth.address]); - const reserveData = await helpersContract.getReserveData(weth.address); + reserveData = await helpersContract.getReserveData(weth.address); const currentLiquidityRate = reserveData.liquidityRate; const currentLiquidityIndex = reserveData.liquidityIndex; - const totalLiquidity = new BigNumber(reserveData.availableLiquidity.toString()) + const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) .plus(reserveData.totalStableDebt.toString()) .plus(reserveData.totalVariableDebt.toString()); - expect(totalLiquidity.toString()).to.be.equal('1000720000000000000'); + expect(totalLiquidityBefore.plus(fees).toString()).to.be.equal(totalLiquidityAfter.toString()); expect(currentLiquidityRate.toString()).to.be.equal('0'); expect(currentLiquidityIndex.toString()).to.be.equal('1000720000000000000000000000'); }); + 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 fees = 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]); + + ethers.utils.parseUnits('10000'); + + reserveData = await helpersContract.getReserveData(aave.address); + + const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); + + expect(totalLiquidityBefore.plus(fees).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 } = testEnv; - const reserveDataBefore = await helpersContract.getReserveData(weth.address); + let reserveData = await helpersContract.getReserveData(weth.address); + + const totalLiquidityBefore = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); + + const flashBorrowedAmount = totalLiquidityBefore.toString(); + + const fees = new BigNumber(flashBorrowedAmount).multipliedBy(9).dividedBy(10000).toString(); + const txResult = await pool.flashLoan( _mockFlashLoanReceiver.address, [weth.address], - ['1000720000000000000'], + [totalLiquidityBefore.toString()], [0], _mockFlashLoanReceiver.address, '0x10', '0' ); - const reserveData = await helpersContract.getReserveData(weth.address); + await pool.mintToTreasury([weth.address]); + + reserveData = await helpersContract.getReserveData(weth.address); + + const totalLiquidityAfter = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); const currentLiqudityRate = reserveData.liquidityRate; const currentLiquidityIndex = reserveData.liquidityIndex; @@ -177,6 +249,12 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { 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( @@ -191,14 +269,25 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { 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];