diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 8e38650c..304fa3a6 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -87,7 +87,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage _addressesProvider = provider; _maxStableRateBorrowSizePercent = 2500; _flashLoanPremiumTotal = 9; - _maxNumberOfReserves = 128; + _maxNumberOfReserves = UserConfiguration._maxReserves; } /** @@ -713,7 +713,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } /** - * @dev Returns the fee on flash loans + * @dev Returns the fee on flash loans */ function FLASHLOAN_PREMIUM_TOTAL() public view returns (uint256) { return _flashLoanPremiumTotal; diff --git a/contracts/protocol/libraries/configuration/UserConfiguration.sol b/contracts/protocol/libraries/configuration/UserConfiguration.sol index 2994f05d..fed8ffda 100644 --- a/contracts/protocol/libraries/configuration/UserConfiguration.sol +++ b/contracts/protocol/libraries/configuration/UserConfiguration.sol @@ -13,6 +13,9 @@ library UserConfiguration { uint256 internal constant BORROWING_MASK = 0x5555555555555555555555555555555555555555555555555555555555555555; + uint256 internal constant _maxReserves = 256; + uint256 internal constant _indexCount = _maxReserves / 128 + ((_maxReserves % 128 > 0) ? 1 : 0); + /** * @dev Sets if the user is borrowing the reserve identified by reserveIndex * @param self The configuration object @@ -24,9 +27,11 @@ library UserConfiguration { uint256 reserveIndex, bool borrowing ) internal { - require(reserveIndex < 128, Errors.UL_INVALID_INDEX); - self.data = - (self.data & ~(1 << (reserveIndex * 2))) | + require(reserveIndex < _maxReserves, Errors.UL_INVALID_INDEX); + uint256 index = reserveIndex / 128; + reserveIndex = reserveIndex % 128; + self.data[index] = + (self.data[index] & ~(1 << (reserveIndex * 2))) | (uint256(borrowing ? 1 : 0) << (reserveIndex * 2)); } @@ -41,9 +46,11 @@ library UserConfiguration { uint256 reserveIndex, bool usingAsCollateral ) internal { - require(reserveIndex < 128, Errors.UL_INVALID_INDEX); - self.data = - (self.data & ~(1 << (reserveIndex * 2 + 1))) | + require(reserveIndex < _maxReserves, Errors.UL_INVALID_INDEX); + uint256 index = reserveIndex / 128; + reserveIndex = reserveIndex % 128; + self.data[index] = + (self.data[index] & ~(1 << (reserveIndex * 2 + 1))) | (uint256(usingAsCollateral ? 1 : 0) << (reserveIndex * 2 + 1)); } @@ -57,8 +64,10 @@ library UserConfiguration { DataTypes.UserConfigurationMap memory self, uint256 reserveIndex ) internal pure returns (bool) { - require(reserveIndex < 128, Errors.UL_INVALID_INDEX); - return (self.data >> (reserveIndex * 2)) & 3 != 0; + require(reserveIndex < _maxReserves, Errors.UL_INVALID_INDEX); + uint256 index = reserveIndex / 128; + reserveIndex = reserveIndex % 128; + return (self.data[index] >> (reserveIndex * 2)) & 3 != 0; } /** @@ -72,8 +81,10 @@ library UserConfiguration { pure returns (bool) { - require(reserveIndex < 128, Errors.UL_INVALID_INDEX); - return (self.data >> (reserveIndex * 2)) & 1 != 0; + require(reserveIndex < _maxReserves, Errors.UL_INVALID_INDEX); + uint256 index = reserveIndex / 128; + reserveIndex = reserveIndex % 128; + return (self.data[index] >> (reserveIndex * 2)) & 1 != 0; } /** @@ -87,8 +98,10 @@ library UserConfiguration { pure returns (bool) { - require(reserveIndex < 128, Errors.UL_INVALID_INDEX); - return (self.data >> (reserveIndex * 2 + 1)) & 1 != 0; + require(reserveIndex < _maxReserves, Errors.UL_INVALID_INDEX); + uint256 index = reserveIndex / 128; + reserveIndex = reserveIndex % 128; + return (self.data[index] >> (reserveIndex * 2 + 1)) & 1 != 0; } /** @@ -97,7 +110,12 @@ library UserConfiguration { * @return True if the user has been borrowing any reserve, false otherwise **/ function isBorrowingAny(DataTypes.UserConfigurationMap memory self) internal pure returns (bool) { - return self.data & BORROWING_MASK != 0; + for (uint8 i = 0; i < _indexCount; i++) { + if (self.data[i] & BORROWING_MASK != 0) { + return true; + } + } + return false; } /** @@ -106,6 +124,11 @@ library UserConfiguration { * @return True if the user has been borrowing any reserve, false otherwise **/ function isEmpty(DataTypes.UserConfigurationMap memory self) internal pure returns (bool) { - return self.data == 0; + for (uint8 i = 0; i < _indexCount; i++) { + if (self.data[i] != 0) { + return false; + } + } + return true; } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index a19e5efc..b3b76b15 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -42,7 +42,7 @@ library DataTypes { } struct UserConfigurationMap { - uint256 data; + uint256[2] data; // size is _maxReserves / 128 + ((_maxReserves % 128 > 0) ? 1 : 0), but need to be literal } enum InterestRateMode {NONE, STABLE, VARIABLE} diff --git a/test-suites/test-aave/many-asset.spec.ts b/test-suites/test-aave/many-asset.spec.ts new file mode 100644 index 00000000..3408e549 --- /dev/null +++ b/test-suites/test-aave/many-asset.spec.ts @@ -0,0 +1,164 @@ +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { + APPROVAL_AMOUNT_LENDING_POOL, + MAX_UINT_AMOUNT, + RAY, + ZERO_ADDRESS, +} from '../../helpers/constants'; +import { + convertToCurrencyDecimals, + getContractAddressWithJsonFallback, +} from '../../helpers/contracts-helpers'; +import { eContractid, ProtocolErrors, RateMode } from '../../helpers/types'; +import { strategyWETH } from '../../markets/aave/reservesConfigs'; +import { + deployATokenImplementations, + deployMintableERC20, + deployValidationLogic, +} from '../../helpers/contracts-deployments'; +import { getATokenExtraParams } from '../../helpers/init-helpers'; +import { ConfigNames } from '../../helpers/configuration'; +import { zeroAddress } from 'hardhat/node_modules/ethereumjs-util'; +import AaveConfig from '../../markets/aave'; +import { getATokensAndRatesHelper } from '../../helpers/contracts-getters'; +import { config } from 'process'; +import { parseEther } from '@ethersproject/units'; +import exp from 'constants'; + +const { expect } = require('chai'); + +makeSuite('Adding > 128 asset to many-asset configured pool', (testEnv: TestEnv) => { + const tokens = {}; + + before('setup', async () => { + const { pool, dai, configurator, deployer, oracle } = testEnv; + const atokenAndRatesDeployer = await getATokensAndRatesHelper(); + + const reservesCount = await pool.getReservesList(); + + const daiData = await pool.getReserveData(dai.address); + const poolName = ConfigNames.Aave; + + const { + ATokenNamePrefix, + StableDebtTokenNamePrefix, + VariableDebtTokenNamePrefix, + SymbolPrefix, + } = AaveConfig; + + // Create tokens + let tokenInitParams = []; + let reserveInitParams = []; + + const aTokenImplAddress = await getContractAddressWithJsonFallback( + eContractid.AToken, + poolName + ); + const stableDebtImplAddress = await getContractAddressWithJsonFallback( + eContractid.StableDebtToken, + poolName + ); + const variableDebtImplAddress = await getContractAddressWithJsonFallback( + eContractid.VariableDebtToken, + poolName + ); + + const daiPrice = await oracle.getAssetPrice(dai.address); + + // All assets will be initialized with the same price as Dai. + for (let i = reservesCount.length; i < 140; i++) { + const tokenName = `RealT-${i}`; + const token = await deployMintableERC20([tokenName, tokenName, '18']); + tokens[tokenName] = token; + + tokenInitParams.push({ + aTokenImpl: aTokenImplAddress, + stableDebtTokenImpl: stableDebtImplAddress, + variableDebtTokenImpl: variableDebtImplAddress, + underlyingAssetDecimals: 18, + interestRateStrategyAddress: daiData.interestRateStrategyAddress, + underlyingAsset: token.address, + treasury: ZERO_ADDRESS, + incentivesController: ZERO_ADDRESS, + underlyingAssetName: tokenName, + aTokenName: `${ATokenNamePrefix} ${tokenName}`, + aTokenSymbol: `a${SymbolPrefix}${tokenName}`, + variableDebtTokenName: `${VariableDebtTokenNamePrefix} ${SymbolPrefix}${tokenName}`, + variableDebtTokenSymbol: `variableDebt${SymbolPrefix}${tokenName}`, + stableDebtTokenName: `${StableDebtTokenNamePrefix} ${tokenName}`, + stableDebtTokenSymbol: `stableDebt${SymbolPrefix}${tokenName}`, + params: '0x10', + }); + + reserveInitParams.push({ + asset: token.address, + baseLTV: 5000, + liquidationThreshold: 8000, + liquidationBonus: 10500, + reserveFactor: 1000, + stableBorrowingEnabled: false, + borrowingEnabled: true, + }); + + if (tokenInitParams.length == 2 || i == 139) { + await configurator.batchInitReserve(tokenInitParams); + + for (let j = 0; j < reserveInitParams.length; j++) { + let params = reserveInitParams[j]; + await configurator + .connect(deployer.signer) + .configureReserveAsCollateral( + params.asset, + params.baseLTV, + params.liquidationThreshold, + params.liquidationBonus + ); + await configurator.enableBorrowingOnReserve(params.asset, false); + await oracle.setAssetPrice(params.asset, daiPrice); + } + tokenInitParams = []; + reserveInitParams = []; + } + } + }); + + it('Check that pool contains >128 assets', async () => { + const { pool } = testEnv; + const reservesCount = await pool.getReservesList(); + expect(reservesCount.length).to.be.gt(128); + }); + + it('Add collateral and borrow asset 130', async () => { + const { + users: [, user, funder], + weth, + pool, + } = testEnv; + + const realT130 = tokens[`RealT-${130}`]; + + const assetdata = await pool.getReserveData(realT130.address); + expect(assetdata.id).to.be.gt(128); + + await realT130.connect(funder.signer).mint(parseEther('100')); + await realT130.connect(funder.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(funder.signer) + .deposit(realT130.address, parseEther('100'), funder.address, 0); + + await weth.connect(user.signer).mint(parseEther('1')); + await weth.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool.connect(user.signer).deposit(weth.address, parseEther('1'), user.address, 0); + + const userDataBefore = await pool.getUserAccountData(user.address); + await pool + .connect(user.signer) + .borrow(realT130.address, parseEther('1'), RateMode.Variable, 0, user.address); + const userDataAfter = await pool.getUserAccountData(user.address); + + expect(userDataAfter.totalCollateralETH).to.be.eq(userDataBefore.totalCollateralETH); + expect(userDataBefore.totalDebtETH).to.be.eq(0); + expect(userDataAfter.totalDebtETH).to.be.gt(userDataBefore.totalDebtETH); + expect(userDataAfter.healthFactor).to.be.lt(userDataBefore.healthFactor); + }); +});