diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 4441d4d4..bcebbde8 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -168,14 +168,11 @@ interface ILendingPool { ); /** - * @dev Emitted when the protocol treasury receives minted aTokens from the accrued interest. - * @param reserve the address of the reserve - * @param amountMinted the amount minted to the treasury - **/ - event MintedToTreasury( - address indexed reserve, - uint256 amountMinted - ); + * @dev Emitted when the protocol treasury receives minted aTokens from the accrued interest. + * @param reserve the address of the reserve + * @param amountMinted the amount minted to the treasury + **/ + event MintedToTreasury(address indexed reserve, uint256 amountMinted); /** * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. @@ -406,6 +403,8 @@ interface ILendingPool { address interestRateStrategyAddress ) external; + function dropReserve(address reserve) external; + function setReserveInterestRateStrategyAddress(address reserve, address rateStrategyAddress) external; diff --git a/contracts/interfaces/ILendingPoolConfigurator.sol b/contracts/interfaces/ILendingPoolConfigurator.sol index ee5cdf2f..312aebed 100644 --- a/contracts/interfaces/ILendingPoolConfigurator.sol +++ b/contracts/interfaces/ILendingPoolConfigurator.sol @@ -132,6 +132,12 @@ interface ILendingPoolConfigurator { **/ event ReserveUnpaused(address indexed asset); + /** + * @dev Emitted when a reserve is dropped + * @param asset The address of the underlying asset of the reserve + **/ + event ReserveDropped(address indexed asset); + /** * @dev Emitted when a reserve factor is updated * @param asset The address of the underlying asset of the reserve @@ -203,13 +209,13 @@ interface ILendingPoolConfigurator { address indexed implementation ); - /** + /** * @dev Emitted when a new borrower is authorized (fees = 0) * @param flashBorrower The address of the authorized borrower **/ event FlashBorrowerAuthorized(address indexed flashBorrower); - /** + /** * @dev Emitted when a borrower is unauthorized * @param flashBorrower The address of the unauthorized borrower **/ @@ -386,16 +392,22 @@ interface ILendingPoolConfigurator { * @param admin The address of the potential admin **/ function isRiskAdmin(address admin) external view returns (bool); - - /** + + /** * @dev Authorize a new borrower (fees are 0 for the authorized borrower) * @param flashBorrower The address of the authorized borrower **/ function authorizeFlashBorrower(address flashBorrower) external; - /** + /** * @dev Unauthorize a borrower * @param flashBorrower The address of the unauthorized borrower **/ function unauthorizeFlashBorrower(address flashBorrower) external; + + /** + * @dev Drops a reserve entirely + * @param asset the address of the reserve to drop + **/ + function dropReserve(address asset) external; } diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 479c9298..e49a53f1 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -545,11 +545,11 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage function mintToTreasury(address[] calldata reserves) public { for (uint256 i = 0; i < reserves.length; i++) { address reserveAddress = reserves[i]; - + DataTypes.ReserveData storage reserve = _reserves[reserveAddress]; // this cover both inactive reserves and invalid reserves since the flag will be 0 for both - if(!reserve.configuration.getActive()){ + if (!reserve.configuration.getActive()) { continue; } @@ -690,15 +690,29 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } /** - * @dev Returns the list of the initialized reserves + * @dev Returns the list of the initialized reserves, does not contain dropped reserves **/ function getReservesList() external view override returns (address[] memory) { - address[] memory _activeReserves = new address[](_reservesCount); + uint256 reserveListCount = _reservesCount; + uint256 droppedReservesCount = 0; + address[] memory reserves = new address[](reserveListCount); - for (uint256 i = 0; i < _reservesCount; i++) { - _activeReserves[i] = _reservesList[i]; + for (uint256 i = 0; i < reserveListCount; i++) { + if (_reservesList[i] != address(0)) { + reserves[i - droppedReservesCount] = _reservesList[i]; + } else { + droppedReservesCount++; + } } - return _activeReserves; + + if (droppedReservesCount == 0) return reserves; + + address[] memory undroppedReserves = new address[](reserveListCount - droppedReservesCount); + for (uint256 i = 0; i < reserveListCount - droppedReservesCount; i++) { + undroppedReserves[i] = reserves[i]; + } + + return undroppedReserves; } /** @@ -808,6 +822,17 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _addReserveToList(asset); } + /** + * @dev Drop a reserve + * - Only callable by the LendingPoolConfigurator contract + * @param asset The address of the underlying asset of the reserve + **/ + function dropReserve(address asset) external override onlyLendingPoolConfigurator { + ValidationLogic.validateDropReserve(_reserves[asset]); + _removeReserveFromList(asset); + delete _reserves[asset]; + } + /** * @dev Updates the address of the interest rate strategy contract * - Only callable by the LendingPoolConfigurator contract @@ -1084,7 +1109,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage return paybackAmount; } - function _addReserveToList(address asset) internal { + function _addReserveToList(address asset) internal returns (uint8) { uint256 reservesCount = _reservesCount; require(reservesCount < _maxNumberOfReserves, Errors.LP_NO_MORE_RESERVES_ALLOWED); @@ -1092,10 +1117,18 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage bool reserveAlreadyAdded = _reserves[asset].id != 0 || _reservesList[0] == asset; if (!reserveAlreadyAdded) { - _reserves[asset].id = uint8(reservesCount); - _reservesList[reservesCount] = asset; - - _reservesCount = reservesCount + 1; + for (uint8 i = 0; i <= reservesCount; i++) { + if (_reservesList[i] == address(0)) { + _reserves[asset].id = i; + _reservesList[i] = asset; + _reservesCount = reservesCount + 1; + return i; + } + } } } + + function _removeReserveFromList(address asset) internal { + _reservesList[_reserves[asset].id] = address(0); + } } diff --git a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol index 2730937f..29c725cf 100644 --- a/contracts/protocol/lendingpool/LendingPoolConfigurator.sol +++ b/contracts/protocol/lendingpool/LendingPoolConfigurator.sol @@ -159,6 +159,12 @@ contract LendingPoolConfigurator is VersionedInitializable, ILendingPoolConfigur ); } + /// @inheritdoc ILendingPoolConfigurator + function dropReserve(address asset) external override onlyPoolAdmin { + _pool.dropReserve(asset); + emit ReserveDropped(asset); + } + /// @inheritdoc ILendingPoolConfigurator function updateAToken(UpdateATokenInput calldata input) external override onlyPoolAdmin { ILendingPool cachedPool = _pool; diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 8a2e7653..4b0644f5 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -109,6 +109,9 @@ library Errors { string public constant LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN = '85'; string public constant VL_RESERVE_PAUSED = '86'; string public constant LPC_CALLER_NOT_RISK_OR_POOL_ADMIN = '87'; + string public constant RL_ATOKEN_SUPPLY_NOT_ZERO = '88'; + string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; + string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; enum CollateralManagerErrors { NO_ERROR, diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 381fe804..549328a8 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -93,6 +93,9 @@ library GenericLogic { } vars.currentReserveAddress = reserves[vars.i]; + + if (vars.currentReserveAddress == address(0)) continue; + DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve @@ -132,7 +135,7 @@ library GenericLogic { IERC20(currentReserve.stableDebtTokenAddress).balanceOf(user) ); vars.userDebtETH = vars.assetPrice.mul(vars.userDebt).div(vars.assetUnit); - vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); + vars.totalDebtInETH = vars.totalDebtInETH.add(vars.userDebtETH); } } diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index 2e9850c9..28e96647 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -320,7 +320,9 @@ library ReserveLogic { vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor); if (vars.amountToMint != 0) { - reserve.accruedToTreasury = reserve.accruedToTreasury.add(vars.amountToMint.rayDiv(newLiquidityIndex)); + reserve.accruedToTreasury = reserve.accruedToTreasury.add( + vars.amountToMint.rayDiv(newLiquidityIndex) + ); } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 40ef23be..e57f55fa 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -477,4 +477,20 @@ library ValidationLogic { function validateTransfer(DataTypes.ReserveData storage reserve) internal view { require(!reserve.configuration.getPaused(), Errors.VL_RESERVE_PAUSED); } + + /** + * @dev Validates a drop reserve action + * @param reserve The reserve object + **/ + function validateDropReserve(DataTypes.ReserveData storage reserve) external view { + require( + IERC20(reserve.stableDebtTokenAddress).totalSupply() == 0, + Errors.RL_STABLE_DEBT_NOT_ZERO + ); + require( + IERC20(reserve.variableDebtTokenAddress).totalSupply() == 0, + Errors.RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + require(IERC20(reserve.aTokenAddress).totalSupply() == 0, Errors.RL_ATOKEN_SUPPLY_NOT_ZERO); + } } diff --git a/helpers/types.ts b/helpers/types.ts index 0e0ac375..157f34ba 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -184,6 +184,9 @@ export enum ProtocolErrors { LPC_CALLER_NOT_EMERGENCY_OR_POOL_ADMIN = '85', VL_RESERVE_PAUSED = '86', LPC_CALLER_NOT_RISK_OR_POOL_ADMIN = '87', + RL_ATOKEN_SUPPLY_NOT_ZERO = '88', + RL_STABLE_DEBT_NOT_ZERO = '89', + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', // old diff --git a/test-suites/test-aave/__setup.spec.ts b/test-suites/test-aave/__setup.spec.ts index dd8ed168..2ef2f3a0 100644 --- a/test-suites/test-aave/__setup.spec.ts +++ b/test-suites/test-aave/__setup.spec.ts @@ -265,6 +265,7 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { ); await configureReservesByHelper(reservesParams, allReservesAddresses, testHelpers, admin); + lendingPoolConfiguratorProxy.dropReserve(mockTokens.KNC.address); const collateralManager = await deployLendingPoolCollateralManager(); await waitForTx( diff --git a/test-suites/test-aave/drop-reserve.spec.ts b/test-suites/test-aave/drop-reserve.spec.ts new file mode 100644 index 00000000..d2257f79 --- /dev/null +++ b/test-suites/test-aave/drop-reserve.spec.ts @@ -0,0 +1,104 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { ProtocolErrors, RateMode } from '../../helpers/types'; +import { APPROVAL_AMOUNT_LENDING_POOL, MAX_UINT_AMOUNT, oneEther } from '../../helpers/constants'; +import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; +import { BigNumber } from 'bignumber.js'; +import { MockFlashLoanReceiver } from '../../types/MockFlashLoanReceiver'; +import { getMockFlashLoanReceiver } from '../../helpers/contracts-getters'; +import { domainToUnicode } from 'url'; + +const { expect } = require('chai'); + +makeSuite('Drop Reserve', (testEnv: TestEnv) => { + let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver; + + const { RL_ATOKEN_SUPPLY_NOT_ZERO, RL_STABLE_DEBT_NOT_ZERO, RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO } = + ProtocolErrors; + + before(async () => { + _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); + }); + + it('User 1 deposits Dai, User 2 borrow Dai stable and variable, should fail to drop Dai reserve', async () => { + const { + deployer, + users: [user1], + pool, + dai, + aDai, + weth, + configurator, + } = testEnv; + + const depositedAmount = parseEther('1000'); + const borrowedAmount = parseEther('100'); + // setting reserve factor to 0 to ease tests, no aToken accrued in reserve + await configurator.setReserveFactor(dai.address, 0); + + await dai.mint(depositedAmount); + await dai.approve(pool.address, depositedAmount); + await dai.connect(user1.signer).mint(depositedAmount); + await dai.connect(user1.signer).approve(pool.address, depositedAmount); + + await weth.connect(user1.signer).mint(depositedAmount); + await weth.connect(user1.signer).approve(pool.address, depositedAmount); + + await pool.deposit(dai.address, depositedAmount, deployer.address, 0); + + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_ATOKEN_SUPPLY_NOT_ZERO + ); + + await pool.connect(user1.signer).deposit(weth.address, depositedAmount, user1.address, 0); + + await pool.connect(user1.signer).borrow(dai.address, borrowedAmount, 2, 0, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + await pool.connect(user1.signer).borrow(dai.address, borrowedAmount, 1, 0, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith(RL_STABLE_DEBT_NOT_ZERO); + }); + it('User 2 repays debts, drop Dai reserve should fail', async () => { + const { + deployer, + users: [user1], + pool, + dai, + weth, + configurator, + } = testEnv; + await pool.connect(user1.signer).repay(dai.address, MAX_UINT_AMOUNT, 1, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO + ); + + await pool.connect(user1.signer).repay(dai.address, MAX_UINT_AMOUNT, 2, user1.address); + await expect(configurator.dropReserve(dai.address)).to.be.revertedWith( + RL_ATOKEN_SUPPLY_NOT_ZERO + ); + }); + it('User 1 withdraw Dai, drop Dai reserve should succeed', async () => { + const { + deployer, + users: [user1], + pool, + dai, + aDai, + weth, + configurator, + helpersContract, + } = testEnv; + + await pool.withdraw(dai.address, MAX_UINT_AMOUNT, deployer.address); + await configurator.dropReserve(dai.address); + + const tokens = await pool.getReservesList(); + + expect(tokens.includes(dai.address)).to.be.false; + + const { isActive } = await helpersContract.getReserveConfigurationData(dai.address); + + expect(isActive).to.be.false; + }); +});