diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index def5606c..f61826f0 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -136,7 +136,15 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage bytes32 permitR, bytes32 permitS ) external override { - IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + IERC20WithPermit(asset).permit( + msg.sender, + address(this), + amount, + deadline, + permitV, + permitR, + permitS + ); _executeDeposit(asset, amount, onBehalfOf, referralCode); } @@ -243,7 +251,15 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage bytes32 permitR, bytes32 permitS ) external override returns (uint256) { - IERC20WithPermit(asset).permit(msg.sender, address(this), amount, deadline, permitV, permitR, permitS); + IERC20WithPermit(asset).permit( + msg.sender, + address(this), + amount, + deadline, + permitV, + permitR, + permitS + ); return _executeRepay(asset, amount, rateMode, onBehalfOf); } @@ -349,22 +365,22 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage { DataTypes.ReserveData storage reserve = _reserves[asset]; - ValidationLogic.validateSetUseReserveAsCollateral( - reserve, - asset, - useAsCollateral, - _reserves, - _usersConfig[msg.sender], - _reservesList, - _reservesCount, - _addressesProvider.getPriceOracle() - ); + ValidationLogic.validateSetUseReserveAsCollateral(reserve); _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, useAsCollateral); if (useAsCollateral) { emit ReserveUsedAsCollateralEnabled(asset, msg.sender); } else { + ValidationLogic.validateHealthFactor( + msg.sender, + _reserves, + _usersConfig[msg.sender], + _reservesList, + _reservesCount, + _addressesProvider.getPriceOracle() + ); + emit ReserveUsedAsCollateralDisabled(asset, msg.sender); } } @@ -671,7 +687,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } /** - * @dev Returns the fee on flash loans + * @dev Returns the fee on flash loans */ function FLASHLOAN_PREMIUM_TOTAL() public view returns (uint256) { return _flashLoanPremiumTotal; @@ -704,22 +720,26 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ) external override whenNotPaused { require(msg.sender == _reserves[asset].aTokenAddress, Errors.LP_CALLER_MUST_BE_AN_ATOKEN); - ValidationLogic.validateTransfer( - from, - _reserves, - _usersConfig[from], - _reservesList, - _reservesCount, - _addressesProvider.getPriceOracle() - ); - uint256 reserveId = _reserves[asset].id; if (from != to) { - if (balanceFromBefore.sub(amount) == 0) { - DataTypes.UserConfigurationMap storage fromConfig = _usersConfig[from]; - fromConfig.setUsingAsCollateral(reserveId, false); - emit ReserveUsedAsCollateralDisabled(asset, from); + DataTypes.UserConfigurationMap storage fromConfig = _usersConfig[from]; + + if (fromConfig.isUsingAsCollateral(reserveId)) { + if (fromConfig.isBorrowingAny()) { + ValidationLogic.validateHealthFactor( + from, + _reserves, + _usersConfig[from], + _reservesList, + _reservesCount, + _addressesProvider.getPriceOracle() + ); + } + if (balanceFromBefore.sub(amount) == 0) { + fromConfig.setUsingAsCollateral(reserveId, false); + emit ReserveUsedAsCollateralDisabled(asset, from); + } } if (balanceToBefore == 0 && amount != 0) { @@ -920,10 +940,15 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage address to ) internal returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[asset]; + DataTypes.UserConfigurationMap storage userConfig = _usersConfig[msg.sender]; address aToken = reserve.aTokenAddress; - uint256 userBalance = IAToken(aToken).balanceOf(msg.sender); + reserve.updateState(); + + uint256 liquidityIndex = reserve.liquidityIndex; + + uint256 userBalance = IAToken(aToken).scaledBalanceOf(msg.sender).rayMul(liquidityIndex); uint256 amountToWithdraw = amount; @@ -931,27 +956,29 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage amountToWithdraw = userBalance; } - ValidationLogic.validateWithdraw( - asset, - amountToWithdraw, - userBalance, - _reserves, - _usersConfig[msg.sender], - _reservesList, - _reservesCount, - _addressesProvider.getPriceOracle() - ); - - reserve.updateState(); + ValidationLogic.validateWithdraw(reserve, amountToWithdraw, userBalance); reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); - if (amountToWithdraw == userBalance) { - _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false); - emit ReserveUsedAsCollateralDisabled(asset, msg.sender); - } + IAToken(aToken).burn(msg.sender, to, amountToWithdraw, liquidityIndex); - IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex); + if (userConfig.isUsingAsCollateral(reserve.id)) { + if (userConfig.isBorrowingAny()) { + ValidationLogic.validateHealthFactor( + msg.sender, + _reserves, + userConfig, + _reservesList, + _reservesCount, + _addressesProvider.getPriceOracle() + ); + } + + if (amountToWithdraw == userBalance) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled(asset, msg.sender); + } + } emit Withdraw(asset, msg.sender, to, amountToWithdraw); diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 8756d797..9729379d 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -30,7 +30,6 @@ library Errors { string public constant VL_RESERVE_FROZEN = '3'; // 'Action cannot be performed because the reserve is frozen' string public constant VL_CURRENT_AVAILABLE_LIQUIDITY_NOT_ENOUGH = '4'; // 'The current liquidity is not enough' string public constant VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE = '5'; // 'User cannot withdraw more than the available balance' - string public constant VL_TRANSFER_NOT_ALLOWED = '6'; // 'Transfer cannot be allowed.' string public constant VL_BORROWING_NOT_ENABLED = '7'; // 'Borrowing is not enabled' string public constant VL_INVALID_INTEREST_RATE_MODE_SELECTED = '8'; // 'Invalid interest rate mode selected' string public constant VL_COLLATERAL_BALANCE_IS_0 = '9'; // 'The collateral balance is 0' diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 5722dd2b..381fe804 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -28,94 +28,6 @@ library GenericLogic { uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1 ether; - struct balanceDecreaseAllowedLocalVars { - uint256 decimals; - uint256 liquidationThreshold; - uint256 totalCollateralInETH; - uint256 totalDebtInETH; - uint256 avgLiquidationThreshold; - uint256 amountToDecreaseInETH; - uint256 collateralBalanceAfterDecrease; - uint256 liquidationThresholdAfterDecrease; - uint256 healthFactorAfterDecrease; - bool reserveUsageAsCollateralEnabled; - } - - /** - * @dev Checks if a specific balance decrease is allowed - * (i.e. doesn't bring the user borrow position health factor under HEALTH_FACTOR_LIQUIDATION_THRESHOLD) - * @param asset The address of the underlying asset of the reserve - * @param user The address of the user - * @param amount The amount to decrease - * @param reservesData The data of all the reserves - * @param userConfig The user configuration - * @param reserves The list of all the active reserves - * @param oracle The address of the oracle contract - * @return true if the decrease of the balance is allowed - **/ - function balanceDecreaseAllowed( - address asset, - address user, - uint256 amount, - mapping(address => DataTypes.ReserveData) storage reservesData, - DataTypes.UserConfigurationMap calldata userConfig, - mapping(uint256 => address) storage reserves, - uint256 reservesCount, - address oracle - ) external view returns (bool) { - if (!userConfig.isBorrowingAny() || !userConfig.isUsingAsCollateral(reservesData[asset].id)) { - return true; - } - - balanceDecreaseAllowedLocalVars memory vars; - - (, vars.liquidationThreshold, , vars.decimals, ) = reservesData[asset] - .configuration - .getParams(); - - if (vars.liquidationThreshold == 0) { - return true; - } - - ( - vars.totalCollateralInETH, - vars.totalDebtInETH, - , - vars.avgLiquidationThreshold, - - ) = calculateUserAccountData(user, reservesData, userConfig, reserves, reservesCount, oracle); - - if (vars.totalDebtInETH == 0) { - return true; - } - - vars.amountToDecreaseInETH = IPriceOracleGetter(oracle).getAssetPrice(asset).mul(amount).div( - 10**vars.decimals - ); - - vars.collateralBalanceAfterDecrease = vars.totalCollateralInETH.sub(vars.amountToDecreaseInETH); - - //if there is a borrow, there can't be 0 collateral - if (vars.collateralBalanceAfterDecrease == 0) { - return false; - } - - vars.liquidationThresholdAfterDecrease = vars - .totalCollateralInETH - .mul(vars.avgLiquidationThreshold) - .sub(vars.amountToDecreaseInETH.mul(vars.liquidationThreshold)) - .div(vars.collateralBalanceAfterDecrease); - - uint256 healthFactorAfterDecrease = - calculateHealthFactorFromBalances( - vars.collateralBalanceAfterDecrease, - vars.totalDebtInETH, - vars.liquidationThresholdAfterDecrease - ); - - return healthFactorAfterDecrease >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; - } - struct CalculateUserAccountDataVars { uint256 assetPrice; uint256 assetUnit; @@ -220,8 +132,7 @@ library GenericLogic { IERC20(currentReserve.stableDebtTokenAddress).balanceOf(user) ); vars.userDebtETH = vars.assetPrice.mul(vars.userDebt).div(vars.assetUnit); - vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); - + vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 080b792d..32f2de4d 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -38,7 +38,7 @@ library ValidationLogic { * @param reserve The reserve object on which the user is depositing * @param amount The amount to be deposited */ - function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) external view { + function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) internal view { (bool isActive, bool isFrozen, , ) = reserve.configuration.getFlags(); require(amount != 0, Errors.VL_INVALID_AMOUNT); @@ -46,46 +46,23 @@ library ValidationLogic { require(!isFrozen, Errors.VL_RESERVE_FROZEN); } + /** * @dev Validates a withdraw action - * @param reserveAddress The address of the reserve + * @param reserve The reserve object * @param amount The amount to be withdrawn * @param userBalance The balance of the user - * @param reservesData The reserves state - * @param userConfig The user configuration - * @param reserves The addresses of the reserves - * @param reservesCount The number of reserves - * @param oracle The price oracle */ function validateWithdraw( - address reserveAddress, + DataTypes.ReserveData storage reserve, uint256 amount, - uint256 userBalance, - mapping(address => DataTypes.ReserveData) storage reservesData, - DataTypes.UserConfigurationMap storage userConfig, - mapping(uint256 => address) storage reserves, - uint256 reservesCount, - address oracle - ) external view { + uint256 userBalance + ) internal view { require(amount != 0, Errors.VL_INVALID_AMOUNT); require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE); - (bool isActive, , , ) = reservesData[reserveAddress].configuration.getFlags(); + (bool isActive, , , ) = reserve.configuration.getFlags(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); - - require( - GenericLogic.balanceDecreaseAllowed( - reserveAddress, - msg.sender, - amount, - reservesData, - userConfig, - reserves, - reservesCount, - oracle - ), - Errors.VL_TRANSFER_NOT_ALLOWED - ); } struct ValidateBorrowLocalVars { @@ -130,7 +107,7 @@ library ValidationLogic { mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle - ) external view { + ) internal view { ValidateBorrowLocalVars memory vars; (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.stableRateBorrowingEnabled) = reserve @@ -227,7 +204,7 @@ library ValidationLogic { address onBehalfOf, uint256 stableDebt, uint256 variableDebt - ) external view { + ) internal view { bool isActive = reserve.configuration.getActive(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); @@ -335,40 +312,13 @@ 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 reserveAddress The address of the reserve - * @param reservesData The data of all the reserves - * @param userConfig The state of the user for the specific reserve - * @param reserves The addresses of all the active reserves - * @param oracle The price oracle */ function validateSetUseReserveAsCollateral( - DataTypes.ReserveData storage reserve, - address reserveAddress, - bool useAsCollateral, - mapping(address => DataTypes.ReserveData) storage reservesData, - DataTypes.UserConfigurationMap storage userConfig, - mapping(uint256 => address) storage reserves, - uint256 reservesCount, - address oracle + DataTypes.ReserveData storage reserve ) external view { uint256 underlyingBalance = IERC20(reserve.aTokenAddress).balanceOf(msg.sender); require(underlyingBalance > 0, Errors.VL_UNDERLYING_BALANCE_NOT_GREATER_THAN_0); - - require( - useAsCollateral || - GenericLogic.balanceDecreaseAllowed( - reserveAddress, - msg.sender, - underlyingBalance, - reservesData, - userConfig, - reserves, - reservesCount, - oracle - ), - Errors.VL_DEPOSIT_ALREADY_IN_USE - ); } /** @@ -436,14 +386,15 @@ library ValidationLogic { } /** - * @dev Validates an aToken transfer + * @dev Validates the health factor of a user * @param from The user from which the aTokens are being transferred * @param reservesData The state of all the reserves * @param userConfig The state of the user for the specific reserve * @param reserves The addresses of all the active reserves + * @param reservesCount The number of available reserves * @param oracle The price oracle */ - function validateTransfer( + function validateHealthFactor( address from, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, @@ -463,7 +414,7 @@ library ValidationLogic { require( healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, - Errors.VL_TRANSFER_NOT_ALLOWED + Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); } } diff --git a/helpers/types.ts b/helpers/types.ts index ea8826a0..8f6b19d2 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -111,7 +111,6 @@ export enum ProtocolErrors { VL_RESERVE_FROZEN = '3', // 'Action requires an unfrozen reserve' VL_CURRENT_AVAILABLE_LIQUIDITY_NOT_ENOUGH = '4', // 'The current liquidity is not enough' VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE = '5', // 'User cannot withdraw more than the available balance' - VL_TRANSFER_NOT_ALLOWED = '6', // 'Transfer cannot be allowed.' VL_BORROWING_NOT_ENABLED = '7', // 'Borrowing is not enabled' VL_INVALID_INTEREST_RATE_MODE_SELECTED = '8', // 'Invalid interest rate mode selected' VL_COLLATERAL_BALANCE_IS_0 = '9', // 'The collateral balance is 0' diff --git a/test-suites/test-aave/atoken-transfer.spec.ts b/test-suites/test-aave/atoken-transfer.spec.ts index c6451280..e5e13a36 100644 --- a/test-suites/test-aave/atoken-transfer.spec.ts +++ b/test-suites/test-aave/atoken-transfer.spec.ts @@ -12,7 +12,7 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { const { INVALID_FROM_BALANCE_AFTER_TRANSFER, INVALID_TO_BALANCE_AFTER_TRANSFER, - VL_TRANSFER_NOT_ALLOWED, + VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD, } = ProtocolErrors; it('User 0 deposits 1000 DAI, transfers to user 1', async () => { @@ -81,8 +81,8 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { await expect( aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer), - VL_TRANSFER_NOT_ALLOWED - ).to.be.revertedWith(VL_TRANSFER_NOT_ALLOWED); + VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ).to.be.revertedWith(VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); }); it('User 1 tries to transfer a small amount of DAI used as collateral back to user 0', async () => { diff --git a/test-suites/test-amm/atoken-transfer.spec.ts b/test-suites/test-amm/atoken-transfer.spec.ts index 0290f941..f645e221 100644 --- a/test-suites/test-amm/atoken-transfer.spec.ts +++ b/test-suites/test-amm/atoken-transfer.spec.ts @@ -12,7 +12,7 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { const { INVALID_FROM_BALANCE_AFTER_TRANSFER, INVALID_TO_BALANCE_AFTER_TRANSFER, - VL_TRANSFER_NOT_ALLOWED, + VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD, } = ProtocolErrors; it('User 0 deposits 1000 DAI, transfers to user 1', async () => { @@ -81,8 +81,8 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { await expect( aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer), - VL_TRANSFER_NOT_ALLOWED - ).to.be.revertedWith(VL_TRANSFER_NOT_ALLOWED); + VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ).to.be.revertedWith(VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); }); it('User 1 tries to transfer a small amount of DAI used as collateral back to user 0', async () => {