diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index ab012fb8..86e58f61 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -167,6 +167,13 @@ interface ILendingPool { uint256 variableBorrowIndex ); + /** + * @dev Emitted when the protocol treasury receives minted aTokens from the accrued interest. + * @param reserve the address of the reserve + * @param amountMinted the amount minted to the treasury + **/ + event MintedToTreasury(address indexed reserve, uint256 amountMinted); + /** * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. * - E.g. User deposits 100 USDC and gets in return 100 aUSDC @@ -396,6 +403,8 @@ interface ILendingPool { address interestRateStrategyAddress ) external; + function dropReserve(address reserve) external; + function setReserveInterestRateStrategyAddress(address reserve, address rateStrategyAddress) external; @@ -458,4 +467,21 @@ interface ILendingPool { function setPause(bool val) external; function paused() external view returns (bool); + + 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 9615f7f5..e3803ae6 100644 --- a/contracts/interfaces/ILendingPoolConfigurator.sol +++ b/contracts/interfaces/ILendingPoolConfigurator.sol @@ -132,6 +132,12 @@ interface ILendingPoolConfigurator { **/ event ReserveUnpaused(address indexed asset); + /** + * @dev Emitted when a reserve is dropped + * @param asset The address of the underlying asset of the reserve + **/ + event ReserveDropped(address indexed asset); + /** * @dev Emitted when a reserve factor is updated * @param asset The address of the underlying asset of the reserve @@ -210,6 +216,18 @@ interface ILendingPoolConfigurator { address indexed implementation ); + /** + * @dev Emitted when a new borrower is authorized (fees = 0) + * @param flashBorrower The address of the authorized borrower + **/ + event FlashBorrowerAuthorized(address indexed flashBorrower); + + /** + * @dev Emitted when a borrower is unauthorized + * @param flashBorrower The address of the unauthorized borrower + **/ + event FlashBorrowerUnauthorized(address indexed flashBorrower); + /** * @dev Emitted when a new risk admin is registered * @param admin the newly registered admin @@ -222,6 +240,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 @@ -390,4 +420,37 @@ interface ILendingPoolConfigurator { * @param admin The address of the potential admin **/ function isRiskAdmin(address admin) external view returns (bool); + + /** + * @dev Authorize a new borrower (fees are 0 for the authorized borrower) + * @param flashBorrower The address of the authorized borrower + **/ + function authorizeFlashBorrower(address flashBorrower) external; + + /** + * @dev Unauthorize a borrower + * @param flashBorrower The address of the unauthorized borrower + **/ + function unauthorizeFlashBorrower(address flashBorrower) external; + + /** + * @dev Drops a reserve entirely + * @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 bb46107d..3d7f0083 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -49,6 +49,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage using WadRayMath for uint256; using PercentageMath for uint256; using SafeERC20 for IERC20; + using ReserveLogic for DataTypes.ReserveCache; uint256 public constant LENDINGPOOL_REVISION = 0x2; @@ -89,6 +90,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _maxStableRateBorrowSizePercent = 2500; _flashLoanPremiumTotal = 9; _maxNumberOfReserves = 128; + _flashLoanPremiumToProtocol = 0; } /** @@ -189,7 +191,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint16 referralCode, address onBehalfOf ) external override whenNotPaused { - DataTypes.ReserveData storage reserve = _reserves[asset]; _executeBorrow( ExecuteBorrowParams( asset, @@ -197,7 +198,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage onBehalfOf, amount, interestRateMode, - reserve.aTokenAddress, referralCode, true ) @@ -270,6 +270,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage **/ function swapBorrowRateMode(address asset, uint256 rateMode) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(msg.sender, reserve); @@ -277,37 +278,40 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ValidationLogic.validateSwapRateMode( reserve, + reserveCache, _usersConfig[msg.sender], stableDebt, variableDebt, interestRateMode ); - reserve.updateState(); + reserve.updateState(reserveCache); if (interestRateMode == DataTypes.InterestRateMode.STABLE) { - IStableDebtToken(reserve.stableDebtTokenAddress).burn(msg.sender, stableDebt); - IVariableDebtToken(reserve.variableDebtTokenAddress).mint( + IStableDebtToken(reserveCache.stableDebtTokenAddress).burn(msg.sender, stableDebt); + IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint( msg.sender, msg.sender, stableDebt, - reserve.variableBorrowIndex + reserveCache.nextVariableBorrowIndex ); + reserveCache.refreshDebt(0, stableDebt, stableDebt, 0); } else { - IVariableDebtToken(reserve.variableDebtTokenAddress).burn( + IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn( msg.sender, variableDebt, - reserve.variableBorrowIndex + reserveCache.nextVariableBorrowIndex ); - IStableDebtToken(reserve.stableDebtTokenAddress).mint( + IStableDebtToken(reserveCache.stableDebtTokenAddress).mint( msg.sender, msg.sender, variableDebt, reserve.currentStableBorrowRate ); + reserveCache.refreshDebt(variableDebt, 0, 0, variableDebt); } - reserve.updateInterestRates(asset, reserve.aTokenAddress, 0, 0); + reserve.updateInterestRates(reserveCache, asset, 0, 0); emit Swap(asset, msg.sender, rateMode); } @@ -323,22 +327,22 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage **/ function rebalanceStableBorrowRate(address asset, address user) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); - IERC20 stableDebtToken = IERC20(reserve.stableDebtTokenAddress); - IERC20 variableDebtToken = IERC20(reserve.variableDebtTokenAddress); - address aTokenAddress = reserve.aTokenAddress; - + IERC20 stableDebtToken = IERC20(reserveCache.stableDebtTokenAddress); + IERC20 variableDebtToken = IERC20(reserveCache.variableDebtTokenAddress); uint256 stableDebt = IERC20(stableDebtToken).balanceOf(user); ValidationLogic.validateRebalanceStableBorrowRate( reserve, + reserveCache, asset, stableDebtToken, variableDebtToken, - aTokenAddress + reserveCache.aTokenAddress ); - reserve.updateState(); + reserve.updateState(reserveCache); IStableDebtToken(address(stableDebtToken)).burn(user, stableDebt); IStableDebtToken(address(stableDebtToken)).mint( @@ -348,7 +352,9 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage reserve.currentStableBorrowRate ); - reserve.updateInterestRates(asset, aTokenAddress, 0, 0); + reserveCache.refreshDebt(stableDebt, stableDebt, 0, 0); + + reserve.updateInterestRates(reserveCache, asset, 0, 0); emit RebalanceStableBorrowRate(asset, user); } @@ -364,8 +370,9 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); - ValidationLogic.validateSetUseReserveAsCollateral(reserve); + ValidationLogic.validateSetUseReserveAsCollateral(reserveCache); _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, useAsCollateral); @@ -433,9 +440,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[] totalPremiums; + uint256 flashloanPremiumTotal; + uint256 flashloanPremiumToProtocol; } /** @@ -466,42 +478,52 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ) external override whenNotPaused { FlashLoanLocalVars memory vars; + vars.aTokenAddresses = new address[](assets.length); + vars.totalPremiums = new uint256[](assets.length); + ValidationLogic.validateFlashloan(assets, amounts, _reserves); - address[] memory aTokenAddresses = new address[](assets.length); - uint256[] memory premiums = new uint256[](assets.length); - vars.receiver = IFlashLoanReceiver(receiverAddress); + (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].mul(_flashLoanPremiumTotal).div(10000); - - IAToken(aTokenAddresses[vars.i]).transferUnderlyingTo(receiverAddress, amounts[vars.i]); + vars.aTokenAddresses[vars.i] = _reserves[assets[vars.i]].aTokenAddress; + 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, 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 = premiums[vars.i]; - vars.currentATokenAddress = aTokenAddresses[vars.i]; - vars.currentAmountPlusPremium = vars.currentAmount.add(vars.currentPremium); + vars.currentATokenAddress = vars.aTokenAddresses[vars.i]; + 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) { - _reserves[vars.currentAsset].updateState(); - _reserves[vars.currentAsset].cumulateToLiquidityIndex( + DataTypes.ReserveData storage reserve = _reserves[vars.currentAsset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + reserve.cumulateToLiquidityIndex( IERC20(vars.currentATokenAddress).totalSupply(), - vars.currentPremium + vars.currentPremiumToLP ); - _reserves[vars.currentAsset].updateInterestRates( + reserve.accruedToTreasury = reserve.accruedToTreasury.add( + vars.currentPremiumToProtocol.rayDiv(reserve.liquidityIndex) + ); + reserve.updateInterestRates( + reserveCache, vars.currentAsset, - vars.currentATokenAddress, vars.currentAmountPlusPremium, 0 ); @@ -521,7 +543,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage onBehalfOf, vars.currentAmount, modes[vars.i], - vars.currentATokenAddress, referralCode, false ) @@ -532,12 +553,40 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage msg.sender, vars.currentAsset, vars.currentAmount, - vars.currentPremium, + vars.totalPremiums[vars.i], referralCode ); } } + /** + * @dev Mints the assets accrued through the reserve factor to the treasury in the form of aTokens + * @param reserves The list of reserves for which the minting needs to be executed + **/ + function mintToTreasury(address[] calldata reserves) public { + for (uint256 i = 0; i < reserves.length; i++) { + address reserveAddress = reserves[i]; + + DataTypes.ReserveData storage reserve = _reserves[reserveAddress]; + + // this cover both inactive reserves and invalid reserves since the flag will be 0 for both + if (!reserve.configuration.getActive()) { + continue; + } + + uint256 accruedToTreasury = reserve.accruedToTreasury; + + if (accruedToTreasury != 0) { + uint256 normalizedIncome = reserve.getNormalizedIncome(); + uint256 amountToMint = accruedToTreasury.rayMul(normalizedIncome); + IAToken(reserve.aTokenAddress).mintToTreasury(amountToMint, normalizedIncome); + + reserve.accruedToTreasury = 0; + emit MintedToTreasury(reserveAddress, amountToMint); + } + } + } + /** * @dev Returns the state and configuration of the reserve * @param asset The address of the underlying asset of the reserve @@ -581,7 +630,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ltv, currentLiquidationThreshold, healthFactor - ) = GenericLogic.calculateUserAccountData( + ) = GenericLogic.getUserAccountData( user, _reserves, _usersConfig[user], @@ -662,15 +711,29 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } /** - * @dev Returns the list of the initialized reserves + * @dev Returns the list of the initialized reserves, does not contain dropped reserves **/ function getReservesList() external view override returns (address[] memory) { - address[] memory _activeReserves = new address[](_reservesCount); + uint256 reserveListCount = _reservesCount; + uint256 droppedReservesCount = 0; + address[] memory reserves = new address[](reserveListCount); - for (uint256 i = 0; i < _reservesCount; i++) { - _activeReserves[i] = _reservesList[i]; + for (uint256 i = 0; i < reserveListCount; i++) { + if (_reservesList[i] != address(0)) { + reserves[i - droppedReservesCount] = _reservesList[i]; + } else { + droppedReservesCount++; + } } - return _activeReserves; + + if (droppedReservesCount == 0) return reserves; + + address[] memory undroppedReserves = new address[](reserveListCount - droppedReservesCount); + for (uint256 i = 0; i < reserveListCount - droppedReservesCount; i++) { + undroppedReserves[i] = reserves[i]; + } + + return undroppedReserves; } /** @@ -683,21 +746,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; } @@ -781,6 +851,17 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _addReserveToList(asset); } + /** + * @dev Drop a reserve + * - Only callable by the LendingPoolConfigurator contract + * @param asset The address of the underlying asset of the reserve + **/ + function dropReserve(address asset) external override onlyLendingPoolConfigurator { + ValidationLogic.validateDropReserve(_reserves[asset]); + _removeReserveFromList(asset); + delete _reserves[asset]; + } + /** * @dev Updates the address of the interest rate strategy contract * - Only callable by the LendingPoolConfigurator contract @@ -823,13 +904,50 @@ 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 + onlyLendingPoolConfigurator + { + _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; address onBehalfOf; uint256 amount; uint256 interestRateMode; - address aTokenAddress; uint16 referralCode; bool releaseUnderlying; } @@ -837,50 +955,45 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage function _executeBorrow(ExecuteBorrowParams memory vars) internal { DataTypes.ReserveData storage reserve = _reserves[vars.asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[vars.onBehalfOf]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); - address oracle = _addressesProvider.getPriceOracle(); - - uint256 amountInETH = - IPriceOracleGetter(oracle).getAssetPrice(vars.asset).mul(vars.amount).div( - 10**reserve.configuration.getDecimals() - ); + reserve.updateState(reserveCache); ValidationLogic.validateBorrow( + reserveCache, vars.asset, - reserve, vars.onBehalfOf, vars.amount, - amountInETH, vars.interestRateMode, _maxStableRateBorrowSizePercent, _reserves, userConfig, _reservesList, _reservesCount, - oracle + _addressesProvider.getPriceOracle() ); - reserve.updateState(); - uint256 currentStableRate = 0; - bool isFirstBorrowing = false; + if (DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE) { currentStableRate = reserve.currentStableBorrowRate; - isFirstBorrowing = IStableDebtToken(reserve.stableDebtTokenAddress).mint( + isFirstBorrowing = IStableDebtToken(reserveCache.stableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, currentStableRate ); + reserveCache.refreshDebt(vars.amount, 0, 0, 0); } else { - isFirstBorrowing = IVariableDebtToken(reserve.variableDebtTokenAddress).mint( + isFirstBorrowing = IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, - reserve.variableBorrowIndex + reserveCache.nextVariableBorrowIndex ); + reserveCache.refreshDebt(0, 0, vars.amount, 0); } if (isFirstBorrowing) { @@ -888,14 +1001,14 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } reserve.updateInterestRates( + reserveCache, vars.asset, - vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0 ); if (vars.releaseUnderlying) { - IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); + IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } emit Borrow( @@ -918,17 +1031,18 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint16 referralCode ) internal { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); - ValidationLogic.validateDeposit(reserve, amount); + reserve.updateState(reserveCache); - address aToken = reserve.aTokenAddress; + ValidationLogic.validateDeposit(reserveCache, amount); - reserve.updateState(); - reserve.updateInterestRates(asset, aToken, amount, 0); + reserve.updateInterestRates(reserveCache, asset, amount, 0); - IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); + IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, amount); - bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); + bool isFirstDeposit = + IAToken(reserveCache.aTokenAddress).mint(onBehalfOf, amount, reserveCache.nextLiquidityIndex); if (isFirstDeposit) { _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); @@ -945,14 +1059,14 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ) internal returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[msg.sender]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); - address aToken = reserve.aTokenAddress; + reserve.updateState(reserveCache); - reserve.updateState(); - - uint256 liquidityIndex = reserve.liquidityIndex; - - uint256 userBalance = IAToken(aToken).scaledBalanceOf(msg.sender).rayMul(liquidityIndex); + uint256 userBalance = + IAToken(reserveCache.aTokenAddress).scaledBalanceOf(msg.sender).rayMul( + reserveCache.nextLiquidityIndex + ); uint256 amountToWithdraw = amount; @@ -960,11 +1074,16 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage amountToWithdraw = userBalance; } - ValidationLogic.validateWithdraw(reserve, amountToWithdraw, userBalance); + ValidationLogic.validateWithdraw(reserveCache, amountToWithdraw, userBalance); - reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); + reserve.updateInterestRates(reserveCache, asset, 0, amountToWithdraw); - IAToken(aToken).burn(msg.sender, to, amountToWithdraw, liquidityIndex); + IAToken(reserveCache.aTokenAddress).burn( + msg.sender, + to, + amountToWithdraw, + reserveCache.nextLiquidityIndex + ); if (userConfig.isUsingAsCollateral(reserve.id)) { if (userConfig.isBorrowingAny()) { @@ -997,13 +1116,14 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address onBehalfOf ) internal returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateRepay( - reserve, + reserveCache, amount, interestRateMode, onBehalfOf, @@ -1018,35 +1138,36 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage paybackAmount = amount; } - reserve.updateState(); + reserve.updateState(reserveCache); if (interestRateMode == DataTypes.InterestRateMode.STABLE) { - IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); + IStableDebtToken(reserveCache.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); + reserveCache.refreshDebt(0, paybackAmount, 0, 0); } else { - IVariableDebtToken(reserve.variableDebtTokenAddress).burn( + IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn( onBehalfOf, paybackAmount, - reserve.variableBorrowIndex + reserveCache.nextVariableBorrowIndex ); + reserveCache.refreshDebt(0, 0, 0, paybackAmount); } - address aToken = reserve.aTokenAddress; - reserve.updateInterestRates(asset, aToken, paybackAmount, 0); + reserve.updateInterestRates(reserveCache, asset, paybackAmount, 0); if (stableDebt.add(variableDebt).sub(paybackAmount) == 0) { _usersConfig[onBehalfOf].setBorrowing(reserve.id, false); } - IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); + IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, paybackAmount); - IAToken(aToken).handleRepayment(msg.sender, paybackAmount); + IAToken(reserveCache.aTokenAddress).handleRepayment(msg.sender, paybackAmount); emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); return paybackAmount; } - function _addReserveToList(address asset) internal { + function _addReserveToList(address asset) internal returns (uint8) { uint256 reservesCount = _reservesCount; require(reservesCount < _maxNumberOfReserves, Errors.LP_NO_MORE_RESERVES_ALLOWED); @@ -1054,10 +1175,18 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage bool reserveAlreadyAdded = _reserves[asset].id != 0 || _reservesList[0] == asset; if (!reserveAlreadyAdded) { - _reserves[asset].id = uint8(reservesCount); - _reservesList[reservesCount] = asset; - - _reservesCount = reservesCount + 1; + for (uint8 i = 0; i <= reservesCount; i++) { + if (_reservesList[i] == address(0)) { + _reserves[asset].id = i; + _reservesList[i] = asset; + _reservesCount = reservesCount + 1; + return i; + } + } } } + + function _removeReserveFromList(address asset) internal { + _reservesList[_reserves[asset].id] = address(0); + } } diff --git a/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol b/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol index 80692726..e530a9a4 100644 --- a/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol +++ b/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + import {SafeMath} from '../../dependencies/openzeppelin/contracts//SafeMath.sol'; import {IERC20} from '../../dependencies/openzeppelin/contracts//IERC20.sol'; import {IAToken} from '../../interfaces/IAToken.sol'; @@ -9,7 +11,7 @@ import {IVariableDebtToken} from '../../interfaces/IVariableDebtToken.sol'; import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; import {ILendingPoolCollateralManager} from '../../interfaces/ILendingPoolCollateralManager.sol'; import {VersionedInitializable} from '../libraries/aave-upgradeability/VersionedInitializable.sol'; -import {GenericLogic} from '../libraries/logic/GenericLogic.sol'; +import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; import {Helpers} from '../libraries/helpers/Helpers.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {PercentageMath} from '../libraries/math/PercentageMath.sol'; @@ -35,6 +37,7 @@ contract LendingPoolCollateralManager is using SafeMath for uint256; using WadRayMath for uint256; using PercentageMath for uint256; + using ReserveLogic for DataTypes.ReserveCache; uint256 internal constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 5000; @@ -52,6 +55,7 @@ contract LendingPoolCollateralManager is uint256 healthFactor; uint256 liquidatorPreviousATokenBalance; IAToken collateralAtoken; + IPriceOracleGetter oracle; bool isCollateralEnabled; DataTypes.InterestRateMode borrowRateMode; uint256 errorCode; @@ -87,28 +91,25 @@ contract LendingPoolCollateralManager is ) external override returns (uint256, string memory) { DataTypes.ReserveData storage collateralReserve = _reserves[collateralAsset]; DataTypes.ReserveData storage debtReserve = _reserves[debtAsset]; + DataTypes.ReserveCache memory debtReserveCache = debtReserve.cache(); DataTypes.UserConfigurationMap storage userConfig = _usersConfig[user]; LiquidationCallLocalVars memory vars; - (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + + (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve); + vars.oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle()); + + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( + collateralReserve, + debtReserveCache, + vars.userStableDebt.add(vars.userVariableDebt), user, _reserves, userConfig, _reservesList, _reservesCount, - _addressesProvider.getPriceOracle() - ); - - (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve); - - (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( - collateralReserve, - debtReserve, - userConfig, - vars.healthFactor, - vars.userStableDebt, - vars.userVariableDebt + address(vars.oracle) ); if (Errors.CollateralManagerErrors(vars.errorCode) != Errors.CollateralManagerErrors.NO_ERROR) { @@ -132,11 +133,12 @@ contract LendingPoolCollateralManager is vars.debtAmountNeeded ) = _calculateAvailableCollateralToLiquidate( collateralReserve, - debtReserve, + debtReserveCache, collateralAsset, debtAsset, vars.actualDebtToLiquidate, - vars.userCollateralBalance + vars.userCollateralBalance, + vars.oracle ); // If debtAmountNeeded < actualDebtToLiquidate, there isn't enough @@ -160,35 +162,38 @@ contract LendingPoolCollateralManager is } } - debtReserve.updateState(); + debtReserve.updateState(debtReserveCache); if (vars.userVariableDebt >= vars.actualDebtToLiquidate) { - IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + IVariableDebtToken(debtReserveCache.variableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate, - debtReserve.variableBorrowIndex + debtReserveCache.nextVariableBorrowIndex ); + debtReserveCache.refreshDebt(0, 0, 0, vars.actualDebtToLiquidate); + debtReserve.updateInterestRates(debtReserveCache, debtAsset, vars.actualDebtToLiquidate, 0); } else { // If the user doesn't have variable debt, no need to try to burn variable debt tokens if (vars.userVariableDebt > 0) { - IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + IVariableDebtToken(debtReserveCache.variableDebtTokenAddress).burn( user, vars.userVariableDebt, - debtReserve.variableBorrowIndex + debtReserveCache.nextVariableBorrowIndex ); } - IStableDebtToken(debtReserve.stableDebtTokenAddress).burn( + IStableDebtToken(debtReserveCache.stableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate.sub(vars.userVariableDebt) ); - } + debtReserveCache.refreshDebt( + 0, + vars.actualDebtToLiquidate.sub(vars.userVariableDebt), + 0, + vars.userVariableDebt + ); - debtReserve.updateInterestRates( - debtAsset, - debtReserve.aTokenAddress, - vars.actualDebtToLiquidate, - 0 - ); + debtReserve.updateInterestRates(debtReserveCache, debtAsset, vars.actualDebtToLiquidate, 0); + } if (receiveAToken) { vars.liquidatorPreviousATokenBalance = IERC20(vars.collateralAtoken).balanceOf(msg.sender); @@ -200,10 +205,11 @@ contract LendingPoolCollateralManager is emit ReserveUsedAsCollateralEnabled(collateralAsset, msg.sender); } } else { - collateralReserve.updateState(); + DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache(); + collateralReserve.updateState(collateralReserveCache); collateralReserve.updateInterestRates( + collateralReserveCache, collateralAsset, - address(vars.collateralAtoken), 0, vars.maxCollateralToLiquidate ); @@ -213,7 +219,7 @@ contract LendingPoolCollateralManager is user, msg.sender, vars.maxCollateralToLiquidate, - collateralReserve.liquidityIndex + collateralReserveCache.nextLiquidityIndex ); } @@ -227,7 +233,7 @@ contract LendingPoolCollateralManager is // Transfers the debt asset being repaid to the aToken, where the liquidity is kept IERC20(debtAsset).safeTransferFrom( msg.sender, - debtReserve.aTokenAddress, + debtReserveCache.aTokenAddress, vars.actualDebtToLiquidate ); @@ -260,7 +266,7 @@ contract LendingPoolCollateralManager is * - This function needs to be called after all the checks to validate the liquidation have been performed, * otherwise it might fail. * @param collateralReserve The data of the collateral reserve - * @param debtReserve The data of the debt reserve + * @param debtReserveCache The cached data of the debt reserve * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover @@ -271,15 +277,15 @@ contract LendingPoolCollateralManager is **/ function _calculateAvailableCollateralToLiquidate( DataTypes.ReserveData storage collateralReserve, - DataTypes.ReserveData storage debtReserve, + DataTypes.ReserveCache memory debtReserveCache, address collateralAsset, address debtAsset, uint256 debtToCover, - uint256 userCollateralBalance + uint256 userCollateralBalance, + IPriceOracleGetter oracle ) internal view returns (uint256, uint256) { uint256 collateralAmount = 0; uint256 debtAmountNeeded = 0; - IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle()); AvailableCollateralToLiquidateLocalVars memory vars; @@ -289,7 +295,7 @@ contract LendingPoolCollateralManager is (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve .configuration .getParams(); - vars.debtAssetDecimals = debtReserve.configuration.getDecimals(); + vars.debtAssetDecimals = debtReserveCache.reserveConfiguration.getDecimalsMemory(); // This is the maximum possible amount of the selected collateral that can be liquidated, given the // max amount of liquidatable debt diff --git a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol index 588249e8..d6f02592 100644 --- a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol @@ -159,6 +159,12 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur ); } + /// @inheritdoc ILendingPoolConfigurator + function dropReserve(address asset) external override onlyPoolAdmin { + _pool.dropReserve(asset); + emit ReserveDropped(asset); + } + /// @inheritdoc ILendingPoolConfigurator function updateAToken(UpdateATokenInput calldata input) external override onlyPoolAdmin { ILendingPool cachedPool = _pool; @@ -229,7 +235,6 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur onlyPoolAdmin { ILendingPool cachedPool = _pool; - DataTypes.ReserveData memory reserveData = cachedPool.getReserveData(input.asset); (, , , uint256 decimals, ) = cachedPool.getConfiguration(input.asset).getParamsMemory(); @@ -504,11 +509,43 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur emit RiskAdminUnregistered(admin); } + /// @inheritdoc ILendingPoolConfigurator + function authorizeFlashBorrower(address flashBorrower) external override onlyPoolAdmin { + _pool.updateFlashBorrowerAuthorization(flashBorrower, true); + emit FlashBorrowerAuthorized(flashBorrower); + } + + /// @inheritdoc ILendingPoolConfigurator + function unauthorizeFlashBorrower(address flashBorrower) external override onlyPoolAdmin { + _pool.updateFlashBorrowerAuthorization(flashBorrower, false); + emit FlashBorrowerUnauthorized(flashBorrower); + } + /// @inheritdoc ILendingPoolConfigurator function isRiskAdmin(address admin) external view override onlyPoolAdmin returns (bool) { 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 198a3eea..b8516bbf 100644 --- a/contracts/protocol/lendingpool/LendingPoolStorage.sol +++ b/contracts/protocol/lendingpool/LendingPoolStorage.sol @@ -29,4 +29,8 @@ contract LendingPoolStorage { uint256 internal _flashLoanPremiumTotal; uint256 internal _maxNumberOfReserves; + + mapping(address => bool) _authorizedFlashBorrowers; + + uint256 internal _flashLoanPremiumToProtocol; } diff --git a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol index b547a7f1..ba2aa0c4 100644 --- a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol +++ b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol @@ -68,6 +68,16 @@ library ReserveConfiguration { return self.data & ~LTV_MASK; } + /** + * @dev Gets the Loan to Value of the reserve + * @param self The reserve configuration + * @return The loan to value + **/ + function getLtvMemory(DataTypes.ReserveConfigurationMap memory self) internal pure returns (uint256) { + return self.data & ~LTV_MASK; + } + + /** * @dev Sets the liquidation threshold of the reserve * @param self The reserve configuration @@ -153,6 +163,19 @@ library ReserveConfiguration { return (self.data & ~DECIMALS_MASK) >> RESERVE_DECIMALS_START_BIT_POSITION; } + /** + * @dev Gets the decimals of the underlying asset of the reserve + * @param self The reserve configuration + * @return The decimals of the asset + **/ + function getDecimalsMemory(DataTypes.ReserveConfigurationMap memory self) + internal + pure + returns (uint256) + { + return (self.data & ~DECIMALS_MASK) >> RESERVE_DECIMALS_START_BIT_POSITION; + } + /** * @dev Sets the active state of the reserve * @param self The reserve configuration @@ -296,6 +319,19 @@ library ReserveConfiguration { return (self.data & ~RESERVE_FACTOR_MASK) >> RESERVE_FACTOR_START_BIT_POSITION; } + /** + * @dev Gets the reserve factor of the reserve + * @param self The reserve configuration + * @return The reserve factor + **/ + function getReserveFactorMemory(DataTypes.ReserveConfigurationMap memory self) + internal + pure + returns (uint256) + { + return (self.data & ~RESERVE_FACTOR_MASK) >> RESERVE_FACTOR_START_BIT_POSITION; + } + /** * @dev Sets the borrow cap of the reserve * @param self The reserve configuration diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index ccee2a7b..9e3f0e5a 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -109,8 +109,11 @@ library Errors { string public constant LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN = '85'; string public constant VL_RESERVE_PAUSED = '86'; string public constant LPC_CALLER_NOT_RISK_OR_POOL_ADMIN = '87'; - string public constant RC_INVALID_EXPOSURE_CAP = '88'; - string public constant VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '89'; + string public constant RL_ATOKEN_SUPPLY_NOT_ZERO = '88'; + string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; + string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; + string public constant RC_INVALID_EXPOSURE_CAP = '91'; + string public constant VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '92'; enum CollateralManagerErrors { NO_ERROR, diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index b32a2e9f..77823c5b 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -95,6 +95,9 @@ library GenericLogic { } vars.currentReserveAddress = reserves[vars.i]; + + if (vars.currentReserveAddress == address(0)) continue; + DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve @@ -201,4 +204,36 @@ library GenericLogic { availableBorrowsETH = availableBorrowsETH.sub(totalDebtInETH); return availableBorrowsETH; } + + /** + * @dev proxy call for calculateUserAccountData as external function. + * Used in LendingPool to work around contract size limit issues + * @param user The address of the user + * @param reservesData Data of all the reserves + * @param userConfig The configuration of the user + * @param reserves The list of the available reserves + * @param oracle The price oracle address + * @return The total collateral and total debt of the user in ETH, the avg ltv, liquidation threshold and the HF + **/ + function getUserAccountData( + address user, + mapping(address => DataTypes.ReserveData) storage reservesData, + DataTypes.UserConfigurationMap memory userConfig, + mapping(uint256 => address) storage reserves, + uint256 reservesCount, + address oracle + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + uint256 + ) + { + return + calculateUserAccountData(user, reservesData, userConfig, reserves, reservesCount, oracle); + } } diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index 2b5b2cf4..bbcc6fb8 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -107,30 +107,12 @@ library ReserveLogic { * @dev Updates the liquidity cumulative index and the variable borrow index. * @param reserve the reserve object **/ - function updateState(DataTypes.ReserveData storage reserve) internal { - uint256 scaledVariableDebt = - IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply(); - uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex; - uint256 previousLiquidityIndex = reserve.liquidityIndex; - uint40 lastUpdatedTimestamp = reserve.lastUpdateTimestamp; - - (uint256 newLiquidityIndex, uint256 newVariableBorrowIndex) = - _updateIndexes( - reserve, - scaledVariableDebt, - previousLiquidityIndex, - previousVariableBorrowIndex, - lastUpdatedTimestamp - ); - - _mintToTreasury( - reserve, - scaledVariableDebt, - previousVariableBorrowIndex, - newLiquidityIndex, - newVariableBorrowIndex, - lastUpdatedTimestamp - ); + function updateState( + DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache + ) internal { + _updateIndexes(reserve, reserveCache); + _accrueToTreasury(reserve, reserveCache); } /** @@ -179,9 +161,6 @@ library ReserveLogic { } struct UpdateInterestRatesLocalVars { - address stableDebtTokenAddress; - uint256 availableLiquidity; - uint256 totalStableDebt; uint256 newLiquidityRate; uint256 newStableRate; uint256 newVariableRate; @@ -197,38 +176,29 @@ library ReserveLogic { **/ function updateInterestRates( DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, address reserveAddress, - address aTokenAddress, uint256 liquidityAdded, uint256 liquidityTaken ) internal { UpdateInterestRatesLocalVars memory vars; - vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress; - - (vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress) - .getTotalSupplyAndAvgRate(); - - //calculates the total variable debt locally using the scaled total supply instead - //of totalSupply(), as it's noticeably cheaper. Also, the index has been - //updated by the previous updateState() call - vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress) - .scaledTotalSupply() - .rayMul(reserve.variableBorrowIndex); - + vars.totalVariableDebt = reserveCache.nextScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); ( vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates( reserveAddress, - aTokenAddress, + reserveCache.aTokenAddress, liquidityAdded, liquidityTaken, - vars.totalStableDebt, + reserveCache.nextTotalStableDebt, vars.totalVariableDebt, - vars.avgStableRate, - reserve.configuration.getReserveFactor() + reserveCache.nextAvgStableBorrowRate, + reserveCache.reserveConfiguration.getReserveFactorMemory() ); require(vars.newLiquidityRate <= type(uint128).max, Errors.RL_LIQUIDITY_RATE_OVERFLOW); require(vars.newStableRate <= type(uint128).max, Errors.RL_STABLE_BORROW_RATE_OVERFLOW); @@ -243,17 +213,15 @@ library ReserveLogic { vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate, - reserve.liquidityIndex, - reserve.variableBorrowIndex + reserveCache.nextLiquidityIndex, + reserveCache.nextVariableBorrowIndex ); } struct MintToTreasuryLocalVars { - uint256 currentStableDebt; - uint256 principalStableDebt; - uint256 previousStableDebt; - uint256 currentVariableDebt; - uint256 previousVariableDebt; + uint256 prevTotalStableDebt; + uint256 prevTotalVariableDebt; + uint256 currTotalVariableDebt; uint256 avgStableRate; uint256 cumulatedStableInterest; uint256 totalDebtAccrued; @@ -266,108 +234,187 @@ library ReserveLogic { * @dev Mints part of the repaid interest to the reserve treasury as a function of the reserveFactor for the * specific asset. * @param reserve The reserve reserve to be updated - * @param scaledVariableDebt The current scaled total variable debt - * @param previousVariableBorrowIndex The variable borrow index before the last accumulation of the interest - * @param newLiquidityIndex The new liquidity index - * @param newVariableBorrowIndex The variable borrow index after the last accumulation of the interest + * @param reserveCache The caching layer for the reserve data **/ - function _mintToTreasury( + function _accrueToTreasury( DataTypes.ReserveData storage reserve, - uint256 scaledVariableDebt, - uint256 previousVariableBorrowIndex, - uint256 newLiquidityIndex, - uint256 newVariableBorrowIndex, - uint40 timestamp + DataTypes.ReserveCache memory reserveCache ) internal { MintToTreasuryLocalVars memory vars; - vars.reserveFactor = reserve.configuration.getReserveFactor(); + vars.reserveFactor = reserveCache.reserveConfiguration.getReserveFactorMemory(); if (vars.reserveFactor == 0) { return; } - //fetching the principal, total stable debt and the avg stable rate - ( - vars.principalStableDebt, - vars.currentStableDebt, - vars.avgStableRate, - vars.stableSupplyUpdatedTimestamp - ) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData(); + //calculate the total variable debt at moment of the last interaction + vars.prevTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.currVariableBorrowIndex + ); - //calculate the last principal variable debt - vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex); - - //calculate the new total supply after accumulation of the index - vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex); + //calculate the new total variable debt after accumulation of the interest on the index + vars.currTotalVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); //calculate the stable debt until the last timestamp update vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest( - vars.avgStableRate, - vars.stableSupplyUpdatedTimestamp, - timestamp + reserveCache.currAvgStableBorrowRate, + reserveCache.stableDebtLastUpdateTimestamp, + reserveCache.reserveLastUpdateTimestamp ); - vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest); + vars.prevTotalStableDebt = reserveCache.currPrincipalStableDebt.rayMul( + vars.cumulatedStableInterest + ); //debt accrued is the sum of the current debt minus the sum of the debt at the last update vars.totalDebtAccrued = vars - .currentVariableDebt - .add(vars.currentStableDebt) - .sub(vars.previousVariableDebt) - .sub(vars.previousStableDebt); + .currTotalVariableDebt + .add(reserveCache.currTotalStableDebt) + .sub(vars.prevTotalVariableDebt) + .sub(vars.prevTotalStableDebt); vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor); if (vars.amountToMint != 0) { - IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex); + reserve.accruedToTreasury = reserve.accruedToTreasury.add( + vars.amountToMint.rayDiv(reserveCache.nextLiquidityIndex) + ); } } /** * @dev Updates the reserve indexes and the timestamp of the update * @param reserve The reserve reserve to be updated - * @param scaledVariableDebt The scaled variable debt - * @param liquidityIndex The last stored liquidity index - * @param variableBorrowIndex The last stored variable borrow index + * @param reserveCache The cache layer holding the cached protocol data **/ function _updateIndexes( DataTypes.ReserveData storage reserve, - uint256 scaledVariableDebt, - uint256 liquidityIndex, - uint256 variableBorrowIndex, - uint40 timestamp - ) internal returns (uint256, uint256) { - uint256 currentLiquidityRate = reserve.currentLiquidityRate; - - uint256 newLiquidityIndex = liquidityIndex; - uint256 newVariableBorrowIndex = variableBorrowIndex; + DataTypes.ReserveCache memory reserveCache + ) internal { + reserveCache.nextLiquidityIndex = reserveCache.currLiquidityIndex; + reserveCache.nextVariableBorrowIndex = reserveCache.currVariableBorrowIndex; //only cumulating if there is any income being produced - if (currentLiquidityRate > 0) { + if (reserveCache.currLiquidityRate > 0) { uint256 cumulatedLiquidityInterest = - MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp); - newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex); - require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW); - - reserve.liquidityIndex = uint128(newLiquidityIndex); + MathUtils.calculateLinearInterest( + reserveCache.currLiquidityRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul( + reserveCache.currLiquidityIndex + ); + require( + reserveCache.nextLiquidityIndex <= type(uint128).max, + Errors.RL_LIQUIDITY_INDEX_OVERFLOW + ); + reserve.liquidityIndex = uint128(reserveCache.nextLiquidityIndex); //as the liquidity rate might come only from stable rate loans, we need to ensure //that there is actual variable debt before accumulating - if (scaledVariableDebt != 0) { + if (reserveCache.currScaledVariableDebt != 0) { uint256 cumulatedVariableBorrowInterest = - MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); - newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); + MathUtils.calculateCompoundedInterest( + reserveCache.currVariableBorrowRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( + reserveCache.currVariableBorrowIndex + ); require( - newVariableBorrowIndex <= type(uint128).max, + reserveCache.nextVariableBorrowIndex <= type(uint128).max, Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW ); - reserve.variableBorrowIndex = uint128(newVariableBorrowIndex); + reserve.variableBorrowIndex = uint128(reserveCache.nextVariableBorrowIndex); } } //solium-disable-next-line reserve.lastUpdateTimestamp = uint40(block.timestamp); - return (newLiquidityIndex, newVariableBorrowIndex); + } + + /** + * @dev Creates a cache object to avoid repeated storage reads and external contract calls when updating state and interest rates. + * @param reserve The reserve object for which the cache will be filled + * @return The cache object + */ + function cache(DataTypes.ReserveData storage reserve) + internal + view + returns (DataTypes.ReserveCache memory) + { + DataTypes.ReserveCache memory reserveCache; + + reserveCache.reserveConfiguration = reserve.configuration; + reserveCache.currLiquidityIndex = reserve.liquidityIndex; + reserveCache.currVariableBorrowIndex = reserve.variableBorrowIndex; + reserveCache.currLiquidityRate = reserve.currentLiquidityRate; + reserveCache.currVariableBorrowRate = reserve.currentVariableBorrowRate; + + reserveCache.aTokenAddress = reserve.aTokenAddress; + reserveCache.stableDebtTokenAddress = reserve.stableDebtTokenAddress; + reserveCache.variableDebtTokenAddress = reserve.variableDebtTokenAddress; + + reserveCache.reserveLastUpdateTimestamp = reserve.lastUpdateTimestamp; + + reserveCache.currScaledVariableDebt = reserveCache.nextScaledVariableDebt = IVariableDebtToken( + reserveCache + .variableDebtTokenAddress + ) + .scaledTotalSupply(); + + ( + reserveCache.currPrincipalStableDebt, + reserveCache.currTotalStableDebt, + reserveCache.currAvgStableBorrowRate, + reserveCache.stableDebtLastUpdateTimestamp + ) = IStableDebtToken(reserveCache.stableDebtTokenAddress).getSupplyData(); + + // by default the actions are considered as not affecting the debt balances. + // if the action involves mint/burn of debt, the cache needs to be updated through refreshDebt() + reserveCache.nextTotalStableDebt = reserveCache.currTotalStableDebt; + reserveCache.nextAvgStableBorrowRate = reserveCache.currAvgStableBorrowRate; + + return reserveCache; + } + + /** + * @dev Updates the debt data in the cache object. MUST be invoked before updateInterestRates() when a protocol interaction + * causes minting or burning of debt. + * @param cache The cache object + * @param stableDebtMinted The stable debt minted as a consequence of the interaction + * @param stableDebtBurned The stable debt burned as a consequence of the interaction + * @param variableDebtMinted The variable debt minted as a consequence of the interaction + * @param variableDebtBurned The variable debt burned as a consequence of the interaction + */ + function refreshDebt( + DataTypes.ReserveCache memory cache, + uint256 stableDebtMinted, + uint256 stableDebtBurned, + uint256 variableDebtMinted, + uint256 variableDebtBurned + ) internal view { + if (stableDebtMinted != 0 || stableDebtBurned != 0) { + if (cache.currTotalStableDebt.add(stableDebtMinted) > stableDebtBurned) { + cache.nextTotalStableDebt = cache.currTotalStableDebt.add(stableDebtMinted).sub( + stableDebtBurned + ); + cache.nextAvgStableBorrowRate = IStableDebtToken(cache.stableDebtTokenAddress) + .getAverageStableRate(); + } else { + cache.nextTotalStableDebt = cache.nextAvgStableBorrowRate = 0; + } + } + + if (variableDebtMinted != 0 || variableDebtBurned != 0) { + uint256 scaledVariableDebtMinted = variableDebtMinted.rayDiv(cache.nextVariableBorrowIndex); + uint256 scaledVariableDebtBurned = variableDebtBurned.rayDiv(cache.nextVariableBorrowIndex); + cache.nextScaledVariableDebt = cache.currScaledVariableDebt.add(scaledVariableDebtMinted).sub( + scaledVariableDebtBurned + ); + } } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 14dca17e..18a9c6be 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -16,7 +16,10 @@ import {Helpers} from '../helpers/Helpers.sol'; import {IReserveInterestRateStrategy} from '../../../interfaces/IReserveInterestRateStrategy.sol'; import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; import {IStableDebtToken} from '../../../interfaces/IStableDebtToken.sol'; +import {IScaledBalanceToken} from '../../../interfaces/IScaledBalanceToken.sol'; +import {IAToken} from '../../../interfaces/IAToken.sol'; import {DataTypes} from '../types/DataTypes.sol'; +import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; /** * @title ReserveLogic library @@ -37,14 +40,17 @@ library ValidationLogic { /** * @dev Validates a deposit action - * @param reserve The reserve object on which the user is depositing + * @param reserveCache The cached data of the reserve * @param amount The amount to be deposited */ - function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) internal view { - DataTypes.ReserveConfigurationMap memory reserveConfiguration = reserve.configuration; - (bool isActive, bool isFrozen, , , bool isPaused) = reserveConfiguration.getFlagsMemory(); - (, , , uint256 reserveDecimals, ) = reserveConfiguration.getParamsMemory(); - uint256 supplyCap = reserveConfiguration.getSupplyCapMemory(); + function validateDeposit(DataTypes.ReserveCache memory reserveCache, uint256 amount) + internal + view + { + (bool isActive, bool isFrozen, , , bool isPaused) = + reserveCache.reserveConfiguration.getFlagsMemory(); + (, , , uint256 reserveDecimals, ) = reserveCache.reserveConfiguration.getParamsMemory(); + uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCapMemory(); require(amount != 0, Errors.VL_INVALID_AMOUNT); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); @@ -52,7 +58,11 @@ library ValidationLogic { require(!isFrozen, Errors.VL_RESERVE_FROZEN); require( supplyCap == 0 || - IERC20(reserve.aTokenAddress).totalSupply().add(amount).div(10**reserveDecimals) < + IAToken(reserveCache.aTokenAddress) + .scaledTotalSupply() + .rayMul(reserveCache.nextLiquidityIndex) + .add(amount) + .div(10**reserveDecimals) < supplyCap, Errors.VL_SUPPLY_CAP_EXCEEDED ); @@ -60,19 +70,19 @@ library ValidationLogic { /** * @dev Validates a withdraw action - * @param reserve The reserve object + * @param reserveCache The cached data of the reserve * @param amount The amount to be withdrawn * @param userBalance The balance of the user */ function validateWithdraw( - DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, uint256 amount, uint256 userBalance - ) external view { + ) internal pure { require(amount != 0, Errors.VL_INVALID_AMOUNT); require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE); - (bool isActive, , , , bool isPaused) = reserve.configuration.getFlags(); + (bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); } @@ -85,10 +95,11 @@ library ValidationLogic { uint256 userBorrowBalanceETH; uint256 availableLiquidity; uint256 healthFactor; - uint256 totalSupplyStableDebt; + uint256 totalDebt; uint256 totalSupplyVariableDebt; uint256 reserveDecimals; uint256 borrowCap; + uint256 amountInETH; bool isActive; bool isFrozen; bool isPaused; @@ -98,11 +109,10 @@ library ValidationLogic { /** * @dev Validates a borrow action + * @param reserveCache the cached data of the reserve * @param asset The address of the asset to borrow - * @param reserve The reserve state from which the user is borrowing * @param userAddress The address of the user * @param amount The amount to be borrowed - * @param amountInETH The amount to be borrowed, in ETH * @param interestRateMode The interest rate mode at which the user is borrowing * @param maxStableLoanPercent The max amount of the liquidity that can be borrowed at stable rate, in percentage * @param reservesData The state of all the reserves @@ -112,11 +122,10 @@ library ValidationLogic { */ function validateBorrow( + DataTypes.ReserveCache memory reserveCache, address asset, - DataTypes.ReserveData storage reserve, address userAddress, uint256 amount, - uint256 amountInETH, uint256 interestRateMode, uint256 maxStableLoanPercent, mapping(address => DataTypes.ReserveData) storage reservesData, @@ -127,8 +136,7 @@ library ValidationLogic { ) external view { ValidateBorrowLocalVars memory vars; - DataTypes.ReserveConfigurationMap memory reserveConfiguration = reserve.configuration; - (, , , vars.reserveDecimals, ) = reserveConfiguration.getParamsMemory(); + (, , , vars.reserveDecimals, ) = reserveCache.reserveConfiguration.getParamsMemory(); ( vars.isActive, @@ -136,7 +144,7 @@ library ValidationLogic { vars.borrowingEnabled, vars.stableRateBorrowingEnabled, vars.isPaused - ) = reserveConfiguration.getFlagsMemory(); + ) = reserveCache.reserveConfiguration.getFlagsMemory(); require(vars.isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!vars.isPaused, Errors.VL_RESERVE_PAUSED); @@ -152,18 +160,23 @@ library ValidationLogic { Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED ); - vars.totalSupplyStableDebt = IERC20(reserve.stableDebtTokenAddress).totalSupply(); - vars.borrowCap = reserveConfiguration.getBorrowCapMemory(); - vars.totalSupplyVariableDebt = IERC20(reserve.variableDebtTokenAddress).totalSupply(); + vars.borrowCap = reserveCache.reserveConfiguration.getBorrowCapMemory(); - require( - vars.borrowCap == 0 || - vars.totalSupplyStableDebt.add(vars.totalSupplyVariableDebt).add(amount).div( - 10**vars.reserveDecimals - ) < - vars.borrowCap, - Errors.VL_BORROW_CAP_EXCEEDED - ); + if (vars.borrowCap != 0) { + { + vars.totalSupplyVariableDebt = reserveCache.currScaledVariableDebt.rayMul( + reserveCache.nextVariableBorrowIndex + ); + + vars.totalDebt = reserveCache.currTotalStableDebt.add(vars.totalSupplyVariableDebt).add( + amount + ); + require( + vars.totalDebt.div(10**vars.reserveDecimals) < vars.borrowCap, + Errors.VL_BORROW_CAP_EXCEEDED + ); + } + } ( vars.userCollateralBalanceETH, @@ -187,8 +200,11 @@ library ValidationLogic { Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); + vars.amountInETH = IPriceOracleGetter(oracle).getAssetPrice(asset); + vars.amountInETH = vars.amountInETH.mul(amount).div(10**vars.reserveDecimals); + //add the current already borrowed amount to the amount requested to calculate the total collateral needed. - vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv( + vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(vars.amountInETH).percentDiv( vars.currentLtv ); //LTV is calculated in percentage @@ -211,13 +227,13 @@ library ValidationLogic { require(vars.stableRateBorrowingEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED); require( - !userConfig.isUsingAsCollateral(reserve.id) || - reserve.configuration.getLtv() == 0 || - amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress), + !userConfig.isUsingAsCollateral(reservesData[asset].id) || + reserveCache.reserveConfiguration.getLtvMemory() == 0 || + amount > IERC20(reserveCache.aTokenAddress).balanceOf(userAddress), Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY ); - vars.availableLiquidity = IERC20(asset).balanceOf(reserve.aTokenAddress); + vars.availableLiquidity = IERC20(asset).balanceOf(reserveCache.aTokenAddress); //calculate the max available loan size in stable rate mode as a percentage of the //available liquidity @@ -229,21 +245,22 @@ library ValidationLogic { /** * @dev Validates a repay action - * @param reserve The reserve state from which the user is repaying + * @param reserveCache The cached data of the reserve * @param amountSent The amount sent for the repayment. Can be an actual value or uint(-1) + * @param rateMode the interest rate mode of the debt being repaid * @param onBehalfOf The address of the user msg.sender is repaying for * @param stableDebt The borrow balance of the user * @param variableDebt The borrow balance of the user */ function validateRepay( - DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, uint256 amountSent, DataTypes.InterestRateMode rateMode, address onBehalfOf, uint256 stableDebt, uint256 variableDebt - ) external view { - (bool isActive, , , , bool isPaused) = reserve.configuration.getFlags(); + ) internal view { + (bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); @@ -266,20 +283,22 @@ library ValidationLogic { /** * @dev Validates a swap of borrow rate mode. * @param reserve The reserve state on which the user is swapping the rate + * @param reserveCache The cached data of the reserve * @param userConfig The user reserves configuration * @param stableDebt The stable debt of the user * @param variableDebt The variable debt of the user - * @param currentRateMode The rate mode of the borrow + * @param currentRateMode The rate mode of the debt being swapped */ function validateSwapRateMode( DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, DataTypes.UserConfigurationMap storage userConfig, uint256 stableDebt, uint256 variableDebt, DataTypes.InterestRateMode currentRateMode ) external view { (bool isActive, bool isFrozen, , bool stableRateEnabled, bool isPaused) = - reserve.configuration.getFlags(); + reserveCache.reserveConfiguration.getFlagsMemory(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); @@ -300,8 +319,8 @@ library ValidationLogic { require( !userConfig.isUsingAsCollateral(reserve.id) || - reserve.configuration.getLtv() == 0 || - stableDebt.add(variableDebt) > IERC20(reserve.aTokenAddress).balanceOf(msg.sender), + reserveCache.reserveConfiguration.getLtvMemory() == 0 || + stableDebt.add(variableDebt) > IERC20(reserveCache.aTokenAddress).balanceOf(msg.sender), Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY ); } else { @@ -312,6 +331,7 @@ library ValidationLogic { /** * @dev Validates a stable borrow rate rebalance action * @param reserve The reserve state on which the user is getting rebalanced + * @param reserveCache The cached state of the reserve * @param reserveAddress The address of the reserve * @param stableDebtToken The stable debt token instance * @param variableDebtToken The variable debt token instance @@ -319,12 +339,13 @@ library ValidationLogic { */ function validateRebalanceStableBorrowRate( DataTypes.ReserveData storage reserve, + DataTypes.ReserveCache memory reserveCache, address reserveAddress, IERC20 stableDebtToken, IERC20 variableDebtToken, address aTokenAddress ) external view { - (bool isActive, , , , bool isPaused) = reserve.configuration.getFlags(); + (bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); @@ -338,7 +359,7 @@ library ValidationLogic { //if the liquidity rate is below REBALANCE_UP_THRESHOLD of the max variable APR at 95% usage, //then we allow rebalancing of the stable rate positions. - uint256 currentLiquidityRate = reserve.currentLiquidityRate; + uint256 currentLiquidityRate = reserveCache.currLiquidityRate; uint256 maxVariableBorrowRate = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).getMaxVariableBorrowRate(); @@ -352,12 +373,16 @@ library ValidationLogic { /** * @dev Validates the action of setting an asset as collateral - * @param reserve The state of the reserve that the user is enabling or disabling as collateral + * @param reserveCache The cached data of the reserve */ - function validateSetUseReserveAsCollateral(DataTypes.ReserveData storage reserve) external view { - uint256 underlyingBalance = IERC20(reserve.aTokenAddress).balanceOf(msg.sender); - bool isPaused = reserve.configuration.getPaused(); + function validateSetUseReserveAsCollateral(DataTypes.ReserveCache memory reserveCache) + external + view + { + uint256 underlyingBalance = IERC20(reserveCache.aTokenAddress).balanceOf(msg.sender); + (bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory(); + require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); require(underlyingBalance > 0, Errors.VL_UNDERLYING_BALANCE_NOT_GREATER_THAN_0); @@ -372,62 +397,95 @@ library ValidationLogic { address[] memory assets, uint256[] memory amounts, mapping(address => DataTypes.ReserveData) storage reservesData - ) external view { + ) internal view { for (uint256 i = 0; i < assets.length; i++) { require(!reservesData[assets[i]].configuration.getPaused(), Errors.VL_RESERVE_PAUSED); } require(assets.length == amounts.length, Errors.VL_INCONSISTENT_FLASHLOAN_PARAMS); } + struct ValidateLiquidationCallLocalVars { + uint256 healthFactor; + bool collateralReserveActive; + bool collateralReservePaused; + bool principalReserveActive; + bool principalReservePaused; + bool isCollateralEnabled; + } + /** * @dev Validates the liquidation action * @param collateralReserve The reserve data of the collateral - * @param principalReserve The reserve data of the principal + * @param principalReserveCache The cached reserve data of the principal * @param userConfig The user configuration - * @param userHealthFactor The user's health factor - * @param userStableDebt Total stable debt balance of the user - * @param userVariableDebt Total variable debt balance of the user + * @param totalDebt Total debt balance of the user + * @param user The address of the user being liquidated + * @param reservesData The mapping of the reserves data + * @param userConfig The user configuration mapping + * @param reserves The list of the reserves + * @param reservesCount The number of reserves in the list + * @param oracle The address of the price oracle **/ function validateLiquidationCall( DataTypes.ReserveData storage collateralReserve, - DataTypes.ReserveData storage principalReserve, + DataTypes.ReserveCache memory principalReserveCache, + uint256 totalDebt, + address user, + mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, - uint256 userHealthFactor, - uint256 userStableDebt, - uint256 userVariableDebt + mapping(uint256 => address) storage reserves, + uint256 reservesCount, + address oracle ) internal view returns (uint256, string memory) { - if ( - !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive() - ) { + ValidateLiquidationCallLocalVars memory vars; + + (vars.collateralReserveActive, , , , vars.collateralReservePaused) = collateralReserve + .configuration + .getFlagsMemory(); + + (vars.principalReserveActive, , , , vars.principalReservePaused) = principalReserveCache + .reserveConfiguration + .getFlagsMemory(); + + if (!vars.collateralReserveActive || !vars.principalReserveActive) { return ( uint256(Errors.CollateralManagerErrors.NO_ACTIVE_RESERVE), Errors.VL_NO_ACTIVE_RESERVE ); } - if (collateralReserve.configuration.getPaused() || principalReserve.configuration.getPaused()) { + if (vars.collateralReservePaused || vars.principalReservePaused) { return (uint256(Errors.CollateralManagerErrors.PAUSED_RESERVE), Errors.VL_RESERVE_PAUSED); } - if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + user, + reservesData, + userConfig, + reserves, + reservesCount, + oracle + ); + + if (vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { return ( uint256(Errors.CollateralManagerErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), Errors.LPCM_HEALTH_FACTOR_NOT_BELOW_THRESHOLD ); } - bool isCollateralEnabled = + vars.isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && - userConfig.isUsingAsCollateral(collateralReserve.id); + userConfig.isUsingAsCollateral(collateralReserve.id); //if collateral isn't enabled as collateral by user, it cannot be liquidated - if (!isCollateralEnabled) { + if (!vars.isCollateralEnabled) { return ( uint256(Errors.CollateralManagerErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), Errors.LPCM_COLLATERAL_CANNOT_BE_LIQUIDATED ); } - if (userStableDebt == 0 && userVariableDebt == 0) { + if (totalDebt == 0) { return ( uint256(Errors.CollateralManagerErrors.CURRRENCY_NOT_BORROWED), Errors.LPCM_SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER @@ -454,7 +512,7 @@ library ValidationLogic { mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle - ) internal view { + ) external view { DataTypes.ReserveData memory reserve = reservesData[collateral]; (, , uint256 ltv, uint256 liquidationThreshold, uint256 healthFactor) = GenericLogic.calculateUserAccountData( @@ -497,4 +555,20 @@ library ValidationLogic { function validateTransfer(DataTypes.ReserveData storage reserve) internal view { require(!reserve.configuration.getPaused(), Errors.VL_RESERVE_PAUSED); } + + /** + * @dev Validates a drop reserve action + * @param reserve The reserve object + **/ + function validateDropReserve(DataTypes.ReserveData storage reserve) external view { + require( + IERC20(reserve.stableDebtTokenAddress).totalSupply() == 0, + Errors.RL_STABLE_DEBT_NOT_ZERO + ); + require( + IERC20(reserve.variableDebtTokenAddress).totalSupply() == 0, + Errors.RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + require(IERC20(reserve.aTokenAddress).totalSupply() == 0, Errors.RL_ATOKEN_SUPPLY_NOT_ZERO); + } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index b92b21d0..3c61c7af 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -25,6 +25,8 @@ library DataTypes { address interestRateStrategyAddress; //the id of the reserve. Represents the position in the list of the active reserves uint8 id; + //the current treasury balance, scaled + uint256 accruedToTreasury; } struct ReserveConfigurationMap { @@ -50,4 +52,26 @@ library DataTypes { } enum InterestRateMode {NONE, STABLE, VARIABLE} + + struct ReserveCache { + uint256 currScaledVariableDebt; + uint256 nextScaledVariableDebt; + uint256 currPrincipalStableDebt; + uint256 currAvgStableBorrowRate; + uint256 currTotalStableDebt; + uint256 nextAvgStableBorrowRate; + uint256 nextTotalStableDebt; + uint256 currLiquidityIndex; + uint256 nextLiquidityIndex; + uint256 currVariableBorrowIndex; + uint256 nextVariableBorrowIndex; + uint256 currLiquidityRate; + uint256 currVariableBorrowRate; + DataTypes.ReserveConfigurationMap reserveConfiguration; + address aTokenAddress; + address stableDebtTokenAddress; + address variableDebtTokenAddress; + uint40 reserveLastUpdateTimestamp; + uint40 stableDebtLastUpdateTimestamp; + } } diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 2d764885..05f79206 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -188,7 +188,8 @@ export const deployAaveLibraries = async ( return { ['__$de8c0cf1a7d7c36c802af9a64fb9d86036$__']: validationLogic.address, ['__$22cd43a9dda9ce44e9b92ba393b88fb9ac$__']: reserveLogic.address, - }; + ["__$52a8a86ab43135662ff256bbc95497e8e3$__"]: genericLogic.address, + } }; export const deployLendingPool = async (verify?: boolean) => { diff --git a/helpers/types.ts b/helpers/types.ts index 8cca7b81..4467001a 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -184,8 +184,11 @@ export enum ProtocolErrors { LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN = '85', VL_RESERVE_PAUSED = '86', LPC_CALLER_NOT_RISK_OR_POOL_ADMIN = '87', - RC_INVALID_EXPOSURE_CAP = '88', - VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '89', + RL_ATOKEN_SUPPLY_NOT_ZERO = '88', + RL_STABLE_DEBT_NOT_ZERO = '89', + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', + RC_INVALID_EXPOSURE_CAP = '91', + VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '92', // old diff --git a/package-lock.json b/package-lock.json index ad482fe2..e5198127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11142,14 +11142,6 @@ "requires": { "min-document": "^2.19.0", "process": "^0.11.10" - }, - "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=", - "dev": true - } } }, "got": { @@ -12794,6 +12786,12 @@ "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -14795,7 +14793,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { diff --git a/test-suites/test-aave/__setup.spec.ts b/test-suites/test-aave/__setup.spec.ts index dd8ed168..2ef2f3a0 100644 --- a/test-suites/test-aave/__setup.spec.ts +++ b/test-suites/test-aave/__setup.spec.ts @@ -265,6 +265,7 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { ); await configureReservesByHelper(reservesParams, allReservesAddresses, testHelpers, admin); + lendingPoolConfiguratorProxy.dropReserve(mockTokens.KNC.address); const collateralManager = await deployLendingPoolCollateralManager(); await waitForTx( diff --git a/test-suites/test-aave/authorized-flashloan.spec.ts b/test-suites/test-aave/authorized-flashloan.spec.ts new file mode 100644 index 00000000..06977e36 --- /dev/null +++ b/test-suites/test-aave/authorized-flashloan.spec.ts @@ -0,0 +1,501 @@ +import BigNumber from 'bignumber.js'; + +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { APPROVAL_AMOUNT_LENDING_POOL, 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; + + before(async () => { + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + }); + it('Authorize a flash borrower', async () => { + const { deployer, pool, weth, configurator } = testEnv; + await configurator.authorizeFlashBorrower(deployer.address); + }); + + it('Deposits WETH into the reserve', async () => { + const { pool, weth } = 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'); + }); + + it('Takes WETH flash loan with mode = 0, returns the funds correctly', async () => { + const { pool, helpersContract, weth } = testEnv; + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + [ethers.utils.parseEther('0.8')], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + ethers.utils.parseUnits('10000'); + + const reserveData = await helpersContract.getReserveData(weth.address); + + const currentLiquidityRate = reserveData.liquidityRate; + const currentLiquidityIndex = reserveData.liquidityIndex; + + const totalLiquidity = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); + + expect(totalLiquidity.toString()).to.be.equal('1000000000000000000'); + expect(currentLiquidityRate.toString()).to.be.equal('0'); + expect(currentLiquidityIndex.toString()).to.be.equal('1000000000000000000000000000'); + }); + + it('Takes an ETH flash loan with mode = 0 as big as the available liquidity', async () => { + const { pool, helpersContract, weth } = testEnv; + + const reserveDataBefore = await helpersContract.getReserveData(weth.address); + const txResult = await pool.flashLoan( + _mockFlashLoanReceiver.address, + [weth.address], + ['1000000000000000000'], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + const reserveData = await helpersContract.getReserveData(weth.address); + + const currentLiquidityRate = reserveData.liquidityRate; + const currentLiquidityIndex = reserveData.liquidityIndex; + + const totalLiquidity = new BigNumber(reserveData.availableLiquidity.toString()) + .plus(reserveData.totalStableDebt.toString()) + .plus(reserveData.totalVariableDebt.toString()); + + expect(totalLiquidity.toString()).to.be.equal('1000000000000000000'); + expect(currentLiquidityRate.toString()).to.be.equal('0'); + expect(currentLiquidityIndex.toString()).to.be.equal('1000000000000000000000000000'); + }); + + 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 flash loan, 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); + + 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 + ); + + const wethDebtToken = await getVariableDebtToken(variableDebtTokenAddress); + + const callerDebt = await wethDebtToken.balanceOf(caller.address); + + expect(callerDebt.toString()).to.be.equal('800000000000000000', 'Invalid user debt'); + }); + + 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], + ['1000000000000000001'], //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, pool, helpersContract, deployer: depositor } = testEnv; + + await _mockFlashLoanReceiver.setFailExecutionTransfer(false); + + const reserveDataBefore = await helpersContract.getReserveData(usdc.address); + + const flashloanAmount = await convertToCurrencyDecimals(usdc.address, '500'); + + await pool.flashLoan( + _mockFlashLoanReceiver.address, + [usdc.address], + [flashloanAmount], + [0], + _mockFlashLoanReceiver.address, + '0x10', + '0' + ); + + const reserveDataAfter = helpersContract.getReserveData(usdc.address); + + const reserveData = await helpersContract.getReserveData(usdc.address); + const userData = await helpersContract.getUserReserveData(usdc.address, depositor.address); + + const totalLiquidity = reserveData.availableLiquidity + .add(reserveData.totalStableDebt) + .add(reserveData.totalVariableDebt) + .toString(); + const currentLiquidityRate = reserveData.liquidityRate.toString(); + const currentLiquidityIndex = reserveData.liquidityIndex.toString(); + const currentUserBalance = userData.currentATokenBalance.toString(); + + const expectedLiquidity = await convertToCurrencyDecimals(usdc.address, '1000'); + + expect(totalLiquidity).to.be.equal(expectedLiquidity, 'Invalid total liquidity'); + expect(currentLiquidityRate).to.be.equal('0', 'Invalid liquidity rate'); + expect(currentLiquidityIndex).to.be.equal( + new BigNumber('1.00000').multipliedBy(oneRay).toFixed(), + 'Invalid liquidity index' + ); + expect(currentUserBalance.toString()).to.be.equal(expectedLiquidity, 'Invalid user balance'); + }); + + 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' + ); + }); +}); diff --git a/test-suites/test-aave/configurator.spec.ts b/test-suites/test-aave/configurator.spec.ts index a3fb8131..49b5f2df 100644 --- a/test-suites/test-aave/configurator.spec.ts +++ b/test-suites/test-aave/configurator.spec.ts @@ -73,7 +73,148 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(await configurator.signer.getAddress()).to.be.equal( await addressesProvider.getPoolAdmin() ); + await configurator.pauseReserve(weth.address); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(true); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); + expect(borrowCap).to.be.equal(strategyWETH.borrowCap); + expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + }); + + it('Unpauses the ETH reserve by pool admin ', async () => { + const { configurator, helpersContract, weth } = testEnv; + await configurator.unpauseReserve(weth.address); + + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(false); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); + expect(borrowCap).to.be.equal(strategyWETH.borrowCap); + expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + }); + it('Pauses the ETH reserve by emergency admin', async () => { + const { configurator, weth, helpersContract, addressesProvider, users, emergencyAdmin } = + testEnv; + await configurator.connect(emergencyAdmin.signer).pauseReserve(weth.address); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(true); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); + expect(borrowCap).to.be.equal(strategyWETH.borrowCap); + expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + }); + + it('Unpauses the ETH reserve by emergency admin ', async () => { + const { configurator, helpersContract, weth, users, emergencyAdmin } = testEnv; + await configurator.connect(emergencyAdmin.signer).unpauseReserve(weth.address); + + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(false); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); + expect(borrowCap).to.be.equal(strategyWETH.borrowCap); + expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + }); + + it('Check the only admin or emergency admin can pauseReserve ', async () => { + const { configurator, users, weth, riskAdmin } = testEnv; + await expect( + configurator.connect(riskAdmin.signer).pauseReserve(weth.address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN); + }); + + it('Check the only admin or emergency admin can unpauseReserve ', async () => { + const { configurator, users, weth, riskAdmin } = testEnv; + await expect( + configurator.connect(riskAdmin.signer).unpauseReserve(weth.address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN); + }); + it('Pauses the ETH reserve by the pool admin', async () => { + const { configurator, weth, helpersContract, addressesProvider, users, emergencyAdmin } = + testEnv; await configurator.pauseReserve(weth.address); const { decimals, @@ -297,7 +438,6 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { }); it('Freezes the ETH reserve by Risk Admin', async () => { const { configurator, weth, helpersContract, riskAdmin } = testEnv; - await configurator.connect(riskAdmin.signer).freezeReserve(weth.address); const { decimals, @@ -944,6 +1084,36 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(exposureCap).to.be.equal(strategyWETH.exposureCap); expect(reserveFactor).to.be.equal(1000); }); + it('Changes the reserve factor of WETH risk admin', async () => { + const { configurator, helpersContract, weth, riskAdmin } = testEnv; + await configurator.connect(riskAdmin.signer).setReserveFactor(weth.address, '1000'); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(false); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(borrowCap).to.be.equal(strategyWETH.borrowCap); + expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(reserveFactor).to.be.equal(1000); + }); it('Check that borrowCap cannot be set to value that exceeds the MAX_BORROW_CAP', async () => { const { configurator, users, weth } = testEnv; @@ -1159,6 +1329,36 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(supplyCap).to.be.equal('3000000'); expect(exposureCap).to.be.equal('3000000'); }); + it('Changes the supply Cap of WETH via risk admin', async () => { + const { configurator, helpersContract, weth, riskAdmin } = testEnv; + await configurator.connect(riskAdmin.signer).setSupplyCap(weth.address, '3000000'); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const isPaused = await helpersContract.getPaused(weth.address); + + expect(borrowingEnabled).to.be.equal(true); + expect(isActive).to.be.equal(true); + expect(isPaused).to.be.equal(false); + expect(isFrozen).to.be.equal(false); + expect(decimals).to.be.equal(strategyWETH.reserveDecimals); + expect(ltv).to.be.equal(strategyWETH.baseLTVAsCollateral); + expect(liquidationThreshold).to.be.equal(strategyWETH.liquidationThreshold); + expect(liquidationBonus).to.be.equal(strategyWETH.liquidationBonus); + expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); + expect(reserveFactor).to.be.equal(1000); + expect(borrowCap).to.be.equal('3000000'); + expect(supplyCap).to.be.equal('3000000'); + }); it('Reverts when trying to disable the DAI reserve with liquidity on it', async () => { const { dai, pool, configurator } = testEnv; @@ -1196,4 +1396,62 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(isNewRegistered).to.be.false; expect(isRiskAdminRegistered).to.be.false; }); + it('Checks only pool admin can register/unregister a risk Admins', async () => { + const { dai, pool, configurator, users, riskAdmin, emergencyAdmin } = testEnv; + + await expect( + configurator.connect(riskAdmin.signer).registerRiskAdmin(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(riskAdmin.signer).unregisterRiskAdmin(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(emergencyAdmin.signer).registerRiskAdmin(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + await expect( + configurator.connect(emergencyAdmin.signer).unregisterRiskAdmin(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + }); + it('Authorized a new flash borrower', async () => { + const { dai, pool, configurator, users, riskAdmin } = testEnv; + await configurator.authorizeFlashBorrower(users[4].address); + + const isFlashBorrowerAuthorized = await pool.isFlashBorrowerAuthorized(users[4].address); + expect(isFlashBorrowerAuthorized).to.be.true; + }); + it('Unauthorized flash borrower', async () => { + const { dai, pool, configurator, users } = testEnv; + await configurator.unauthorizeFlashBorrower(users[4].address); + + const isFlashBorrowerAuthorized = await pool.isFlashBorrowerAuthorized(users[4].address); + expect(isFlashBorrowerAuthorized).to.be.false; + }); + it('Checks only pool admin can authorize/unauthorize a flash borrower', async () => { + const { dai, pool, configurator, users, riskAdmin, emergencyAdmin } = testEnv; + + await expect( + configurator.connect(riskAdmin.signer).authorizeFlashBorrower(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(riskAdmin.signer).authorizeFlashBorrower(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + + await expect( + configurator.connect(emergencyAdmin.signer).unauthorizeFlashBorrower(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + await expect( + configurator.connect(emergencyAdmin.signer).unauthorizeFlashBorrower(users[3].address), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); + }); }); diff --git a/test-suites/test-aave/drop-reserve.spec.ts b/test-suites/test-aave/drop-reserve.spec.ts new file mode 100644 index 00000000..d2257f79 --- /dev/null +++ b/test-suites/test-aave/drop-reserve.spec.ts @@ -0,0 +1,104 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { ProtocolErrors, RateMode } from '../../helpers/types'; +import { APPROVAL_AMOUNT_LENDING_POOL, MAX_UINT_AMOUNT, oneEther } from '../../helpers/constants'; +import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; +import { BigNumber } from 'bignumber.js'; +import { MockFlashLoanReceiver } from '../../types/MockFlashLoanReceiver'; +import { getMockFlashLoanReceiver } from '../../helpers/contracts-getters'; +import { domainToUnicode } from 'url'; + +const { expect } = require('chai'); + +makeSuite('Drop Reserve', (testEnv: TestEnv) => { + let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; + + const { RL_ATOKEN_SUPPLY_NOT_ZERO, RL_STABLE_DEBT_NOT_ZERO, RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO } = + ProtocolErrors; + + before(async () => { + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + }); + + it('User 1 deposits Dai, User 2 borrow Dai stable and variable, should fail to drop Dai reserve', async () => { + const { + deployer, + users: [user1], + pool, + dai, + aDai, + weth, + configurator, + } = testEnv; + + const depositedAmount = parseEther('1000'); + const borrowedAmount = parseEther('100'); + // setting reserve factor to 0 to ease tests, no aToken accrued in reserve + await configurator.setReserveFactor(dai.address, 0); + + await dai.mint(depositedAmount); + await dai.approve(pool.address, depositedAmount); + await dai.connect(user1.signer).mint(depositedAmount); + await dai.connect(user1.signer).approve(pool.address, depositedAmount); + + await weth.connect(user1.signer).mint(depositedAmount); + await weth.connect(user1.signer).approve(pool.address, depositedAmount); + + await pool.deposit(dai.address, depositedAmount, deployer.address, 0); + + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_ATOKEN_SUPPLY_NOT_ZERO + ); + + await pool.connect(user1.signer).deposit(weth.address, depositedAmount, user1.address, 0); + + await pool.connect(user1.signer).borrow(dai.address, borrowedAmount, 2, 0, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + await pool.connect(user1.signer).borrow(dai.address, borrowedAmount, 1, 0, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(RL_STABLE_DEBT_NOT_ZERO); + }); + it('User 2 repays debts, drop Dai reserve should fail', async () => { + const { + deployer, + users: [user1], + pool, + dai, + weth, + configurator, + } = testEnv; + await pool.connect(user1.signer).repay(dai.address, MAX_UINT_AMOUNT, 1, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + + await pool.connect(user1.signer).repay(dai.address, MAX_UINT_AMOUNT, 2, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_ATOKEN_SUPPLY_NOT_ZERO + ); + }); + it('User 1 withdraw Dai, drop Dai reserve should succeed', async () => { + const { + deployer, + users: [user1], + pool, + dai, + aDai, + weth, + configurator, + helpersContract, + } = testEnv; + + await pool.withdraw(dai.address, MAX_UINT_AMOUNT, deployer.address); + await configurator.dropReserve(dai.address); + + const tokens = await pool.getReservesList(); + + expect(tokens.includes(dai.address)).to.be.false; + + const { isActive } = await helpersContract.getReserveConfigurationData(dai.address); + + expect(isActive).to.be.false; + }); +}); 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]; diff --git a/test-suites/test-aave/helpers/utils/calculations.ts b/test-suites/test-aave/helpers/utils/calculations.ts index de3d31f9..db633d51 100644 --- a/test-suites/test-aave/helpers/utils/calculations.ts +++ b/test-suites/test-aave/helpers/utils/calculations.ts @@ -1196,7 +1196,7 @@ const calcLinearInterest = ( return cumulatedInterest; }; -const calcCompoundedInterest = ( +export const calcCompoundedInterest = ( rate: BigNumber, currentTimestamp: BigNumber, lastUpdateTimestamp: BigNumber diff --git a/test-suites/test-aave/mint-to-treasury.spec.ts b/test-suites/test-aave/mint-to-treasury.spec.ts new file mode 100644 index 00000000..1ed83274 --- /dev/null +++ b/test-suites/test-aave/mint-to-treasury.spec.ts @@ -0,0 +1,90 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { RateMode } from '../../helpers/types'; +import { APPROVAL_AMOUNT_LENDING_POOL, ONE_YEAR } from '../../helpers/constants'; +import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers'; +import { BigNumber } from 'bignumber.js'; +import { advanceTimeAndBlock, waitForTx } from '../../helpers/misc-utils'; +import './helpers/utils/math'; + +const { expect } = require('chai'); + +makeSuite('Mint to treasury', (testEnv: TestEnv) => { + it('User 0 deposits 1000 DAI. Borrower borrows 100 DAI. Clock moved forward one year. Calculates and verifies the amount accrued to the treasury', async () => { + const { users, pool, dai, helpersContract } = testEnv; + + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + const amountDAItoBorrow = await convertToCurrencyDecimals(dai.address, '100'); + + await waitForTx(await dai.connect(users[0].signer).mint(amountDAItoDeposit)); + + // user 0 deposits 1000 DAI + await waitForTx( + await dai.connect(users[0].signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL) + ); + await waitForTx( + await pool + .connect(users[0].signer) + .deposit(dai.address, amountDAItoDeposit, users[0].address, '0') + ); + + await waitForTx( + await pool + .connect(users[0].signer) + .borrow(dai.address, amountDAItoBorrow, RateMode.Variable, '0', users[0].address) + ); + + const { reserveFactor } = await helpersContract.getReserveConfigurationData(dai.address); + + await advanceTimeAndBlock(parseInt(ONE_YEAR)); + + await waitForTx(await dai.connect(users[0].signer).mint(amountDAItoDeposit)); + + await waitForTx( + await pool + .connect(users[0].signer) + .deposit(dai.address, amountDAItoDeposit, users[0].address, '0') + ); + + const { liquidityIndex, variableBorrowIndex } = await pool.getReserveData(dai.address); + + const amountBorrowedBN = new BigNumber(amountDAItoBorrow.toString()); + const liquidityIndexBN = new BigNumber(liquidityIndex.toString()); + const variableBorrowIndexBN = new BigNumber(variableBorrowIndex.toString()); + + const expectedAccruedToTreasury = amountBorrowedBN + .rayMul(variableBorrowIndexBN) + .minus(amountBorrowedBN) + .times(reserveFactor.toString()) + .div(10000) + .rayDiv(liquidityIndexBN) + .toFixed(0); + + const { accruedToTreasury } = await pool.getReserveData(dai.address); + + console.log("Accrued to treasury ", accruedToTreasury.toString()); + + expect(accruedToTreasury.toString()).to.be.bignumber.almostEqual( + expectedAccruedToTreasury, + 'Invalid amount accrued to the treasury' + ); + }); + + it('Mints the accrued to the treasury', async () => { + const { users, pool, dai, aDai, helpersContract } = testEnv; + + const treasuryAddress = await aDai.RESERVE_TREASURY_ADDRESS(); + const { accruedToTreasury } = await pool.getReserveData(dai.address); + + await waitForTx(await pool.connect(users[0].signer).mintToTreasury([dai.address])); + const normalizedIncome = await pool.getReserveNormalizedIncome(dai.address); + + const treasuryBalance = await aDai.balanceOf(treasuryAddress); + + const expectedTreasuryBalance = new BigNumber(accruedToTreasury.toString()).rayMul( + new BigNumber(normalizedIncome.toString()) + ); + + expect(treasuryBalance.toString()).to.be.bignumber.almostEqual(expectedTreasuryBalance, "Invalid treasury balance after minting"); + + }); +}); diff --git a/test-suites/test-aave/reserve-pause.spec.ts b/test-suites/test-aave/reserve-pause.spec.ts index 22851764..b9686b48 100644 --- a/test-suites/test-aave/reserve-pause.spec.ts +++ b/test-suites/test-aave/reserve-pause.spec.ts @@ -22,6 +22,7 @@ makeSuite('Pause Reserve', (testEnv: TestEnv) => { _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); }); + it('User 0 deposits 1000 DAI. Configurator pauses pool. Transfers to user 1 reverts. Configurator unpauses the network and next transfer succeeds', async () => { const { users, pool, dai, aDai, configurator } = testEnv; diff --git a/test-suites/test-amm/helpers/utils/calculations.ts b/test-suites/test-amm/helpers/utils/calculations.ts index e1574656..310df15b 100644 --- a/test-suites/test-amm/helpers/utils/calculations.ts +++ b/test-suites/test-amm/helpers/utils/calculations.ts @@ -1195,7 +1195,7 @@ const calcLinearInterest = ( return cumulatedInterest; }; -const calcCompoundedInterest = ( +export const calcCompoundedInterest = ( rate: BigNumber, currentTimestamp: BigNumber, lastUpdateTimestamp: BigNumber