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 e49a53f1..08cbd170 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -89,6 +89,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _maxStableRateBorrowSizePercent = 2500; _flashLoanPremiumTotal = 9; _maxNumberOfReserves = 128; + _flashLoanPremiumToProtocol = 0; } /** @@ -432,10 +433,12 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address currentAsset; address currentATokenAddress; uint256 currentAmount; - uint256 currentPremium; + uint256 currentPremiumToLP; + uint256 currentPremiumToProtocol; uint256 currentAmountPlusPremium; address debtToken; uint256 flashloanPremiumTotal; + uint256 flashloanPremiumToProtocol; } /** @@ -469,36 +472,44 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ValidationLogic.validateFlashloan(assets, amounts, _reserves); address[] memory aTokenAddresses = new address[](assets.length); - uint256[] memory premiums = new uint256[](assets.length); + uint256[] memory totalPremiums = new uint256[](assets.length); 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++) { aTokenAddresses[vars.i] = _reserves[assets[vars.i]].aTokenAddress; - premiums[vars.i] = amounts[vars.i].percentMul(vars.flashloanPremiumTotal); + totalPremiums[vars.i] = amounts[vars.i].percentMul(vars.flashloanPremiumTotal); + vars.currentPremiumToProtocol = amounts[vars.i].percentMul(vars.flashloanPremiumToProtocol); + vars.currentPremiumToLP = totalPremiums[vars.i].sub(vars.currentPremiumToProtocol); IAToken(aTokenAddresses[vars.i]).transferUnderlyingTo(receiverAddress, amounts[vars.i]); } require( - vars.receiver.executeOperation(assets, amounts, premiums, msg.sender, params), + vars.receiver.executeOperation(assets, amounts, 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 = premiums[vars.i]; vars.currentATokenAddress = aTokenAddresses[vars.i]; - vars.currentAmountPlusPremium = vars.currentAmount.add(vars.currentPremium); + vars.currentAmountPlusPremium = vars.currentAmount.add(totalPremiums[vars.i]); if (DataTypes.InterestRateMode(modes[vars.i]) == DataTypes.InterestRateMode.NONE) { _reserves[vars.currentAsset].updateState(); _reserves[vars.currentAsset].cumulateToLiquidityIndex( IERC20(vars.currentATokenAddress).totalSupply(), - vars.currentPremium + vars.currentPremiumToLP ); + _reserves[vars.currentAsset].accruedToTreasury = _reserves[vars.currentAsset] + .accruedToTreasury + .add(vars.currentPremiumToProtocol.rayDiv(_reserves[vars.currentAsset].liquidityIndex)); _reserves[vars.currentAsset].updateInterestRates( vars.currentAsset, vars.currentATokenAddress, @@ -532,7 +543,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage msg.sender, vars.currentAsset, vars.currentAmount, - vars.currentPremium, + totalPremiums[vars.i], referralCode ); } @@ -725,21 +736,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; } @@ -875,6 +893,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 @@ -883,10 +906,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/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index e57f55fa..d71cd15c 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -40,7 +40,7 @@ library ValidationLogic { * @param reserve The reserve object on which the user is depositing * @param amount The amount to be deposited */ - function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) internal view { + function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) external view { DataTypes.ReserveConfigurationMap memory reserveConfiguration = reserve.configuration; (bool isActive, bool isFrozen, , , bool isPaused) = reserveConfiguration.getFlagsMemory(); (, , , uint256 reserveDecimals, ) = reserveConfiguration.getParamsMemory(); @@ -453,7 +453,7 @@ library ValidationLogic { mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle - ) internal view { + ) external view { (, , , , uint256 healthFactor) = GenericLogic.calculateUserAccountData( from, @@ -474,7 +474,7 @@ library ValidationLogic { * @dev Validates a transfer action * @param reserve The reserve object */ - function validateTransfer(DataTypes.ReserveData storage reserve) internal view { + function validateTransfer(DataTypes.ReserveData storage reserve) external view { require(!reserve.configuration.getPaused(), Errors.VL_RESERVE_PAUSED); } 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];