diff --git a/contracts/deployments/ATokensAndRatesHelper.sol b/contracts/deployments/ATokensAndRatesHelper.sol index 15a5e81a..5d77bb44 100644 --- a/contracts/deployments/ATokensAndRatesHelper.sol +++ b/contracts/deployments/ATokensAndRatesHelper.sol @@ -33,6 +33,7 @@ contract ATokensAndRatesHelper is Ownable { uint256 reserveFactor; uint256 borrowCap; uint256 supplyCap; + uint256 exposureCap; bool stableBorrowingEnabled; bool borrowingEnabled; } @@ -73,7 +74,8 @@ contract ATokensAndRatesHelper is Ownable { inputParams[i].asset, inputParams[i].baseLTV, inputParams[i].liquidationThreshold, - inputParams[i].liquidationBonus + inputParams[i].liquidationBonus, + inputParams[i].exposureCap ); if (inputParams[i].borrowingEnabled) { diff --git a/contracts/interfaces/ILendingPoolConfigurator.sol b/contracts/interfaces/ILendingPoolConfigurator.sol index 634bde26..e3803ae6 100644 --- a/contracts/interfaces/ILendingPoolConfigurator.sol +++ b/contracts/interfaces/ILendingPoolConfigurator.sol @@ -159,6 +159,13 @@ interface ILendingPoolConfigurator { **/ event SupplyCapChanged(address indexed asset, uint256 supplyCap); + /** + * @dev Emitted when the exposure cap of a reserve is updated + * @param asset The address of the underlying asset of the reserve + * @param exposureCap The new exposure cap + **/ + event ExposureCapChanged(address indexed asset, uint256 exposureCap); + /** * @dev Emitted when the reserve decimals are updated * @param asset The address of the underlying asset of the reserve @@ -294,13 +301,15 @@ interface ILendingPoolConfigurator { * @param ltv The loan to value of the asset when used as collateral * @param liquidationThreshold The threshold at which loans using this asset as collateral will be considered undercollateralized * @param liquidationBonus The bonus liquidators receive to liquidate this asset. The values is always above 100%. A value of 105% + * @param exposureCap The exposure cap for the collateral reserve. If cap is reached, effective LTV = 0 * means the liquidator will receive a 5% bonus **/ function configureReserveAsCollateral( address asset, uint256 ltv, uint256 liquidationThreshold, - uint256 liquidationBonus + uint256 liquidationBonus, + uint256 exposureCap ) external; /** @@ -387,6 +396,13 @@ interface ILendingPoolConfigurator { **/ function setSupplyCap(address asset, uint256 supplyCap) external; + /** + * @dev Updates the exposure cap of a reserve + * @param asset The address of the underlying asset of the reserve + * @param exposureCap The new exposure of the reserve + **/ + function setExposureCap(address asset, uint256 exposureCap) external; + /** * @dev Registers a new admin with rights on risk related configurations * @param admin The address of the admin to register diff --git a/contracts/misc/AaveProtocolDataProvider.sol b/contracts/misc/AaveProtocolDataProvider.sol index 95deb6ea..ea22b29e 100644 --- a/contracts/misc/AaveProtocolDataProvider.sol +++ b/contracts/misc/AaveProtocolDataProvider.sol @@ -81,14 +81,14 @@ contract AaveProtocolDataProvider { bool isFrozen ) { - DataTypes.ReserveConfigurationMap memory configuration = + DataTypes.ReserveConfigurationMap memory configuration = ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).getConfiguration(asset); - (ltv, liquidationThreshold, liquidationBonus, decimals, reserveFactor) = - configuration.getParamsMemory(); + (ltv, liquidationThreshold, liquidationBonus, decimals, reserveFactor) = configuration + .getParamsMemory(); - (isActive, isFrozen, borrowingEnabled, stableBorrowRateEnabled, ) = - configuration.getFlagsMemory(); + (isActive, isFrozen, borrowingEnabled, stableBorrowRateEnabled, ) = configuration + .getFlagsMemory(); usageAsCollateralEnabled = liquidationThreshold > 0; } @@ -96,18 +96,21 @@ contract AaveProtocolDataProvider { function getReserveCaps(address asset) external view - returns (uint256 borrowCap, uint256 supplyCap) { - - (borrowCap, supplyCap) = ILendingPool(ADDRESSES_PROVIDER.getLendingPool()) - .getConfiguration(asset) - .getCapsMemory(); - } + returns ( + uint256 borrowCap, + uint256 supplyCap, + uint256 exposureCap + ) + { + (borrowCap, supplyCap, exposureCap) = ILendingPool(ADDRESSES_PROVIDER.getLendingPool()) + .getConfiguration(asset) + .getCapsMemory(); + } function getPaused(address asset) external view returns (bool isPaused) { - (, , , , isPaused) = - ILendingPool(ADDRESSES_PROVIDER.getLendingPool()) - .getConfiguration(asset) - .getFlagsMemory(); + (, , , , isPaused) = ILendingPool(ADDRESSES_PROVIDER.getLendingPool()) + .getConfiguration(asset) + .getFlagsMemory(); } function getReserveData(address asset) diff --git a/contracts/misc/UiPoolDataProvider.sol b/contracts/misc/UiPoolDataProvider.sol index 4e4641c3..8c05686c 100644 --- a/contracts/misc/UiPoolDataProvider.sol +++ b/contracts/misc/UiPoolDataProvider.sol @@ -114,10 +114,9 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.decimals, reserveData.reserveFactor ) = baseData.configuration.getParamsMemory(); - ( - reserveData.borrowCap, - reserveData.supplyCap - ) = baseData.configuration.getCapsMemory(); + (reserveData.borrowCap, reserveData.supplyCap, reserveData.exposureCap) = baseData + .configuration + .getCapsMemory(); ( reserveData.isActive, reserveData.isFrozen, diff --git a/contracts/misc/interfaces/IUiPoolDataProvider.sol b/contracts/misc/interfaces/IUiPoolDataProvider.sol index ac7d344a..fdfd6895 100644 --- a/contracts/misc/interfaces/IUiPoolDataProvider.sol +++ b/contracts/misc/interfaces/IUiPoolDataProvider.sol @@ -17,6 +17,7 @@ interface IUiPoolDataProvider { uint256 reserveFactor; uint256 borrowCap; uint256 supplyCap; + uint256 exposureCap; bool usageAsCollateralEnabled; bool borrowingEnabled; bool stableBorrowRateEnabled; diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 2396d8f5..1bb6b684 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -289,7 +289,8 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage if (useAsCollateral) { emit ReserveUsedAsCollateralEnabled(asset, msg.sender); } else { - ValidationLogic.validateHealthFactor( + ValidationLogic.validateHFAndExposureCap( + asset, msg.sender, _reserves, _usersConfig[msg.sender], @@ -496,7 +497,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage totalDebtETH, ltv, currentLiquidationThreshold, - healthFactor + healthFactor, ) = GenericLogic.getUserAccountData( user, _reserves, @@ -628,7 +629,8 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage if (fromConfig.isUsingAsCollateral(reserveId)) { if (fromConfig.isBorrowingAny()) { - ValidationLogic.validateHealthFactor( + ValidationLogic.validateHFAndExposureCap( + asset, from, _reserves, _usersConfig[from], @@ -875,7 +877,8 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage if (userConfig.isUsingAsCollateral(reserve.id)) { if (userConfig.isBorrowingAny()) { - ValidationLogic.validateHealthFactor( + ValidationLogic.validateHFAndExposureCap( + asset, msg.sender, _reserves, userConfig, diff --git a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol index f8b730ee..6cbfc0f3 100644 --- a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol @@ -278,7 +278,6 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur _pool.setConfiguration(asset, currentConfig.data); - emit BorrowCapChanged(asset, borrowCap); emit BorrowingEnabledOnReserve(asset, stableBorrowRateEnabled); } @@ -297,7 +296,8 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur address asset, uint256 ltv, uint256 liquidationThreshold, - uint256 liquidationBonus + uint256 liquidationBonus, + uint256 exposureCap ) external override onlyRiskOrPoolAdmins { DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); @@ -331,6 +331,7 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur currentConfig.setLtv(ltv); currentConfig.setLiquidationThreshold(liquidationThreshold); currentConfig.setLiquidationBonus(liquidationBonus); + currentConfig.setExposureCap(exposureCap); _pool.setConfiguration(asset, currentConfig.data); @@ -464,6 +465,21 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur emit SupplyCapChanged(asset, supplyCap); } + ///@inheritdoc ILendingPoolConfigurator + function setExposureCap(address asset, uint256 exposureCap) + external + override + onlyRiskOrPoolAdmins + { + DataTypes.ReserveConfigurationMap memory currentConfig = _pool.getConfiguration(asset); + + currentConfig.setExposureCap(exposureCap); + + _pool.setConfiguration(asset, currentConfig.data); + + emit ExposureCapChanged(asset, exposureCap); + } + ///@inheritdoc ILendingPoolConfigurator function setReserveInterestRateStrategyAddress(address asset, address rateStrategyAddress) external diff --git a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol index b369aaee..ba2aa0c4 100644 --- a/contracts/protocol/libraries/configuration/ReserveConfiguration.sol +++ b/contracts/protocol/libraries/configuration/ReserveConfiguration.sol @@ -22,6 +22,7 @@ library ReserveConfiguration { uint256 constant RESERVE_FACTOR_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFF; // prettier-ignore uint256 constant BORROW_CAP_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFF; // prettier-ignore uint256 constant SUPPLY_CAP_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore + uint256 constant EXPOSURE_CAP_MASK = 0xFFFFFFFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore /// @dev For the LTV, the start bit is 0 (up to 15), hence no bitshifting is needed uint256 constant LIQUIDATION_THRESHOLD_START_BIT_POSITION = 16; @@ -36,6 +37,7 @@ library ReserveConfiguration { uint256 constant RESERVE_FACTOR_START_BIT_POSITION = 64; uint256 constant BORROW_CAP_START_BIT_POSITION = 80; uint256 constant SUPPLY_CAP_START_BIT_POSITION = 116; + uint256 constant EXPOSURE_CAP_START_BIT_POSITION = 152; uint256 constant MAX_VALID_LTV = 65535; uint256 constant MAX_VALID_LIQUIDATION_THRESHOLD = 65535; @@ -44,6 +46,7 @@ library ReserveConfiguration { uint256 constant MAX_VALID_RESERVE_FACTOR = 65535; uint256 constant MAX_VALID_BORROW_CAP = 68719476735; uint256 constant MAX_VALID_SUPPLY_CAP = 68719476735; + uint256 constant MAX_VALID_EXPOSURE_CAP = 68719476735; /** * @dev Sets the Loan to Value of the reserve @@ -213,7 +216,7 @@ library ReserveConfiguration { return (self.data & ~FROZEN_MASK) != 0; } - /** + /** * @dev Sets the paused state of the reserve * @param self The reserve configuration * @param paused The paused state @@ -383,6 +386,33 @@ library ReserveConfiguration { return (self.data & ~SUPPLY_CAP_MASK) >> SUPPLY_CAP_START_BIT_POSITION; } + /** + * @dev Sets the exposure cap of the reserve + * @param self The reserve configuration + * @param exposureCap The exposure cap + **/ + function setExposureCap(DataTypes.ReserveConfigurationMap memory self, uint256 exposureCap) + internal + pure + { + require(exposureCap <= MAX_VALID_EXPOSURE_CAP, Errors.RC_INVALID_EXPOSURE_CAP); + + self.data = (self.data & EXPOSURE_CAP_MASK) | (exposureCap << EXPOSURE_CAP_START_BIT_POSITION); + } + + /** + * @dev Gets the exposure cap of the reserve + * @param self The reserve configuration + * @return The exposure cap + **/ + function getExposureCap(DataTypes.ReserveConfigurationMap storage self) + internal + view + returns (uint256) + { + return (self.data & ~EXPOSURE_CAP_MASK) >> EXPOSURE_CAP_START_BIT_POSITION; + } + /** * @dev Gets the configuration flags of the reserve * @param self The reserve configuration @@ -445,13 +475,18 @@ library ReserveConfiguration { function getCaps(DataTypes.ReserveConfigurationMap storage self) internal view - returns (uint256, uint256) + returns ( + uint256, + uint256, + uint256 + ) { uint256 dataLocal = self.data; return ( (dataLocal & ~BORROW_CAP_MASK) >> BORROW_CAP_START_BIT_POSITION, - (dataLocal & ~SUPPLY_CAP_MASK) >> SUPPLY_CAP_START_BIT_POSITION + (dataLocal & ~SUPPLY_CAP_MASK) >> SUPPLY_CAP_START_BIT_POSITION, + (dataLocal & ~EXPOSURE_CAP_MASK) >> EXPOSURE_CAP_START_BIT_POSITION ); } @@ -488,11 +523,16 @@ library ReserveConfiguration { function getCapsMemory(DataTypes.ReserveConfigurationMap memory self) internal pure - returns (uint256, uint256) + returns ( + uint256, + uint256, + uint256 + ) { return ( (self.data & ~BORROW_CAP_MASK) >> BORROW_CAP_START_BIT_POSITION, - (self.data & ~SUPPLY_CAP_MASK) >> SUPPLY_CAP_START_BIT_POSITION + (self.data & ~SUPPLY_CAP_MASK) >> SUPPLY_CAP_START_BIT_POSITION, + (self.data & ~EXPOSURE_CAP_MASK) >> EXPOSURE_CAP_START_BIT_POSITION ); } @@ -546,4 +586,17 @@ library ReserveConfiguration { { return (self.data & ~BORROW_CAP_MASK) >> BORROW_CAP_START_BIT_POSITION; } + + /** + * @dev Gets the exposure cap of the reserve from a memory object + * @param self The reserve configuration + * @return The exposure cap + **/ + function getExposureCapMemory(DataTypes.ReserveConfigurationMap memory self) + internal + pure + returns (uint256) + { + return (self.data & ~EXPOSURE_CAP_MASK) >> EXPOSURE_CAP_START_BIT_POSITION; + } } diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 8635ba8f..6f4fdb4d 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -113,6 +113,8 @@ library Errors { string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; string public constant LP_CALLER_NOT_EOA = '91'; + string public constant RC_INVALID_EXPOSURE_CAP = '92'; + string public constant VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '93'; string public constant VL_SAME_BLOCK_BORROW_REPAY = '94'; string public constant LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95'; string public constant LPC_FLASHLOAN_PREMIUM_INVALID = '96'; diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index fb867ce2..5a087bc2 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -34,6 +34,7 @@ library GenericLogic { uint256 userBalance; uint256 userBalanceETH; uint256 userDebt; + uint256 userStableDebt; uint256 userDebtETH; uint256 decimals; uint256 ltv; @@ -43,14 +44,18 @@ library GenericLogic { uint256 totalCollateralInETH; uint256 totalDebtInETH; uint256 avgLtv; + uint256 avgUncappedLtv; uint256 avgLiquidationThreshold; uint256 reservesLength; uint256 normalizedIncome; uint256 normalizedDebt; + uint256 exposureCap; + uint256 aTokenSupply; bool healthFactorBelowThreshold; address currentReserveAddress; bool usageAsCollateralEnabled; bool userUsesReserveAsCollateral; + bool exposureCapCrossed; } /** @@ -62,7 +67,7 @@ library GenericLogic { * @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 + * @return The total collateral and total debt of the user in ETH, the avg ltv, liquidation threshold, the HF and the uncapped avg ltv (without exposure ceiling) **/ function calculateUserAccountData( address user, @@ -79,13 +84,14 @@ library GenericLogic { uint256, uint256, uint256, + uint256, uint256 ) { CalculateUserAccountDataVars memory vars; if (userConfig.isEmpty()) { - return (0, 0, 0, 0, uint256(-1)); + return (0, 0, 0, 0, uint256(-1), 0); } for (vars.i = 0; vars.i < reservesCount; vars.i++) { if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) { @@ -101,28 +107,44 @@ library GenericLogic { (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); + vars.exposureCap = currentReserve.configuration.getExposureCap(); vars.assetUnit = 10**vars.decimals; vars.assetPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress); if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) { - vars.userBalance = IScaledBalanceToken(currentReserve.aTokenAddress).scaledBalanceOf(user); - if (vars.userBalance > 0) { - vars.normalizedIncome = currentReserve.getNormalizedIncome(); + vars.normalizedIncome = currentReserve.getNormalizedIncome(); + + if (vars.exposureCap != 0) { + (vars.userBalance, vars.aTokenSupply) = IScaledBalanceToken(currentReserve.aTokenAddress) + .getScaledUserBalanceAndSupply(user); + vars.userBalance = vars.userBalance.rayMul(vars.normalizedIncome); + vars.aTokenSupply = vars.aTokenSupply.rayMul(vars.normalizedIncome); + } else { + vars.userBalance = IScaledBalanceToken(currentReserve.aTokenAddress).scaledBalanceOf( + user + ); + vars.aTokenSupply = 0; } vars.userBalanceETH = vars.assetPrice.mul(vars.userBalance).div(vars.assetUnit); - vars.totalCollateralInETH = vars.totalCollateralInETH.add(vars.userBalanceETH); + vars.exposureCapCrossed = + vars.exposureCap != 0 && + vars.aTokenSupply.div(10**vars.decimals) > vars.exposureCap; - vars.avgLtv = vars.avgLtv.add(vars.userBalanceETH.mul(vars.ltv)); + vars.avgLtv = vars.avgLtv.add( + vars.exposureCapCrossed ? 0 : vars.userBalanceETH.mul(vars.ltv) + ); + vars.avgUncappedLtv = vars.avgUncappedLtv.add(vars.userBalanceETH.mul(vars.ltv)); vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add( vars.userBalanceETH.mul(vars.liquidationThreshold) ); } if (userConfig.isBorrowing(vars.i)) { + vars.userStableDebt = IERC20(currentReserve.stableDebtTokenAddress).balanceOf(user); vars.userDebt = IScaledBalanceToken(currentReserve.variableDebtTokenAddress) .scaledBalanceOf(user); @@ -130,16 +152,16 @@ library GenericLogic { vars.normalizedDebt = currentReserve.getNormalizedDebt(); vars.userDebt = vars.userDebt.rayMul(vars.normalizedDebt); } - - vars.userDebt = vars.userDebt.add( - IERC20(currentReserve.stableDebtTokenAddress).balanceOf(user) - ); + vars.userDebt = vars.userDebt.add(vars.userStableDebt); vars.userDebtETH = vars.assetPrice.mul(vars.userDebt).div(vars.assetUnit); vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); } } vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0; + vars.avgUncappedLtv = vars.totalCollateralInETH > 0 + ? vars.avgUncappedLtv.div(vars.totalCollateralInETH) + : 0; vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0 ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH) : 0; @@ -154,7 +176,8 @@ library GenericLogic { vars.totalDebtInETH, vars.avgLtv, vars.avgLiquidationThreshold, - vars.healthFactor + vars.healthFactor, + vars.avgUncappedLtv ); } @@ -200,7 +223,7 @@ library GenericLogic { } /** - * @dev proxy call for calculateUserAccountData as external function. + * @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 @@ -224,6 +247,7 @@ library GenericLogic { uint256, uint256, uint256, + uint256, uint256 ) { diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 9c7ea35a..42fee42a 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -185,7 +185,7 @@ library ValidationLogic { vars.userBorrowBalanceETH, vars.currentLtv, vars.currentLiquidationThreshold, - vars.healthFactor + vars.healthFactor, ) = GenericLogic.calculateUserAccountData( userAddress, reservesData, @@ -263,7 +263,7 @@ library ValidationLogic { address onBehalfOf, uint256 stableDebt, uint256 variableDebt - ) internal view { + ) external view { (bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isPaused, Errors.VL_RESERVE_PAUSED); @@ -469,7 +469,7 @@ library ValidationLogic { return (uint256(Errors.CollateralManagerErrors.PAUSED_RESERVE), Errors.VL_RESERVE_PAUSED); } - (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + (, , , , vars.healthFactor, ) = GenericLogic.calculateUserAccountData( user, reservesData, userConfig, @@ -508,7 +508,7 @@ library ValidationLogic { } /** - * @dev Validates the health factor of a user + * @dev Validates the health factor of a user and the exposure cap for the asset being withdrawn * @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 @@ -516,7 +516,8 @@ library ValidationLogic { * @param reservesCount The number of available reserves * @param oracle The price oracle */ - function validateHealthFactor( + function validateHFAndExposureCap( + address collateral, address from, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, @@ -524,7 +525,8 @@ library ValidationLogic { uint256 reservesCount, address oracle ) external view { - (, , , , uint256 healthFactor) = + DataTypes.ReserveData memory reserve = reservesData[collateral]; + (, , uint256 ltv, uint256 liquidationThreshold, uint256 healthFactor, uint256 uncappedLtv) = GenericLogic.calculateUserAccountData( from, reservesData, @@ -534,10 +536,22 @@ library ValidationLogic { oracle ); + require( healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); + + uint256 exposureCap = reserve.configuration.getExposureCapMemory(); + + if (exposureCap != 0) { + if (ltv < uncappedLtv) { + uint256 totalSupplyAtoken = IERC20(reserve.aTokenAddress).totalSupply(); + (, , , uint256 reserveDecimals, ) = reserve.configuration.getParamsMemory(); + bool isAssetCapped = totalSupplyAtoken.div(10**reserveDecimals) >= exposureCap; + require(isAssetCapped, Errors.VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED); + } + } } /** diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 62cd4de5..3c61c7af 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -42,7 +42,8 @@ library DataTypes { //bit 61-63: reserved //bit 64-79: reserve factor //bit 80-115 borrow cap, borrowCap == 0 => disabled - //bit 116-152 supply cap, supplyCap == 0 => disabled + //bit 116-151 supply cap, supplyCap == 0 => disabled + //bit 152-185 exposure cap, exposureCap == 0 => disabled uint256 data; } diff --git a/helpers/constants.ts b/helpers/constants.ts index f2568ee0..b25ce643 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -17,6 +17,7 @@ export const MAX_UINT_AMOUNT = '115792089237316195423570985008687907853269984665640564039457584007913129639935'; export const MAX_BORROW_CAP = '68719476735'; export const MAX_SUPPLY_CAP = '68719476735'; +export const MAX_EXPOSURE_CAP = '68719476735'; export const ONE_YEAR = '31536000'; export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; export const ONE_ADDRESS = '0x0000000000000000000000000000000000000001'; diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index 678125a1..4289e68a 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -253,10 +253,9 @@ export const getPairsTokenAggregator = ( const aggregatorAddressIndex = Object.keys(aggregatorsAddresses).findIndex( (value) => value === tokenSymbol ); - const [, aggregatorAddress] = (Object.entries(aggregatorsAddresses) as [ - string, - tEthereumAddress - ][])[aggregatorAddressIndex]; + const [, aggregatorAddress] = ( + Object.entries(aggregatorsAddresses) as [string, tEthereumAddress][] + )[aggregatorAddressIndex]; return [tokenAddress, aggregatorAddress]; } }) as [string, string][]; @@ -286,6 +285,7 @@ export const configureReservesByHelper = async ( reserveFactor: BigNumberish; borrowCap: BigNumberish; supplyCap: BigNumberish; + exposureCap: BigNumberish; stableBorrowingEnabled: boolean; borrowingEnabled: boolean; }[] = []; @@ -299,6 +299,7 @@ export const configureReservesByHelper = async ( reserveFactor, borrowCap, supplyCap, + exposureCap, stableBorrowRateEnabled, borrowingEnabled, }, @@ -335,6 +336,7 @@ export const configureReservesByHelper = async ( reserveFactor, borrowCap, supplyCap, + exposureCap, stableBorrowingEnabled: stableBorrowRateEnabled, borrowingEnabled: borrowingEnabled, }); diff --git a/helpers/types.ts b/helpers/types.ts index d80e51e4..cff3f951 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -187,6 +187,9 @@ export enum ProtocolErrors { RL_ATOKEN_SUPPLY_NOT_ZERO = '88', RL_STABLE_DEBT_NOT_ZERO = '89', RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', + LP_CALLER_NOT_EOA = '91', + RC_INVALID_EXPOSURE_CAP = '92', + VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED = '93', VL_SAME_BLOCK_BORROW_REPAY = '94', LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95', LPC_FLASHLOAN_PREMIUM_INVALID = '96', @@ -400,6 +403,7 @@ export interface IReserveCollateralParams { baseLTVAsCollateral: string; liquidationThreshold: string; liquidationBonus: string; + exposureCap: string; } export interface IMarketRates { borrowRate: string; diff --git a/markets/aave/reservesConfigs.ts b/markets/aave/reservesConfigs.ts index f44e4dc8..cd48a26c 100644 --- a/markets/aave/reservesConfigs.ts +++ b/markets/aave/reservesConfigs.ts @@ -24,6 +24,7 @@ export const strategyBUSD: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyDAI: IReserveParams = { @@ -38,6 +39,7 @@ export const strategyDAI: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategySUSD: IReserveParams = { @@ -52,6 +54,7 @@ export const strategySUSD: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyTUSD: IReserveParams = { @@ -66,6 +69,7 @@ export const strategyTUSD: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDC: IReserveParams = { @@ -80,6 +84,7 @@ export const strategyUSDC: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDT: IReserveParams = { @@ -94,6 +99,7 @@ export const strategyUSDT: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyAAVE: IReserveParams = { @@ -108,6 +114,7 @@ export const strategyAAVE: IReserveParams = { reserveFactor: '0', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyBAT: IReserveParams = { @@ -122,6 +129,7 @@ export const strategyBAT: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyENJ: IReserveParams = { @@ -136,6 +144,7 @@ export const strategyENJ: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWETH: IReserveParams = { @@ -150,6 +159,7 @@ export const strategyWETH: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyKNC: IReserveParams = { @@ -164,6 +174,7 @@ export const strategyKNC: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyLINK: IReserveParams = { @@ -178,6 +189,7 @@ export const strategyLINK: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyMANA: IReserveParams = { @@ -192,6 +204,7 @@ export const strategyMANA: IReserveParams = { reserveFactor: '3500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyMKR: IReserveParams = { @@ -206,6 +219,7 @@ export const strategyMKR: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyREN: IReserveParams = { @@ -220,6 +234,7 @@ export const strategyREN: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategySNX: IReserveParams = { @@ -234,6 +249,7 @@ export const strategySNX: IReserveParams = { reserveFactor: '3500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; // Invalid borrow rates in params currently, replaced with snx params @@ -249,6 +265,7 @@ export const strategyUNI: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWBTC: IReserveParams = { @@ -263,6 +280,7 @@ export const strategyWBTC: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyYFI: IReserveParams = { @@ -277,6 +295,7 @@ export const strategyYFI: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyZRX: IReserveParams = { @@ -291,6 +310,7 @@ export const strategyZRX: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyXSUSHI: IReserveParams = { @@ -305,4 +325,5 @@ export const strategyXSUSHI: IReserveParams = { reserveFactor: '3500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; diff --git a/markets/amm/reservesConfigs.ts b/markets/amm/reservesConfigs.ts index d59f9ad6..fcbf8280 100644 --- a/markets/amm/reservesConfigs.ts +++ b/markets/amm/reservesConfigs.ts @@ -13,6 +13,7 @@ export const strategyWETH: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWBTC: IReserveParams = { @@ -27,6 +28,7 @@ export const strategyWBTC: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyDAI: IReserveParams = { @@ -41,6 +43,7 @@ export const strategyDAI: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDC: IReserveParams = { @@ -55,6 +58,7 @@ export const strategyUSDC: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDT: IReserveParams = { @@ -69,6 +73,7 @@ export const strategyUSDT: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyDAIWETH: IReserveParams = { @@ -83,6 +88,7 @@ export const strategyDAIWETH: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWBTCWETH: IReserveParams = { @@ -97,6 +103,7 @@ export const strategyWBTCWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyAAVEWETH: IReserveParams = { @@ -111,6 +118,7 @@ export const strategyAAVEWETH: IReserveParams = { reserveFactor: '500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyBATWETH: IReserveParams = { @@ -125,6 +133,7 @@ export const strategyBATWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyDAIUSDC: IReserveParams = { @@ -139,6 +148,7 @@ export const strategyDAIUSDC: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyCRVWETH: IReserveParams = { @@ -153,6 +163,7 @@ export const strategyCRVWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyLINKWETH: IReserveParams = { @@ -167,6 +178,7 @@ export const strategyLINKWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyMKRWETH: IReserveParams = { @@ -181,6 +193,7 @@ export const strategyMKRWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyRENWETH: IReserveParams = { @@ -195,6 +208,7 @@ export const strategyRENWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategySNXWETH: IReserveParams = { @@ -209,6 +223,7 @@ export const strategySNXWETH: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUNIWETH: IReserveParams = { @@ -223,6 +238,7 @@ export const strategyUNIWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDCWETH: IReserveParams = { @@ -237,6 +253,7 @@ export const strategyUSDCWETH: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWBTCUSDC: IReserveParams = { @@ -251,6 +268,7 @@ export const strategyWBTCUSDC: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyYFIWETH: IReserveParams = { @@ -265,6 +283,7 @@ export const strategyYFIWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyBALWETH: IReserveParams = { @@ -279,4 +298,5 @@ export const strategyBALWETH: IReserveParams = { reserveFactor: '1500', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; diff --git a/markets/matic/reservesConfigs.ts b/markets/matic/reservesConfigs.ts index cbeba1a6..0c9da5af 100644 --- a/markets/matic/reservesConfigs.ts +++ b/markets/matic/reservesConfigs.ts @@ -22,6 +22,7 @@ export const strategyDAI: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDC: IReserveParams = { @@ -36,6 +37,7 @@ export const strategyUSDC: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyUSDT: IReserveParams = { @@ -50,6 +52,7 @@ export const strategyUSDT: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWETH: IReserveParams = { @@ -64,6 +67,7 @@ export const strategyWETH: IReserveParams = { reserveFactor: '1000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyWBTC: IReserveParams = { @@ -78,6 +82,7 @@ export const strategyWBTC: IReserveParams = { reserveFactor: '2000', borrowCap: '0', supplyCap: '0', + exposureCap: '0', }; export const strategyMATIC: IReserveParams = { @@ -91,6 +96,7 @@ export const strategyMATIC: IReserveParams = { aTokenImpl: eContractid.AToken, borrowCap: '0', supplyCap: '0', + exposureCap: '0', reserveFactor: '2000', }; @@ -105,5 +111,6 @@ export const strategyAAVE: IReserveParams = { aTokenImpl: eContractid.AToken, borrowCap: '0', supplyCap: '0', + exposureCap: '0', reserveFactor: '0', }; diff --git a/test-suites/test-aave/configurator.spec.ts b/test-suites/test-aave/configurator.spec.ts index ecbcb0b0..c3eb05d0 100644 --- a/test-suites/test-aave/configurator.spec.ts +++ b/test-suites/test-aave/configurator.spec.ts @@ -229,7 +229,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -244,6 +246,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Unpauses the ETH reserve by pool admin ', async () => { @@ -261,7 +264,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -276,6 +281,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Pauses the ETH reserve by emergency admin', async () => { const { configurator, weth, helpersContract, addressesProvider, users, emergencyAdmin } = @@ -292,7 +298,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -307,6 +315,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Unpauses the ETH reserve by emergency admin ', async () => { @@ -324,7 +333,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -339,6 +350,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Check the only admin or emergency admin can pauseReserve ', async () => { @@ -372,7 +384,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -387,6 +401,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Unfreezes the ETH reserve by Pool admin', async () => { @@ -404,7 +419,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -419,6 +436,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Freezes the ETH reserve by Risk Admin', async () => { const { configurator, weth, helpersContract, riskAdmin } = testEnv; @@ -434,7 +452,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -449,6 +469,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Unfreezes the ETH reserve by Risk admin', async () => { @@ -466,7 +487,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -481,6 +504,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Check the onlyRiskOrPoolAdmins on freezeReserve ', async () => { @@ -513,7 +537,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(false); @@ -528,6 +554,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Activates the ETH reserve for borrowing via pool admin', async () => { @@ -546,7 +573,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -561,6 +590,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); expect(variableBorrowIndex.toString()).to.be.equal(RAY); }); @@ -579,7 +609,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(false); @@ -594,6 +626,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Activates the ETH reserve for borrowing via risk admin', async () => { @@ -612,7 +645,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -627,6 +662,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); expect(variableBorrowIndex.toString()).to.be.equal(RAY); }); @@ -652,7 +688,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { it('Deactivates the ETH reserve as collateral via pool admin', async () => { const { configurator, helpersContract, weth } = testEnv; - await configurator.configureReserveAsCollateral(weth.address, 0, 0, 0); + await configurator.configureReserveAsCollateral(weth.address, 0, 0, 0, 0); const { decimals, @@ -665,7 +701,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -680,11 +718,12 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Activates the ETH reserve as collateral via pool admin', async () => { const { configurator, helpersContract, weth } = testEnv; - await configurator.configureReserveAsCollateral(weth.address, '8000', '8250', '10500'); + await configurator.configureReserveAsCollateral(weth.address, '8000', '8250', '10500', '0'); const { decimals, @@ -697,7 +736,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -712,12 +753,13 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Deactivates the ETH reserve as collateral via risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; await configurator .connect(riskAdmin.signer) - .configureReserveAsCollateral(weth.address, 0, 0, 0); + .configureReserveAsCollateral(weth.address, 0, 0, 0, 0); const { decimals, @@ -730,7 +772,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -745,13 +789,14 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Activates the ETH reserve as collateral via risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; await configurator .connect(riskAdmin.signer) - .configureReserveAsCollateral(weth.address, '8000', '8250', '10500'); + .configureReserveAsCollateral(weth.address, '8000', '8250', '10500', '0'); const { decimals, @@ -764,7 +809,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -779,6 +826,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Check the onlyRiskOrPoolAdmin on configureReserveAsCollateral ', async () => { @@ -786,7 +834,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { await expect( configurator .connect(emergencyAdmin.signer) - .configureReserveAsCollateral(weth.address, '7500', '8000', '10500'), + .configureReserveAsCollateral(weth.address, '7500', '8000', '10500', '0'), CALLER_NOT_POOL_ADMIN ).to.be.revertedWith(LPC_CALLER_NOT_RISK_OR_POOL_ADMIN); }); @@ -805,7 +853,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -820,6 +870,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Enables stable borrow rate on the ETH reserve via pool admin', async () => { @@ -836,7 +887,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -851,6 +904,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Disable stable borrow rate on the ETH reserve risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; @@ -866,7 +920,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -881,6 +937,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Enables stable borrow rate on the ETH reserve risk admin', async () => { @@ -897,7 +954,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -912,6 +971,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(strategyWETH.reserveFactor); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Check the onlyRiskOrPoolAdmin on disableReserveStableRate', async () => { @@ -952,6 +1012,13 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { CALLER_NOT_POOL_ADMIN ).to.be.revertedWith(LPC_CALLER_NOT_RISK_OR_POOL_ADMIN); }); + it('Check the onlyRiskOrPoolAdmin on setExposureCap', async () => { + const { configurator, users, weth, emergencyAdmin } = testEnv; + await expect( + configurator.connect(emergencyAdmin.signer).setExposureCap(weth.address, '3000000000'), + CALLER_NOT_POOL_ADMIN + ).to.be.revertedWith(LPC_CALLER_NOT_RISK_OR_POOL_ADMIN); + }); it('Changes the reserve factor of WETH via pool admin', async () => { const { configurator, helpersContract, weth } = testEnv; @@ -967,7 +1034,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -981,6 +1050,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); expect(reserveFactor).to.be.equal(1000); }); it('Changes the reserve factor of WETH risk admin', async () => { @@ -997,7 +1067,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -1011,6 +1083,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(stableBorrowRateEnabled).to.be.equal(strategyWETH.stableBorrowRateEnabled); expect(borrowCap).to.be.equal(strategyWETH.borrowCap); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); expect(reserveFactor).to.be.equal(1000); }); it('Changes the reserve factor of WETH risk admin', async () => { @@ -1073,7 +1146,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -1088,6 +1163,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(1000); expect(borrowCap).to.be.equal('3000000'); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Changes the borrow Cap of WETH risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; @@ -1103,7 +1179,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -1118,6 +1196,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(1000); expect(borrowCap).to.be.equal('3000000'); expect(supplyCap).to.be.equal(strategyWETH.supplyCap); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Changes the supply Cap of WETH via pool admin', async () => { @@ -1134,7 +1213,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -1149,6 +1230,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(1000); expect(borrowCap).to.be.equal('3000000'); expect(supplyCap).to.be.equal('3000000'); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); }); it('Changes the supply Cap of WETH via risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; @@ -1164,7 +1246,9 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { isActive, isFrozen, } = await helpersContract.getReserveConfigurationData(weth.address); - const { borrowCap, supplyCap } = await helpersContract.getReserveCaps(weth.address); + const { borrowCap, supplyCap, exposureCap } = await helpersContract.getReserveCaps( + weth.address + ); const isPaused = await helpersContract.getPaused(weth.address); expect(borrowingEnabled).to.be.equal(true); @@ -1179,6 +1263,73 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(1000); expect(borrowCap).to.be.equal('3000000'); expect(supplyCap).to.be.equal('3000000'); + expect(exposureCap).to.be.equal(strategyWETH.exposureCap); + }); + it('Changes the exposure Cap of WETH via pool admin', async () => { + const { configurator, helpersContract, weth } = testEnv; + await configurator.setExposureCap(weth.address, '3000000'); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap, exposureCap } = 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'); + expect(exposureCap).to.be.equal('3000000'); + }); + it('Changes the exposure Cap of WETH via risk admin', async () => { + const { configurator, helpersContract, weth, riskAdmin } = testEnv; + await configurator.connect(riskAdmin.signer).setExposureCap(weth.address, '3000000'); + const { + decimals, + ltv, + liquidationBonus, + liquidationThreshold, + reserveFactor, + stableBorrowRateEnabled, + borrowingEnabled, + isActive, + isFrozen, + } = await helpersContract.getReserveConfigurationData(weth.address); + const { borrowCap, supplyCap, exposureCap } = 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'); + expect(exposureCap).to.be.equal('3000000'); }); it('Changes the supply Cap of WETH via risk admin', async () => { const { configurator, helpersContract, weth, riskAdmin } = testEnv; diff --git a/test-suites/test-aave/exposure-cap.spec.ts b/test-suites/test-aave/exposure-cap.spec.ts new file mode 100644 index 00000000..387043b0 --- /dev/null +++ b/test-suites/test-aave/exposure-cap.spec.ts @@ -0,0 +1,276 @@ +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { + APPROVAL_AMOUNT_LENDING_POOL, + MAX_UINT_AMOUNT, + RAY, + MAX_EXPOSURE_CAP, + MOCK_CHAINLINK_AGGREGATORS_PRICES, +} from '../../helpers/constants'; +import { ProtocolErrors } from '../../helpers/types'; +import { MintableERC20, WETH9, WETH9Mocked } from '../../types'; +import { parseEther } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { strategyDAI } from '../../markets/amm/reservesConfigs'; +import { strategyUSDC } from '../../markets/amm/reservesConfigs'; +import { ethers } from 'ethers'; + +const { expect } = require('chai'); +makeSuite('Exposure Cap', (testEnv: TestEnv) => { + const { + VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED, + RC_INVALID_EXPOSURE_CAP, + VL_COLLATERAL_CANNOT_COVER_NEW_BORROW, + } = ProtocolErrors; + const daiPrice = Number(MOCK_CHAINLINK_AGGREGATORS_PRICES.DAI); + const usdcPrice = Number(MOCK_CHAINLINK_AGGREGATORS_PRICES.USDC); + const daiLTV = Number(strategyDAI.baseLTVAsCollateral); + const usdcLTV = Number(strategyUSDC.baseLTVAsCollateral); + + const unitParse = async (token: WETH9Mocked | MintableERC20, nb: string) => + BigNumber.from(nb).mul(BigNumber.from('10').pow((await token.decimals()) - 3)); + it('Reserves should initially have exposure cap disabled (exposureCap = 0)', async () => { + const { + weth, + pool, + dai, + usdc, + deployer, + helpersContract, + users: [user1], + } = testEnv; + + const mintedAmount = parseEther('1000000000'); + // minting for main user + await dai.mint(mintedAmount); + await weth.mint(mintedAmount); + await usdc.mint(mintedAmount); + // minting for lp user + await dai.connect(user1.signer).mint(mintedAmount); + await weth.connect(user1.signer).mint(mintedAmount); + await usdc.connect(user1.signer).mint(mintedAmount); + + await dai.approve(pool.address, MAX_UINT_AMOUNT); + await weth.approve(pool.address, MAX_UINT_AMOUNT); + await usdc.approve(pool.address, MAX_UINT_AMOUNT); + await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await weth.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + await usdc.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await pool.deposit(weth.address, mintedAmount, deployer.address, 0); + + let usdcExposureCap = (await helpersContract.getReserveCaps(usdc.address)).exposureCap; + let daiExposureCap = (await helpersContract.getReserveCaps(dai.address)).exposureCap; + + expect(usdcExposureCap).to.be.equal('0'); + expect(daiExposureCap).to.be.equal('0'); + }); + it('Deposit 10 Dai, 10 USDC, LTV for both should increase', async () => { + const { + pool, + dai, + usdc, + users: [user1], + } = testEnv; + + const suppliedAmount = 10; + const precisionSuppliedAmount = (suppliedAmount * 1000).toString(); + + // user 1 deposit more dai and usdc to be able to borrow + let { ltv } = await pool.getUserAccountData(user1.address); + expect(ltv.toString()).to.be.equal('0'); + await pool + .connect(user1.signer) + .deposit(dai.address, await unitParse(dai, precisionSuppliedAmount), user1.address, 0); + + ltv = (await pool.getUserAccountData(user1.address)).ltv; + expect(ltv).to.be.equal(daiLTV); + await pool + .connect(user1.signer) + .deposit(usdc.address, await unitParse(usdc, precisionSuppliedAmount), user1.address, 0); + + ltv = (await pool.getUserAccountData(user1.address)).ltv; + expect(Number(ltv)).to.be.equal( + Math.floor((daiLTV * daiPrice + usdcLTV * usdcPrice) / (daiPrice + usdcPrice)) + ); + }); + it('Sets the exposure cap for DAI to 10 Units', async () => { + const { + configurator, + dai, + helpersContract, + users: [], + } = testEnv; + + const newExposureCap = 10; + + await configurator.setExposureCap(dai.address, newExposureCap); + + const daiExposureCap = (await helpersContract.getReserveCaps(dai.address)).exposureCap; + + expect(daiExposureCap).to.be.equal(newExposureCap); + }); + it('should succeed to deposit 10 dai but dai ltv drops to 0', async () => { + const { + pool, + dai, + users: [user1], + } = testEnv; + const suppliedAmount = 10; + const precisionSuppliedAmount = (suppliedAmount * 1000).toString(); + + await pool + .connect(user1.signer) + .deposit(dai.address, await unitParse(dai, precisionSuppliedAmount), user1.address, 0); + + let ltv = (await pool.getUserAccountData(user1.address)).ltv; + expect(ltv).to.be.equal(Math.floor((usdcLTV * usdcPrice) / (usdcPrice + 2 * daiPrice))); + }); + it('should succeed to deposit 1 dai but avg ltv decreases', async () => { + const { + pool, + dai, + users: [user1], + } = testEnv; + const suppliedAmount = 1; + const precisionSuppliedAmount = (suppliedAmount * 1000).toString(); + let ltv = (await pool.getUserAccountData(user1.address)).ltv; + + await pool + .connect(user1.signer) + .deposit(dai.address, await unitParse(dai, precisionSuppliedAmount), user1.address, 0); + + expect(ltv.toNumber()).to.be.gt((await pool.getUserAccountData(user1.address)).ltv.toNumber()); + }); + it('should succeed to deposit 1 usdc and ltv should increase', async () => { + const { + usdc, + pool, + users: [user1], + } = testEnv; + const suppliedAmount = 1; + const precisionSuppliedAmount = (suppliedAmount * 1000).toString(); + let ltv = (await pool.getUserAccountData(user1.address)).ltv; + + await pool + .connect(user1.signer) + .deposit(usdc.address, await unitParse(usdc, precisionSuppliedAmount), user1.address, 0); + + expect(ltv.toNumber()).to.be.lt((await pool.getUserAccountData(user1.address)).ltv.toNumber()); + }); + it('Should not be able to borrow 15 USD of weth', async () => { + const { + pool, + weth, + users: [user1], + } = testEnv; + const precisionBorrowedUsdAmount = 15 * 1000; + const precisionBorrowedEthAmount = ethers.BigNumber.from(precisionBorrowedUsdAmount) + .mul(daiPrice) + .div(parseEther('1.0')) + .toString(); + const borrowedAmount = await unitParse(weth, precisionBorrowedEthAmount); + + await expect( + pool.connect(user1.signer).borrow(weth.address, borrowedAmount, 1, 0, user1.address) + ).to.be.revertedWith(VL_COLLATERAL_CANNOT_COVER_NEW_BORROW); + }); + it('should be able to borrow 15 USD of weth after dai exposure cap raised to 100', async () => { + const { + pool, + dai, + weth, + configurator, + helpersContract, + users: [user1], + } = testEnv; + + const newExposureCap = 100; + + await configurator.setExposureCap(dai.address, newExposureCap); + + const daiExposureCap = (await helpersContract.getReserveCaps(dai.address)).exposureCap; + + expect(daiExposureCap).to.be.equal(newExposureCap); + + const precisionBorrowedUsdAmount = 15 * 1000; + const precisionBorrowedEthAmount = ethers.BigNumber.from(precisionBorrowedUsdAmount) + .mul(daiPrice) + .div(parseEther('1.0')) + .toString(); + const borrowedAmount = await unitParse(weth, precisionBorrowedEthAmount); + + pool.connect(user1.signer).borrow(weth.address, borrowedAmount, 1, 0, user1.address); + }); + it('should not be able to withdraw 5 dai, transfer 5 aDai after cap decrease of usdc back to 10 (capped)', async () => { + const { + pool, + dai, + usdc, + aDai, + configurator, + helpersContract, + users: [user1, , , receiver], + } = testEnv; + + const newExposureCap = 10; + + await configurator.setExposureCap(usdc.address, newExposureCap); + + const usdcExposureCap = (await helpersContract.getReserveCaps(usdc.address)).exposureCap; + + expect(usdcExposureCap).to.be.equal(newExposureCap); + + const precisionWithdrawnAmount = (5 * 1000).toString(); + const withdrawnAmount = await unitParse(dai, precisionWithdrawnAmount); + + await expect( + pool.connect(user1.signer).withdraw(dai.address, withdrawnAmount, user1.address) + ).to.be.revertedWith(VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED); + await expect( + aDai.connect(user1.signer).transfer(receiver.address, withdrawnAmount) + ).to.be.revertedWith(VL_COLLATERAL_EXPOSURE_CAP_EXCEEDED); + }); + it('should be able to withdraw 5 usdc and transfer 5 aUsdc', async () => { + const { + usdc, + pool, + aUsdc, + users: [user1, , , receiver], + } = testEnv; + + const precisionWithdrawnAmount = (5 * 1000).toString(); + const withdrawnAmount = await unitParse(usdc, precisionWithdrawnAmount); + + await pool.connect(user1.signer).withdraw(usdc.address, withdrawnAmount, user1.address); + await aUsdc.connect(user1.signer).transfer(receiver.address, withdrawnAmount); + }); + it('should be able to withdraw 5 dai, transfer 5 aDai after repaying weth Debt', async () => { + const { + pool, + dai, + weth, + aDai, + users: [user1, , , receiver], + } = testEnv; + + const precisionWithdrawnAmount = (5 * 1000).toString(); + const withdrawnAmount = await unitParse(dai, precisionWithdrawnAmount); + await ( + await pool.connect(user1.signer).repay(weth.address, MAX_UINT_AMOUNT, 1, user1.address) + ).wait(); + + pool.connect(user1.signer).withdraw(dai.address, withdrawnAmount, user1.address); + aDai.connect(user1.signer).transfer(receiver.address, withdrawnAmount); + }); + it('Should fail to set the exposure cap for usdc and DAI to max cap + 1 Units', async () => { + const { configurator, usdc, dai } = testEnv; + const newCap = Number(MAX_EXPOSURE_CAP) + 1; + + await expect(configurator.setExposureCap(usdc.address, newCap)).to.be.revertedWith( + RC_INVALID_EXPOSURE_CAP + ); + await expect(configurator.setExposureCap(dai.address, newCap)).to.be.revertedWith( + RC_INVALID_EXPOSURE_CAP + ); + }); +});