diff --git a/contracts/interfaces/IReserveInterestRateStrategy.sol b/contracts/interfaces/IReserveInterestRateStrategy.sol index 99f30b78..e04edbe5 100644 --- a/contracts/interfaces/IReserveInterestRateStrategy.sol +++ b/contracts/interfaces/IReserveInterestRateStrategy.sol @@ -10,9 +10,13 @@ interface IReserveInterestRateStrategy { /** * @dev returns the base variable borrow rate, in rays */ - function baseVariableBorrowRate() external view returns (uint256); + /** + * @dev returns the maximum variable borrow rate + */ + function getMaxVariableBorrowRate() external view returns (uint256); + /** * @dev calculates the liquidity, stable, and variable rates depending on the current utilization rate * and the base parameters diff --git a/contracts/lendingpool/DefaultReserveInterestRateStrategy.sol b/contracts/lendingpool/DefaultReserveInterestRateStrategy.sol index 045e9358..8ad61dca 100644 --- a/contracts/lendingpool/DefaultReserveInterestRateStrategy.sol +++ b/contracts/lendingpool/DefaultReserveInterestRateStrategy.sol @@ -91,6 +91,10 @@ contract DefaultReserveInterestRateStrategy is IReserveInterestRateStrategy { return _baseVariableBorrowRate; } + function getMaxVariableBorrowRate() external override view returns (uint256) { + return _baseVariableBorrowRate.add(_variableRateSlope1).add(_variableRateSlope2); + } + struct CalcInterestRatesLocalVars { uint256 totalBorrows; diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index d8d27969..95af83b1 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -28,21 +28,22 @@ import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; import {ILendingPool} from '../interfaces/ILendingPool.sol'; import {LendingPoolStorage} from './LendingPoolStorage.sol'; - +import {IReserveInterestRateStrategy} from '../interfaces/IReserveInterestRateStrategy.sol'; /** * @title LendingPool contract * @notice Implements the actions of the LendingPool, and exposes accessory methods to fetch the users and reserve data * @author Aave **/ - contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage { using SafeMath for uint256; using WadRayMath for uint256; + using PercentageMath for uint256; using SafeERC20 for IERC20; //main configuration parameters - uint256 public constant REBALANCE_DOWN_RATE_DELTA = (1e27) / 5; - uint256 public constant MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 25; + uint256 public constant REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD = 4000; + uint256 public constant REBALANCE_UP_USAGE_RATIO_THRESHOLD = 0.95 * 1e27; //usage ratio of 95% + uint256 public constant MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 2500; uint256 public constant FLASHLOAN_PREMIUM_TOTAL = 9; uint256 public constant MAX_NUMBER_RESERVES = 128; uint256 public constant LENDINGPOOL_REVISION = 0x2; @@ -50,13 +51,12 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage /** * @dev only lending pools configurator can use functions affected by this modifier **/ - function onlyLendingPoolConfigurator() internal view { + function _onlyLendingPoolConfigurator() internal view { require( _addressesProvider.getLendingPoolConfigurator() == msg.sender, Errors.CALLER_NOT_LENDING_POOL_CONFIGURATOR ); } - /** * @dev Function to make a function callable only when the contract is not paused. * @@ -64,7 +64,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * * - The contract must not be paused. */ - function whenNotPaused() internal view { + function _whenNotPaused() internal view { require(!_paused, Errors.IS_PAUSED); } @@ -94,7 +94,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address onBehalfOf, uint16 referralCode ) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateDeposit(reserve, amount); @@ -123,7 +123,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param amount the underlying amount to be redeemed **/ function withdraw(address asset, uint256 amount) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; address aToken = reserve.aTokenAddress; @@ -139,7 +139,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ValidationLogic.validateWithdraw( asset, - aToken, amountToWithdraw, userBalance, _reserves, @@ -161,6 +160,14 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage emit Withdraw(asset, msg.sender, amount); } + /** + * @dev returns the borrow allowance of the user + * @param asset The underlying asset of the debt token + * @param fromUser The user to giving allowance + * @param toUser The user to give allowance to + * @param interestRateMode Type of debt: 1 for stable, 2 for variable + * @return the current allowance of toUser + **/ function getBorrowAllowance( address fromUser, address toUser, @@ -184,7 +191,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 interestRateMode, uint256 amount ) external override { - whenNotPaused(); + _whenNotPaused(); address debtToken = _reserves[asset].getDebtTokenAddress(interestRateMode); _borrowAllowance[debtToken][msg.sender][user] = amount; @@ -207,7 +214,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint16 referralCode, address onBehalfOf ) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; if (onBehalfOf != msg.sender) { @@ -247,7 +254,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 rateMode, address onBehalfOf ) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; @@ -304,7 +311,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param rateMode the rate mode that the user wants to swap **/ function swapBorrowRateMode(address asset, uint256 rateMode) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(msg.sender, reserve); @@ -356,43 +363,50 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param user the address of the user to be rebalanced **/ function rebalanceStableBorrowRate(address asset, address user) external override { - whenNotPaused(); + + _whenNotPaused(); + ReserveLogic.ReserveData storage reserve = _reserves[asset]; - IStableDebtToken stableDebtToken = IStableDebtToken(reserve.stableDebtTokenAddress); + IERC20 stableDebtToken = IERC20(reserve.stableDebtTokenAddress); + IERC20 variableDebtToken = IERC20(reserve.variableDebtTokenAddress); + address aTokenAddress = reserve.aTokenAddress; - uint256 stableBorrowBalance = IERC20(address(stableDebtToken)).balanceOf(user); + uint256 stableBorrowBalance = IERC20(stableDebtToken).balanceOf(user); - // user must be borrowing on asset at a stable rate - require(stableBorrowBalance > 0, Errors.NOT_ENOUGH_STABLE_BORROW_BALANCE); + //if the utilization rate is below 95%, no rebalances are needed + uint256 totalBorrows = stableDebtToken.totalSupply().add(variableDebtToken.totalSupply()).wadToRay(); + uint256 availableLiquidity = IERC20(asset).balanceOf(aTokenAddress).wadToRay(); + uint256 usageRatio = totalBorrows == 0 + ? 0 + : totalBorrows.rayDiv(availableLiquidity.add(totalBorrows)); - uint256 rebalanceDownRateThreshold = WadRayMath.ray().add(REBALANCE_DOWN_RATE_DELTA).rayMul( - reserve.currentStableBorrowRate - ); + //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. - //1. user stable borrow rate is below the current liquidity rate. The loan needs to be rebalanced, - //as this situation can be abused (user putting back the borrowed liquidity in the same reserve to earn on it) - //2. user stable rate is above the market avg borrow rate of a certain delta, and utilization rate is low. - //In this case, the user is paying an interest that is too high, and needs to be rescaled down. - - uint256 userStableRate = stableDebtToken.getUserStableRate(user); + uint256 currentLiquidityRate = reserve.currentLiquidityRate; + uint256 maxVariableBorrowRate = IReserveInterestRateStrategy( + reserve + .interestRateStrategyAddress + ) + .getMaxVariableBorrowRate(); require( - userStableRate < reserve.currentLiquidityRate || userStableRate > rebalanceDownRateThreshold, + usageRatio >= REBALANCE_UP_USAGE_RATIO_THRESHOLD && + currentLiquidityRate <= + maxVariableBorrowRate.percentMul(REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD), Errors.INTEREST_RATE_REBALANCE_CONDITIONS_NOT_MET ); reserve.updateState(); - //burn old debt tokens, mint new ones - stableDebtToken.burn(user, stableBorrowBalance); - stableDebtToken.mint(user, stableBorrowBalance, reserve.currentStableBorrowRate); + IStableDebtToken(address(stableDebtToken)).burn(user, stableBorrowBalance); + IStableDebtToken(address(stableDebtToken)).mint(user, stableBorrowBalance, reserve.currentStableBorrowRate); - reserve.updateInterestRates(asset, reserve.aTokenAddress, 0, 0); + reserve.updateInterestRates(asset, aTokenAddress, 0, 0); emit RebalanceStableBorrowRate(asset, user); - return; } /** @@ -401,7 +415,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param useAsCollateral true if the user wants to user the deposit as collateral, false otherwise. **/ function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateSetUseReserveAsCollateral( @@ -438,7 +452,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 purchaseAmount, bool receiveAToken ) external override { - whenNotPaused(); + _whenNotPaused(); address collateralManager = _addressesProvider.getLendingPoolCollateralManager(); //solium-disable-next-line @@ -482,7 +496,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address receiver, bytes calldata params ) external override { - whenNotPaused(); + _whenNotPaused(); require(!_flashLiquidationLocked, Errors.REENTRANCY_NOT_ALLOWED); _flashLiquidationLocked = true; @@ -538,7 +552,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage bytes calldata params, uint16 referralCode ) external override { - whenNotPaused(); + _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; FlashLoanLocalVars memory vars; @@ -600,7 +614,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage uint256 amountToSwap, bytes calldata params ) external override { - whenNotPaused(); + _whenNotPaused(); address collateralManager = _addressesProvider.getLendingPoolCollateralManager(); //solium-disable-next-line @@ -740,7 +754,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _reservesList, _addressesProvider.getPriceOracle() ); - + availableBorrowsETH = GenericLogic.calculateAvailableBorrowsETH( totalCollateralETH, totalBorrowsETH, @@ -799,7 +813,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address variableDebtAddress, address interestRateStrategyAddress ) external override { - onlyLendingPoolConfigurator(); + _onlyLendingPoolConfigurator(); _reserves[asset].init( aTokenAddress, stableDebtAddress, @@ -819,12 +833,12 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage external override { - onlyLendingPoolConfigurator(); + _onlyLendingPoolConfigurator(); _reserves[asset].interestRateStrategyAddress = rateStrategyAddress; } function setConfiguration(address asset, uint256 configuration) external override { - onlyLendingPoolConfigurator(); + _onlyLendingPoolConfigurator(); _reserves[asset].configuration.data = configuration; } @@ -980,7 +994,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address user, uint256 amount ) external override view returns (bool) { - whenNotPaused(); + _whenNotPaused(); return GenericLogic.balanceDecreaseAllowed( asset, @@ -998,7 +1012,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage * @param val the boolean value to set the current pause state of LendingPool */ function setPause(bool val) external override { - onlyLendingPoolConfigurator(); + _onlyLendingPoolConfigurator(); _paused = val; if (_paused) { diff --git a/contracts/libraries/configuration/ReserveConfiguration.sol b/contracts/libraries/configuration/ReserveConfiguration.sol index ed4df31e..66fa21f4 100644 --- a/contracts/libraries/configuration/ReserveConfiguration.sol +++ b/contracts/libraries/configuration/ReserveConfiguration.sol @@ -6,7 +6,6 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {ReserveLogic} from '../logic/ReserveLogic.sol'; import {WadRayMath} from '../math/WadRayMath.sol'; import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; -import "@nomiclabs/buidler/console.sol"; /** * @title ReserveConfiguration library @@ -143,7 +142,7 @@ library ReserveConfiguration { * @param self the reserve configuration * @param active the active state **/ - function setActive(ReserveConfiguration.Map memory self, bool active) internal { + function setActive(ReserveConfiguration.Map memory self, bool active) internal pure { self.data = (self.data & ACTIVE_MASK) | (uint256(active ? 1 : 0) << 56); } diff --git a/contracts/libraries/logic/GenericLogic.sol b/contracts/libraries/logic/GenericLogic.sol index 8cb33370..4b64e85f 100644 --- a/contracts/libraries/logic/GenericLogic.sol +++ b/contracts/libraries/logic/GenericLogic.sol @@ -254,7 +254,7 @@ library GenericLogic { return (collateralBalanceETH.percentMul(liquidationThreshold)).wadDiv(borrowBalanceETH); } - /** + /** * @dev calculates the equivalent amount in ETH that an user can borrow, depending on the available collateral and the * average Loan To Value. * @param collateralBalanceETH the total collateral balance diff --git a/contracts/libraries/logic/ReserveLogic.sol b/contracts/libraries/logic/ReserveLogic.sol index 8f6069ab..8a3604f7 100644 --- a/contracts/libraries/logic/ReserveLogic.sol +++ b/contracts/libraries/logic/ReserveLogic.sol @@ -127,7 +127,7 @@ library ReserveLogic { * @return an address of the corresponding debt token from reserve configuration **/ function getDebtTokenAddress(ReserveLogic.ReserveData storage reserve, uint256 interestRateMode) - internal + external view returns (address) { @@ -297,7 +297,7 @@ library ReserveLogic { uint40 stableSupplyUpdatedTimestamp; } - /** + /** * @dev mints part of the repaid interest to the reserve treasury, depending on the reserveFactor for the * specific asset. * @param reserve the reserve reserve to be updated @@ -358,7 +358,7 @@ library ReserveLogic { IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex); } - /** + /** * @dev updates the reserve indexes and the timestamp of the update * @param reserve the reserve reserve to be updated * @param variableDebtToken the debt token address @@ -371,7 +371,6 @@ library ReserveLogic { uint256 liquidityIndex, uint256 variableBorrowIndex ) internal returns (uint256, uint256) { - uint40 timestamp = reserve.lastUpdateTimestamp; uint256 currentLiquidityRate = reserve.currentLiquidityRate; diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 5d201efd..4ffa49f2 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -34,7 +34,7 @@ library ValidationLogic { * @param reserve the reserve state on which the user is depositing * @param amount the amount to be deposited */ - function validateDeposit(ReserveLogic.ReserveData storage reserve, uint256 amount) internal view { + function validateDeposit(ReserveLogic.ReserveData storage reserve, uint256 amount) external view { (bool isActive, bool isFreezed, , ) = reserve.configuration.getFlags(); require(amount > 0, Errors.AMOUNT_NOT_GREATER_THAN_0); @@ -45,13 +45,11 @@ library ValidationLogic { /** * @dev validates a withdraw action. * @param reserveAddress the address of the reserve - * @param aTokenAddress the address of the aToken for the reserve * @param amount the amount to be withdrawn * @param userBalance the balance of the user */ function validateWithdraw( address reserveAddress, - address aTokenAddress, uint256 amount, uint256 userBalance, mapping(address => ReserveLogic.ReserveData) storage reservesData, diff --git a/contracts/tokenization/StableDebtToken.sol b/contracts/tokenization/StableDebtToken.sol index 1974d51e..160d0f88 100644 --- a/contracts/tokenization/StableDebtToken.sol +++ b/contracts/tokenization/StableDebtToken.sol @@ -8,7 +8,6 @@ import {DebtTokenBase} from './base/DebtTokenBase.sol'; import {MathUtils} from '../libraries/math/MathUtils.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {IStableDebtToken} from './interfaces/IStableDebtToken.sol'; -import "@nomiclabs/buidler/console.sol"; /** * @title contract StableDebtToken diff --git a/test/helpers/scenarios/rebalance-stable-rate.json b/test/helpers/scenarios/rebalance-stable-rate.json index 1200ff3e..70ea820a 100644 --- a/test/helpers/scenarios/rebalance-stable-rate.json +++ b/test/helpers/scenarios/rebalance-stable-rate.json @@ -19,7 +19,7 @@ ] }, { - "description": "User 0 deposits 1000 DAI, user 1 deposits 1 ETH, borrows 100 DAI at a variable rate, user 0 rebalances user 1 (revert expected)", + "description": "User 0 deposits 1000 DAI, user 1 deposits 5 ETH, borrows 600 DAI at a variable rate, user 0 rebalances user 1 (revert expected)", "actions": [ { "name": "mint", @@ -51,7 +51,7 @@ "name": "mint", "args": { "reserve": "WETH", - "amount": "1", + "amount": "5", "user": "1" }, "expected": "success" @@ -69,7 +69,7 @@ "args": { "reserve": "WETH", - "amount": "1", + "amount": "5", "user": "1" }, "expected": "success" @@ -78,8 +78,8 @@ "name": "borrow", "args": { "reserve": "DAI", - "amount": "100", - "borrowRateMode": "variable", + "amount": "250", + "borrowRateMode": "stable", "user": "1", "timeTravel": "365" }, @@ -98,14 +98,16 @@ ] }, { - "description": "User 1 swaps to stable, user 0 tries to rebalance but the conditions are not met (revert expected)", + "description": "User 1 borrows another 200 at stable, user 0 tries to rebalance but the conditions are not met (revert expected)", "actions": [ { - "name": "swapBorrowRateMode", + "name": "borrow", "args": { "reserve": "DAI", + "amount": "200", + "borrowRateMode": "stable", "user": "1", - "borrowRateMode": "variable" + "timeTravel": "365" }, "expected": "success" }, @@ -122,7 +124,59 @@ ] }, { - "description": "User 2 deposits ETH and borrows the remaining DAI, causing the stable rates to rise (liquidity rate < user 1 borrow rate). User 0 tries to rebalance user 1 (revert expected)", + "description": "User 1 borrows another 200 at stable, user 0 tries to rebalance but the conditions are not met (revert expected)", + "actions": [ + { + "name": "borrow", + "args": { + "reserve": "DAI", + "amount": "200", + "borrowRateMode": "stable", + "user": "1", + "timeTravel": "365" + }, + "expected": "success" + }, + { + "name": "rebalanceStableBorrowRate", + "args": { + "reserve": "DAI", + "user": "0", + "target": "1" + }, + "expected": "revert", + "revertMessage": "Interest rate rebalance conditions were not met" + } + ] + }, + { + "description": "User 1 borrows another 100 at stable, user 0 tries to rebalance but the conditions are not met (revert expected)", + "actions": [ + { + "name": "borrow", + "args": { + "reserve": "DAI", + "amount": "100", + "borrowRateMode": "stable", + "user": "1", + "timeTravel": "365" + }, + "expected": "success" + }, + { + "name": "rebalanceStableBorrowRate", + "args": { + "reserve": "DAI", + "user": "0", + "target": "1" + }, + "expected": "revert", + "revertMessage": "Interest rate rebalance conditions were not met" + } + ] + }, + { + "description": "User 2 deposits ETH and borrows the remaining DAI, causing the stable rates to rise (usage ratio = 94%). User 0 tries to rebalance user 1 (revert expected)", "actions": [ { "name": "mint", @@ -155,7 +209,7 @@ "name": "borrow", "args": { "reserve": "DAI", - "amount": "100", + "amount": "190", "borrowRateMode": "variable", "user": "2" }, @@ -174,40 +228,13 @@ ] }, { - "description": "User 2 borrows more DAI, causing the liquidity rate to rise above user 1 stable borrow rate User 0 rebalances user 1", + "description": "User 2 borrows the remaining DAI (usage ratio = 100%). User 0 rebalances user 1", "actions": [ - { - "name": "mint", - "args": { - "reserve": "WETH", - "amount": "3", - "user": "2" - }, - "expected": "success" - }, - { - "name": "approve", - "args": { - "reserve": "WETH", - "user": "2" - }, - "expected": "success" - }, - { - "name": "deposit", - "args": { - "reserve": "WETH", - - "amount": "3", - "user": "2" - }, - "expected": "success" - }, { "name": "borrow", "args": { "reserve": "DAI", - "amount": "700", + "amount": "60", "borrowRateMode": "variable", "user": "2" },