diff --git a/abis/LendPool.json b/abis/LendPool.json index 59a5012c..9f0188cc 100644 --- a/abis/LendPool.json +++ b/abis/LendPool.json @@ -859,6 +859,11 @@ "internalType": "uint256", "name": "maxTokenId", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxCollateralCap", + "type": "uint256" } ], "internalType": "struct DataTypes.NftData", @@ -1072,6 +1077,11 @@ "internalType": "uint8", "name": "id", "type": "uint8" + }, + { + "internalType": "uint256", + "name": "maxUtilizationRate", + "type": "uint256" } ], "internalType": "struct DataTypes.ReserveData", @@ -1380,6 +1390,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxCap", + "type": "uint256" + } + ], + "name": "setNftMaxCollateralCap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1470,6 +1498,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxUtilRate", + "type": "uint256" + } + ], + "name": "setReserveMaxUtilizationRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abis/LendPoolAddressesProvider.json b/abis/LendPoolAddressesProvider.json index 0bba4d43..d8552bd1 100644 --- a/abis/LendPoolAddressesProvider.json +++ b/abis/LendPoolAddressesProvider.json @@ -266,6 +266,19 @@ "name": "WalletBalanceProviderUpdated", "type": "event" }, + { + "inputs": [], + "name": "RISK_ADMIN", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/abis/LendPoolConfigurator.json b/abis/LendPoolConfigurator.json index 87701fe8..1b598962 100644 --- a/abis/LendPoolConfigurator.json +++ b/abis/LendPoolConfigurator.json @@ -233,6 +233,25 @@ "name": "NftInitialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxCap", + "type": "uint256" + } + ], + "name": "NftMaxCollateralCapChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -436,6 +455,25 @@ "name": "ReserveInterestRateChanged", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxUtilRate", + "type": "uint256" + } + ], + "name": "ReserveMaxUtilizationRateChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -449,6 +487,19 @@ "name": "ReserveUnfrozen", "type": "event" }, + { + "inputs": [], + "name": "RISK_ADMIN", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -543,9 +594,14 @@ "internalType": "uint256", "name": "maxTokenId", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxCollateralCap", + "type": "uint256" } ], - "internalType": "struct ILendPoolConfigurator.ConfigNftInput[]", + "internalType": "struct ConfigTypes.ConfigNftInput[]", "name": "inputs", "type": "tuple[]" } @@ -568,9 +624,14 @@ "internalType": "uint256", "name": "reserveFactor", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxUtilizationRate", + "type": "uint256" } ], - "internalType": "struct ILendPoolConfigurator.ConfigReserveInput[]", + "internalType": "struct ConfigTypes.ConfigReserveInput[]", "name": "inputs", "type": "tuple[]" } @@ -920,6 +981,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "maxCap", + "type": "uint256" + } + ], + "name": "setNftMaxCollateralCap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1046,6 +1125,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256", + "name": "maxUtilRate", + "type": "uint256" + } + ], + "name": "setReserveMaxUtilizationRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/interfaces/ILendPool.sol b/contracts/interfaces/ILendPool.sol index e6177c7e..6de2a041 100644 --- a/contracts/interfaces/ILendPool.sol +++ b/contracts/interfaces/ILendPool.sol @@ -464,6 +464,8 @@ interface ILendPool { function setReserveConfiguration(address asset, uint256 configuration) external; + function setReserveMaxUtilizationRate(address asset, uint256 maxUtilRate) external; + function setNftConfiguration(address asset, uint256 configuration) external; function setNftMaxSupplyAndTokenId( @@ -472,6 +474,8 @@ interface ILendPool { uint256 maxTokenId ) external; + function setNftMaxCollateralCap(address asset, uint256 maxCap) external; + function setMaxNumberOfReserves(uint256 val) external; function setMaxNumberOfNfts(uint256 val) external; diff --git a/contracts/interfaces/ILendPoolConfigurator.sol b/contracts/interfaces/ILendPoolConfigurator.sol index 46cb8e11..4fcd49c5 100644 --- a/contracts/interfaces/ILendPoolConfigurator.sol +++ b/contracts/interfaces/ILendPoolConfigurator.sol @@ -2,25 +2,6 @@ pragma solidity 0.8.4; interface ILendPoolConfigurator { - struct ConfigReserveInput { - address asset; - uint256 reserveFactor; - } - - struct ConfigNftInput { - address asset; - uint256 baseLTV; - uint256 liquidationThreshold; - uint256 liquidationBonus; - uint256 redeemDuration; - uint256 auctionDuration; - uint256 redeemFine; - uint256 redeemThreshold; - uint256 minBidFine; - uint256 maxSupply; - uint256 maxTokenId; - } - /** * @dev Emitted when a reserve is initialized. * @param asset The address of the underlying asset of the reserve @@ -92,6 +73,8 @@ interface ILendPoolConfigurator { **/ event ReserveInterestRateChanged(address indexed asset, address strategy); + event ReserveMaxUtilizationRateChanged(address indexed asset, uint256 maxUtilRate); + /** * @dev Emitted when a nft is initialized. * @param asset The address of the underlying asset of the nft @@ -152,6 +135,8 @@ interface ILendPoolConfigurator { event NftMaxSupplyAndTokenIdChanged(address indexed asset, uint256 maxSupply, uint256 maxTokenId); + event NftMaxCollateralCapChanged(address indexed asset, uint256 maxCap); + event LoanRepaidInterceptorApproval(address indexed interceptor, bool approved); event FlashLoanLockerApproval(address indexed locker, bool approved); diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 1ad403c3..9af68481 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -50,6 +50,7 @@ library Errors { string public constant VL_SPECIFIED_RESERVE_NOT_BORROWED_BY_USER = "318"; string public constant VL_HEALTH_FACTOR_HIGHER_THAN_LIQUIDATION_THRESHOLD = "319"; string public constant VL_PRICE_STALE = "320"; + string public constant VL_EXCEED_MAX_UTILIZATION_RATE = "321"; //lend pool errors string public constant LP_CALLER_NOT_LEND_POOL_CONFIGURATOR = "400"; // 'The caller of the function is not the lending pool configurator' @@ -73,6 +74,7 @@ library Errors { string public constant LP_NFT_SUPPLY_NUM_EXCEED_MAX_LIMIT = "418"; string public constant LP_CALLER_NOT_VALID_INTERCEPTOR = "419"; string public constant LP_CALLER_NOT_VALID_LOCKER = "420"; + string public constant LP_NFT_COLLATERAL_NUM_EXCEED_CAP_LIMIT = "421"; //lend pool loan errors string public constant LPL_INVALID_LOAN_STATE = "480"; diff --git a/contracts/libraries/logic/BorrowLogic.sol b/contracts/libraries/logic/BorrowLogic.sol index b2a02e18..86d64f1f 100644 --- a/contracts/libraries/logic/BorrowLogic.sol +++ b/contracts/libraries/logic/BorrowLogic.sol @@ -88,6 +88,7 @@ library BorrowLogic { address nftOracle; address loanAddress; uint256 totalSupply; + uint256 bnftTotalSupply; } /** @@ -183,6 +184,9 @@ library BorrowLogic { ); if (vars.loanId == 0) { + vars.bnftTotalSupply = IERC721EnumerableUpgradeable(nftData.bNftAddress).totalSupply(); + require(vars.bnftTotalSupply < nftData.maxCollateralCap, Errors.LP_NFT_COLLATERAL_NUM_EXCEED_CAP_LIMIT); + IERC721Upgradeable(params.nftAsset).safeTransferFrom(vars.initiator, address(this), params.nftTokenId); vars.loanId = ILendPoolLoan(vars.loanAddress).createLoan( diff --git a/contracts/libraries/logic/ConfiguratorLogic.sol b/contracts/libraries/logic/ConfiguratorLogic.sol index 32c49322..86bf5958 100644 --- a/contracts/libraries/logic/ConfiguratorLogic.sol +++ b/contracts/libraries/logic/ConfiguratorLogic.sol @@ -15,6 +15,7 @@ import {NftConfiguration} from "../../libraries/configuration/NftConfiguration.s import {DataTypes} from "../../libraries/types/DataTypes.sol"; import {ConfigTypes} from "../../libraries/types/ConfigTypes.sol"; import {Errors} from "../../libraries/helpers/Errors.sol"; +import {PercentageMath} from "../../libraries/math/PercentageMath.sol"; /** * @title ConfiguratorLogic library @@ -22,6 +23,7 @@ import {Errors} from "../../libraries/helpers/Errors.sol"; * @notice Implements the logic to configuration feature */ library ConfiguratorLogic { + using PercentageMath for uint256; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using NftConfiguration for DataTypes.NftConfigurationMap; @@ -39,6 +41,15 @@ library ConfiguratorLogic { address interestRateAddress ); + /** + * @dev Emitted when a reserve factor is updated + * @param asset The address of the underlying asset of the reserve + * @param factor The new reserve factor + **/ + event ReserveFactorChanged(address indexed asset, uint256 factor); + + event ReserveMaxUtilizationRateChanged(address indexed asset, uint256 maxUtilRate); + /** * @dev Emitted when a nft is initialized. * @param asset The address of the underlying asset of the nft @@ -46,6 +57,23 @@ library ConfiguratorLogic { **/ event NftInitialized(address indexed asset, address indexed bNft); + event NftConfigurationChanged( + address indexed asset, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus + ); + + event NftAuctionChanged(address indexed asset, uint256 redeemDuration, uint256 auctionDuration, uint256 redeemFine); + + event NftRedeemThresholdChanged(address indexed asset, uint256 redeemThreshold); + + event NftMinBidFineChanged(address indexed asset, uint256 minBidFine); + + event NftMaxSupplyAndTokenIdChanged(address indexed asset, uint256 maxSupply, uint256 maxTokenId); + + event NftMaxCollateralCapChanged(address indexed asset, uint256 maxCap); + /** * @dev Emitted when an bToken implementation is upgraded * @param asset The address of the underlying asset of the reserve @@ -111,6 +139,32 @@ library ConfiguratorLogic { ); } + function executeSetReserveMaxUtilizationRate( + ILendPool cachedPool, + address[] calldata assets, + uint256 maxUtilRate + ) external { + for (uint256 i = 0; i < assets.length; i++) { + cachedPool.setReserveMaxUtilizationRate(assets[i], maxUtilRate); + + emit ReserveMaxUtilizationRateChanged(assets[i], maxUtilRate); + } + } + + function executeBatchConfigReserve(ILendPool cachedPool, ConfigTypes.ConfigReserveInput[] calldata inputs) external { + for (uint256 i = 0; i < inputs.length; i++) { + DataTypes.ReserveConfigurationMap memory currentConfig = cachedPool.getReserveConfiguration(inputs[i].asset); + + currentConfig.setReserveFactor(inputs[i].reserveFactor); + + cachedPool.setReserveConfiguration(inputs[i].asset, currentConfig.data); + emit ReserveFactorChanged(inputs[i].asset, inputs[i].reserveFactor); + + cachedPool.setReserveMaxUtilizationRate(inputs[i].asset, inputs[i].maxUtilizationRate); + emit ReserveMaxUtilizationRateChanged(inputs[i].asset, inputs[i].maxUtilizationRate); + } + } + function executeInitNft( ILendPool pool_, IBNFTRegistry registry_, @@ -132,6 +186,72 @@ library ConfiguratorLogic { emit NftInitialized(input.underlyingAsset, bNftProxy); } + function executeBatchConfigNft(ILendPool cachedPool, ConfigTypes.ConfigNftInput[] calldata inputs) external { + for (uint256 i = 0; i < inputs.length; i++) { + DataTypes.NftConfigurationMap memory currentConfig = cachedPool.getNftConfiguration(inputs[i].asset); + + //validation of the parameters: the LTV can + //only be lower or equal than the liquidation threshold + //(otherwise a loan against the asset would cause instantaneous liquidation) + require(inputs[i].baseLTV <= inputs[i].liquidationThreshold, Errors.LPC_INVALID_CONFIGURATION); + + if (inputs[i].liquidationThreshold != 0) { + //liquidation bonus must be smaller than or equal 100.00% + require(inputs[i].liquidationBonus <= PercentageMath.PERCENTAGE_FACTOR, Errors.LPC_INVALID_CONFIGURATION); + } else { + require(inputs[i].liquidationBonus == 0, Errors.LPC_INVALID_CONFIGURATION); + } + + // collateral parameters + currentConfig.setLtv(inputs[i].baseLTV); + currentConfig.setLiquidationThreshold(inputs[i].liquidationThreshold); + currentConfig.setLiquidationBonus(inputs[i].liquidationBonus); + + // auction parameters + currentConfig.setRedeemDuration(inputs[i].redeemDuration); + currentConfig.setAuctionDuration(inputs[i].auctionDuration); + currentConfig.setRedeemFine(inputs[i].redeemFine); + currentConfig.setRedeemThreshold(inputs[i].redeemThreshold); + currentConfig.setMinBidFine(inputs[i].minBidFine); + + cachedPool.setNftConfiguration(inputs[i].asset, currentConfig.data); + + emit NftConfigurationChanged( + inputs[i].asset, + inputs[i].baseLTV, + inputs[i].liquidationThreshold, + inputs[i].liquidationBonus + ); + emit NftAuctionChanged( + inputs[i].asset, + inputs[i].redeemDuration, + inputs[i].auctionDuration, + inputs[i].redeemFine + ); + emit NftRedeemThresholdChanged(inputs[i].asset, inputs[i].redeemThreshold); + emit NftMinBidFineChanged(inputs[i].asset, inputs[i].minBidFine); + + // max limit + cachedPool.setNftMaxSupplyAndTokenId(inputs[i].asset, inputs[i].maxSupply, inputs[i].maxTokenId); + emit NftMaxSupplyAndTokenIdChanged(inputs[i].asset, inputs[i].maxSupply, inputs[i].maxTokenId); + + cachedPool.setNftMaxCollateralCap(inputs[i].asset, inputs[i].maxCollateralCap); + emit NftMaxCollateralCapChanged(inputs[i].asset, inputs[i].maxCollateralCap); + } + } + + function executeSetNftMaxCollateralCap( + ILendPool cachedPool, + address[] calldata assets, + uint256 maxCap + ) external { + for (uint256 i = 0; i < assets.length; i++) { + cachedPool.setNftMaxCollateralCap(assets[i], maxCap); + + emit NftMaxCollateralCapChanged(assets[i], maxCap); + } + } + function executeUpdateBToken(ILendPool cachedPool, ConfigTypes.UpdateBTokenInput calldata input) external { DataTypes.ReserveData memory reserveData = cachedPool.getReserveData(input.asset); diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index deb35104..30d5b5fd 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -11,6 +11,7 @@ import {Errors} from "../helpers/Errors.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {IInterestRate} from "../../interfaces/IInterestRate.sol"; import {ILendPoolLoan} from "../../interfaces/ILendPoolLoan.sol"; +import {IDebtToken} from "../../interfaces/IDebtToken.sol"; import {INFTOracleGetter} from "../../interfaces/INFTOracleGetter.sol"; import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; @@ -77,6 +78,8 @@ library ValidationLogic { bool isPriceStale; address loanReserveAsset; address loanBorrower; + uint256 totalDebt; + uint256 utilizationRate; } /** @@ -126,6 +129,15 @@ library ValidationLogic { vars.isPriceStale = INFTOracleGetter(nftOracle).isPriceStale(nftAsset); require(!vars.isPriceStale, Errors.VL_PRICE_STALE); + // check utilization rate + vars.availableLiquidity = IERC20Upgradeable(reserveAsset).balanceOf(reserveData.bTokenAddress); + vars.totalDebt = IDebtToken(reserveData.debtTokenAddress).scaledTotalSupply().rayMul( + reserveData.variableBorrowIndex + ); + vars.totalDebt += amount; + vars.utilizationRate = vars.totalDebt.rayDiv(vars.availableLiquidity + (vars.totalDebt)); + require(vars.utilizationRate <= reserveData.maxUtilizationRate, Errors.VL_EXCEED_MAX_UTILIZATION_RATE); + (vars.currentLtv, vars.currentLiquidationThreshold, ) = nftData.configuration.getCollateralParams(); (vars.userCollateralBalance, vars.userBorrowBalance, vars.healthFactor) = GenericLogic.calculateLoanData( diff --git a/contracts/libraries/types/ConfigTypes.sol b/contracts/libraries/types/ConfigTypes.sol index 7a2a444b..8da176ad 100644 --- a/contracts/libraries/types/ConfigTypes.sol +++ b/contracts/libraries/types/ConfigTypes.sol @@ -16,10 +16,31 @@ library ConfigTypes { string debtTokenSymbol; } + struct ConfigReserveInput { + address asset; + uint256 reserveFactor; + uint256 maxUtilizationRate; + } + struct InitNftInput { address underlyingAsset; } + struct ConfigNftInput { + address asset; + uint256 baseLTV; + uint256 liquidationThreshold; + uint256 liquidationBonus; + uint256 redeemDuration; + uint256 auctionDuration; + uint256 redeemFine; + uint256 redeemThreshold; + uint256 minBidFine; + uint256 maxSupply; + uint256 maxTokenId; + uint256 maxCollateralCap; + } + struct UpdateBTokenInput { address asset; address implementation; diff --git a/contracts/libraries/types/DataTypes.sol b/contracts/libraries/types/DataTypes.sol index 18b3cffe..87355779 100644 --- a/contracts/libraries/types/DataTypes.sol +++ b/contracts/libraries/types/DataTypes.sol @@ -21,6 +21,8 @@ library DataTypes { address interestRateAddress; //the id of the reserve. Represents the position in the list of the active reserves uint8 id; + // disable borrow when utilization rate exceeds max + uint256 maxUtilizationRate; } struct NftData { @@ -32,6 +34,7 @@ library DataTypes { uint8 id; uint256 maxSupply; uint256 maxTokenId; + uint256 maxCollateralCap; } struct ReserveConfigurationMap { diff --git a/contracts/protocol/LendPool.sol b/contracts/protocol/LendPool.sol index 90a036d5..1c385614 100644 --- a/contracts/protocol/LendPool.sol +++ b/contracts/protocol/LendPool.sol @@ -828,6 +828,10 @@ contract LendPool is _reserves[asset].interestRateAddress = rateAddress; } + function setReserveMaxUtilizationRate(address asset, uint256 maxUtilRate) external override onlyLendPoolConfigurator { + _reserves[asset].maxUtilizationRate = maxUtilRate; + } + /** * @dev Sets the configuration bitmap of the reserve as a whole * - Only callable by the LendPoolConfigurator contract @@ -857,6 +861,10 @@ contract LendPool is _nfts[asset].maxTokenId = maxTokenId; } + function setNftMaxCollateralCap(address asset, uint256 maxCap) external override onlyLendPoolConfigurator { + _nfts[asset].maxCollateralCap = maxCap; + } + function _addReserveToList(address asset) internal { uint256 reservesCount = _reservesCount; diff --git a/contracts/protocol/LendPoolConfigurator.sol b/contracts/protocol/LendPoolConfigurator.sol index 4db0dec2..a861ffb6 100644 --- a/contracts/protocol/LendPoolConfigurator.sol +++ b/contracts/protocol/LendPoolConfigurator.sol @@ -182,17 +182,14 @@ contract LendPoolConfigurator is Initializable, ILendPoolConfigurator { } } - function batchConfigReserve(ConfigReserveInput[] calldata inputs) external onlyPoolAdmin { + function batchConfigReserve(ConfigTypes.ConfigReserveInput[] calldata inputs) external onlyPoolAdmin { ILendPool cachedPool = _getLendPool(); - for (uint256 i = 0; i < inputs.length; i++) { - DataTypes.ReserveConfigurationMap memory currentConfig = cachedPool.getReserveConfiguration(inputs[i].asset); - - currentConfig.setReserveFactor(inputs[i].reserveFactor); - - cachedPool.setReserveConfiguration(inputs[i].asset, currentConfig.data); + ConfiguratorLogic.executeBatchConfigReserve(cachedPool, inputs); + } - emit ReserveFactorChanged(inputs[i].asset, inputs[i].reserveFactor); - } + function setReserveMaxUtilizationRate(address[] calldata assets, uint256 maxUtilRate) external onlyRiskOrPoolAdmin { + ILendPool cachedPool = _getLendPool(); + ConfiguratorLogic.executeSetReserveMaxUtilizationRate(cachedPool, assets, maxUtilRate); } function setActiveFlagOnNft(address[] calldata assets, bool flag) external onlyPoolAdmin { @@ -340,56 +337,14 @@ contract LendPoolConfigurator is Initializable, ILendPoolConfigurator { } } - function batchConfigNft(ConfigNftInput[] calldata inputs) external onlyPoolAdmin { + function batchConfigNft(ConfigTypes.ConfigNftInput[] calldata inputs) external onlyPoolAdmin { ILendPool cachedPool = _getLendPool(); - for (uint256 i = 0; i < inputs.length; i++) { - DataTypes.NftConfigurationMap memory currentConfig = cachedPool.getNftConfiguration(inputs[i].asset); - - //validation of the parameters: the LTV can - //only be lower or equal than the liquidation threshold - //(otherwise a loan against the asset would cause instantaneous liquidation) - require(inputs[i].baseLTV <= inputs[i].liquidationThreshold, Errors.LPC_INVALID_CONFIGURATION); - - if (inputs[i].liquidationThreshold != 0) { - //liquidation bonus must be smaller than or equal 100.00% - require(inputs[i].liquidationBonus <= PercentageMath.PERCENTAGE_FACTOR, Errors.LPC_INVALID_CONFIGURATION); - } else { - require(inputs[i].liquidationBonus == 0, Errors.LPC_INVALID_CONFIGURATION); - } + ConfiguratorLogic.executeBatchConfigNft(cachedPool, inputs); + } - // collateral parameters - currentConfig.setLtv(inputs[i].baseLTV); - currentConfig.setLiquidationThreshold(inputs[i].liquidationThreshold); - currentConfig.setLiquidationBonus(inputs[i].liquidationBonus); - - // auction parameters - currentConfig.setRedeemDuration(inputs[i].redeemDuration); - currentConfig.setAuctionDuration(inputs[i].auctionDuration); - currentConfig.setRedeemFine(inputs[i].redeemFine); - currentConfig.setRedeemThreshold(inputs[i].redeemThreshold); - currentConfig.setMinBidFine(inputs[i].minBidFine); - - cachedPool.setNftConfiguration(inputs[i].asset, currentConfig.data); - - emit NftConfigurationChanged( - inputs[i].asset, - inputs[i].baseLTV, - inputs[i].liquidationThreshold, - inputs[i].liquidationBonus - ); - emit NftAuctionChanged( - inputs[i].asset, - inputs[i].redeemDuration, - inputs[i].auctionDuration, - inputs[i].redeemFine - ); - emit NftRedeemThresholdChanged(inputs[i].asset, inputs[i].redeemThreshold); - emit NftMinBidFineChanged(inputs[i].asset, inputs[i].minBidFine); - - // max limit - cachedPool.setNftMaxSupplyAndTokenId(inputs[i].asset, inputs[i].maxSupply, inputs[i].maxTokenId); - emit NftMaxSupplyAndTokenIdChanged(inputs[i].asset, inputs[i].maxSupply, inputs[i].maxTokenId); - } + function setNftMaxCollateralCap(address[] calldata assets, uint256 maxCap) external onlyRiskOrPoolAdmin { + ILendPool cachedPool = _getLendPool(); + ConfiguratorLogic.executeSetNftMaxCollateralCap(cachedPool, assets, maxCap); } function setMaxNumberOfReserves(uint256 newVal) external onlyPoolAdmin { diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index e9260b7c..44b9fb7f 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -13,6 +13,8 @@ import { getContractAddressWithJsonFallback, rawInsertContractAddressInDb } from import { BigNumberish } from "ethers"; import { ConfigNames } from "./configuration"; import { deployRateStrategy } from "./contracts-deployments"; +import { oneRay, RAY } from "./constants"; +import BigNumber from "bignumber.js"; export const getBTokenExtraParams = async (bTokenName: string, tokenAddress: tEthereumAddress) => { //console.log(bTokenName); @@ -232,6 +234,7 @@ export const configureReservesByHelper = async ( const inputParams: { asset: string; reserveFactor: BigNumberish; + maxUtilizationRate: BigNumberish; }[] = []; const assetsParams: string[] = []; @@ -256,6 +259,7 @@ export const configureReservesByHelper = async ( inputParams.push({ asset: tokenAddress, reserveFactor: reserveFactor, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }); tokens.push(tokenAddress); @@ -304,6 +308,7 @@ export const configureNftsByHelper = async ( minBidFine: BigNumberish; maxSupply: BigNumberish; maxTokenId: BigNumberish; + maxCollateralCap: BigNumberish; }[] = []; console.log(`- Configure NFTs`); @@ -344,6 +349,7 @@ export const configureNftsByHelper = async ( minBidFine: minBidFine, maxSupply: maxSupply, maxTokenId: maxTokenId, + maxCollateralCap: 100, }); tokens.push(tokenAddress); diff --git a/tasks/helpers/add-nft-to-pool.ts b/tasks/helpers/add-nft-to-pool.ts index bd772b76..a09d7aca 100644 --- a/tasks/helpers/add-nft-to-pool.ts +++ b/tasks/helpers/add-nft-to-pool.ts @@ -91,6 +91,7 @@ task("helpers:add-nft-to-pool", "Add and config new nft asset to lend pool") minBidFine: BigNumberish; maxSupply: BigNumberish; maxTokenId: BigNumberish; + maxCollateralCap: BigNumberish; }[] = [ { asset: asset, @@ -104,6 +105,7 @@ task("helpers:add-nft-to-pool", "Add and config new nft asset to lend pool") minBidFine: nftParam.minBidFine, maxSupply: nftParam.maxSupply, maxTokenId: nftParam.maxTokenId, + maxCollateralCap: 100, }, ]; await waitForTx(await lendPoolConfiguratorProxy.connect(poolAdminSigner).batchConfigNft(cfgInputParams)); diff --git a/tasks/helpers/add-reserve-to-pool.ts b/tasks/helpers/add-reserve-to-pool.ts index 907dd9c4..6d8edd5e 100644 --- a/tasks/helpers/add-reserve-to-pool.ts +++ b/tasks/helpers/add-reserve-to-pool.ts @@ -1,7 +1,7 @@ import { BigNumberish } from "@ethersproject/bignumber"; import { task } from "hardhat/config"; import { ConfigNames, getProviderRegistryAddress, loadPoolConfig } from "../../helpers/configuration"; -import { ADDRESS_ID_PUNK_GATEWAY } from "../../helpers/constants"; +import { ADDRESS_ID_PUNK_GATEWAY, oneRay } from "../../helpers/constants"; import { deployInterestRate } from "../../helpers/contracts-deployments"; import { getBToken, @@ -16,6 +16,7 @@ import { getEthersSignerByAddress, getParamPerNetwork } from "../../helpers/cont import { getNowTimeInSeconds, notFalsyOrZeroAddress, waitForTx } from "../../helpers/misc-utils"; import { eContractid, eNetwork, IReserveParams } from "../../helpers/types"; import { strategyReserveParams } from "../../markets/bend/reservesConfigs"; +import BigNumber from "bignumber.js"; task("helpers:add-reserve-to-pool", "Add and config new reserve asset to lend pool") .addParam("pool", `Pool name to retrieve configuration, supported: ${Object.values(ConfigNames)}`) @@ -107,10 +108,12 @@ task("helpers:add-reserve-to-pool", "Add and config new reserve asset to lend po let cfgInputParams: { asset: string; reserveFactor: BigNumberish; + maxUtilizationRate: BigNumberish; }[] = [ { asset: asset, reserveFactor: reserveParam.reserveFactor, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }, ]; console.log("Reserve cfgInputParams:", cfgInputParams); diff --git a/tasks/helpers/encode-add-nft-to-pool.ts b/tasks/helpers/encode-add-nft-to-pool.ts index 3c33af15..cffec770 100644 --- a/tasks/helpers/encode-add-nft-to-pool.ts +++ b/tasks/helpers/encode-add-nft-to-pool.ts @@ -82,6 +82,7 @@ task("helpers:encode-add-nft-to-pool", "Init and config new nft asset to lend po minBidFine: BigNumberish; maxSupply: BigNumberish; maxTokenId: BigNumberish; + maxCollateralCap: BigNumberish; }[] = [ { asset: asset, @@ -95,6 +96,7 @@ task("helpers:encode-add-nft-to-pool", "Init and config new nft asset to lend po minBidFine: nftParam.minBidFine, maxSupply: nftParam.maxSupply, maxTokenId: nftParam.maxTokenId, + maxCollateralCap: 100, }, ]; const batchCfgNftEncodeData = lendPoolConfiguratorProxy.interface.encodeFunctionData("batchConfigNft", [ diff --git a/test/configurator-nft.spec.ts b/test/configurator-nft.spec.ts index 04a1ad96..27d74984 100644 --- a/test/configurator-nft.spec.ts +++ b/test/configurator-nft.spec.ts @@ -21,6 +21,7 @@ makeSuite("Configurator-NFT", (testEnv: TestEnv) => { minBidFine: BigNumberish; maxSupply: BigNumberish; maxTokenId: BigNumberish; + maxCollateralCap: BigNumberish; }[] = [ { asset: "", @@ -34,10 +35,12 @@ makeSuite("Configurator-NFT", (testEnv: TestEnv) => { minBidFine: 0, maxSupply: 10000, maxTokenId: 9999, + maxCollateralCap: 100, }, ]; - const { CALLER_NOT_POOL_ADMIN, LPC_INVALID_CONFIGURATION, LPC_NFT_LIQUIDITY_NOT_0 } = ProtocolErrors; + const { CALLER_NOT_POOL_ADMIN, CALLER_NOT_RISK_OR_POOL_ADMIN, LPC_INVALID_CONFIGURATION, LPC_NFT_LIQUIDITY_NOT_0 } = + ProtocolErrors; it("Deactivates the BAYC NFT", async () => { const { configurator, bayc, dataProvider } = testEnv; @@ -288,6 +291,21 @@ makeSuite("Configurator-NFT", (testEnv: TestEnv) => { ); }); + it("Config setNftMaxCollateralCap invalid value", async () => { + const { configurator, weth, pool } = testEnv; + await configurator.setNftMaxCollateralCap([weth.address], 512); + const { maxCollateralCap } = await pool.getNftData(weth.address); + expect(maxCollateralCap).to.be.equal(512); + }); + + it("Check the onlyPoolAdmin on setNftMaxCollateralCap ", async () => { + const { configurator, users, weth } = testEnv; + await expect( + configurator.connect(users[2].signer).setNftMaxCollateralCap([weth.address], 512), + CALLER_NOT_RISK_OR_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_RISK_OR_POOL_ADMIN); + }); + it("Config setMaxNumberOfNfts valid value", async () => { const { configurator, users, pool } = testEnv; await configurator.setMaxNumberOfNfts(512); diff --git a/test/configurator-reserve.spec.ts b/test/configurator-reserve.spec.ts index 7def2d9f..6784f80e 100644 --- a/test/configurator-reserve.spec.ts +++ b/test/configurator-reserve.spec.ts @@ -1,23 +1,30 @@ import { TestEnv, makeSuite } from "./helpers/make-suite"; -import { APPROVAL_AMOUNT_LENDING_POOL, RAY } from "../helpers/constants"; +import { APPROVAL_AMOUNT_LENDING_POOL, oneRay, RAY } from "../helpers/constants"; import { convertToCurrencyDecimals } from "../helpers/contracts-helpers"; import { ProtocolErrors } from "../helpers/types"; import { strategyWETH } from "../markets/bend/reservesConfigs"; import { BigNumberish } from "ethers"; +import BigNumber from "bignumber.js"; const { expect } = require("chai"); makeSuite("Configurator-Reserve", (testEnv: TestEnv) => { - const { CALLER_NOT_POOL_ADMIN, LPC_RESERVE_LIQUIDITY_NOT_0, LPC_INVALID_CONFIGURATION, RC_INVALID_RESERVE_FACTOR } = - ProtocolErrors; + const { + CALLER_NOT_POOL_ADMIN, + CALLER_NOT_RISK_OR_POOL_ADMIN, + LPC_RESERVE_LIQUIDITY_NOT_0, + LPC_INVALID_CONFIGURATION, + RC_INVALID_RESERVE_FACTOR, + } = ProtocolErrors; it("Reverts trying to set an invalid reserve factor", async () => { const { configurator, weth } = testEnv; - let inputs: { asset: string; reserveFactor: BigNumberish }[] = [ + let inputs: { asset: string; reserveFactor: BigNumberish; maxUtilizationRate: BigNumberish }[] = [ { asset: weth.address, reserveFactor: 65536, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }, ]; @@ -153,7 +160,7 @@ makeSuite("Configurator-Reserve", (testEnv: TestEnv) => { expect(reserveFactor).to.be.equal(1000); }); - it("Check the onlyLendPoolManager on setReserveFactor", async () => { + it("Check the onlyAdmin on setReserveFactor", async () => { const { configurator, users, weth } = testEnv; await expect( configurator.connect(users[2].signer).setReserveFactor([weth.address], "2000"), @@ -164,14 +171,16 @@ makeSuite("Configurator-Reserve", (testEnv: TestEnv) => { it("Batch Changes the reserve factor of WETH & DAI", async () => { const { configurator, dataProvider, weth, dai } = testEnv; - let inputs: { asset: string; reserveFactor: BigNumberish }[] = [ + let inputs: { asset: string; reserveFactor: BigNumberish; maxUtilizationRate: BigNumberish }[] = [ { asset: weth.address, reserveFactor: 1000, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }, { asset: dai.address, reserveFactor: 2000, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }, ]; @@ -193,10 +202,11 @@ makeSuite("Configurator-Reserve", (testEnv: TestEnv) => { it("Check the onlyPoolAdmin on batchConfigReserve", async () => { const { configurator, users, weth } = testEnv; - let inputs: { asset: string; reserveFactor: BigNumberish }[] = [ + let inputs: { asset: string; reserveFactor: BigNumberish; maxUtilizationRate: BigNumberish }[] = [ { asset: weth.address, reserveFactor: 2000, + maxUtilizationRate: new BigNumber(0.75).multipliedBy(oneRay).toFixed(), }, ]; @@ -246,4 +256,22 @@ makeSuite("Configurator-Reserve", (testEnv: TestEnv) => { CALLER_NOT_POOL_ADMIN ).to.be.revertedWith(CALLER_NOT_POOL_ADMIN); }); + + it("Changes the reserve MaxUtilizationRate of WETH", async () => { + const { configurator, pool, weth } = testEnv; + await configurator.setReserveMaxUtilizationRate([weth.address], new BigNumber(0.75).multipliedBy(oneRay).toFixed()); + const { maxUtilizationRate } = await pool.getReserveData(weth.address); + + expect(maxUtilizationRate).to.be.equal(new BigNumber(0.75).multipliedBy(oneRay).toFixed()); + }); + + it("Check the onlyPoolAdmin on MaxUtilizationRate ", async () => { + const { configurator, users, weth } = testEnv; + await expect( + configurator + .connect(users[2].signer) + .setReserveMaxUtilizationRate([weth.address], new BigNumber(0.75).multipliedBy(oneRay).toFixed()), + CALLER_NOT_RISK_OR_POOL_ADMIN + ).to.be.revertedWith(CALLER_NOT_RISK_OR_POOL_ADMIN); + }); }); diff --git a/test/foundry/ListingUSDTFork.t.sol b/test/foundry/ListingUSDTFork.t.sol index 64971e8a..8dc76c85 100644 --- a/test/foundry/ListingUSDTFork.t.sol +++ b/test/foundry/ListingUSDTFork.t.sol @@ -96,8 +96,12 @@ contract ListingUSDTForkTest is Test { vm.prank(timelockController7DAddress); configurator.batchInitReserve(initInputs); - ILendPoolConfigurator.ConfigReserveInput[] memory cfgInputs = new ILendPoolConfigurator.ConfigReserveInput[](1); - cfgInputs[0] = ILendPoolConfigurator.ConfigReserveInput({asset: usdtTokenAddress, reserveFactor: 3000}); + ConfigTypes.ConfigReserveInput[] memory cfgInputs = new ConfigTypes.ConfigReserveInput[](1); + cfgInputs[0] = ConfigTypes.ConfigReserveInput({ + asset: usdtTokenAddress, + reserveFactor: 3000, + maxUtilizationRate: 1e27 + }); vm.prank(timelockController7DAddress); configurator.batchConfigReserve(cfgInputs);