diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index a946c3813..3ba3c12f5 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -286,7 +286,9 @@ library ReserveLogic { reserveCache.nextLiquidityIndex = reserveCache.currLiquidityIndex; reserveCache.nextVariableBorrowIndex = reserveCache.currVariableBorrowIndex; - //only cumulating if there is any income being produced + // Only cumulating on the supply side if there is any income being produced + // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0), + // as liquidity index should not be updated if (reserveCache.currLiquidityRate != 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( reserveCache.currLiquidityRate, @@ -296,19 +298,21 @@ library ReserveLogic { reserveCache.currLiquidityIndex ); reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); + } - //as the liquidity rate might come only from stable rate loans, we need to ensure - //that there is actual variable debt before accumulating - if (reserveCache.currScaledVariableDebt != 0) { - uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( - reserveCache.currVariableBorrowRate, - reserveCache.reserveLastUpdateTimestamp - ); - reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( - reserveCache.currVariableBorrowIndex - ); - reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); - } + // Variable borrow index only gets updated if there is any variable debt. + // reserveCache.currVariableBorrowRate != 0 is not a correct validation, + // because a positive base variable rate can be stored on + // reserveCache.currVariableBorrowRate, but the index should not increase + if (reserveCache.currScaledVariableDebt != 0) { + uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( + reserveCache.currVariableBorrowRate, + reserveCache.reserveLastUpdateTimestamp + ); + reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( + reserveCache.currVariableBorrowIndex + ); + reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); } //solium-disable-next-line @@ -342,8 +346,10 @@ library ReserveLogic { reserveCache.reserveLastUpdateTimestamp = reserve.lastUpdateTimestamp; reserveCache.currScaledVariableDebt = reserveCache.nextScaledVariableDebt = IVariableDebtToken( - reserveCache.variableDebtTokenAddress - ).scaledTotalSupply(); + reserveCache + .variableDebtTokenAddress + ) + .scaledTotalSupply(); ( reserveCache.currPrincipalStableDebt, diff --git a/test-suites/pool-edge.spec.ts b/test-suites/pool-edge.spec.ts index d2c18be5e..a85f5ac3f 100644 --- a/test-suites/pool-edge.spec.ts +++ b/test-suites/pool-edge.spec.ts @@ -1,14 +1,14 @@ -import { expect } from 'chai'; -import { BigNumber, BigNumberish, utils } from 'ethers'; -import { impersonateAccountsHardhat } from '../helpers/misc-utils'; -import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'; -import { deployMintableERC20 } from '@aave/deploy-v3/dist/helpers/contract-deployments'; -import { ProtocolErrors } from '../helpers/types'; -import { getFirstSigner } from '@aave/deploy-v3/dist/helpers/utilities/signer'; -import { topUpNonPayableWithEther } from './helpers/utils/funds'; -import { makeSuite, TestEnv } from './helpers/make-suite'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { evmSnapshot, evmRevert, getPoolLibraries } from '@aave/deploy-v3'; +import {expect} from 'chai'; +import {BigNumber, BigNumberish, utils} from 'ethers'; +import {impersonateAccountsHardhat} from '../helpers/misc-utils'; +import {MAX_UINT_AMOUNT, ZERO_ADDRESS} from '../helpers/constants'; +import {deployMintableERC20} from '@aave/deploy-v3/dist/helpers/contract-deployments'; +import {ProtocolErrors, RateMode} from '../helpers/types'; +import {getFirstSigner} from '@aave/deploy-v3/dist/helpers/utilities/signer'; +import {topUpNonPayableWithEther} from './helpers/utils/funds'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {HardhatRuntimeEnvironment} from 'hardhat/types'; +import {evmSnapshot, evmRevert, getPoolLibraries, advanceTimeAndBlock} from '@aave/deploy-v3'; import { MockPoolInherited__factory, MockReserveInterestRateStrategy__factory, @@ -19,10 +19,64 @@ import { InitializableImmutableAdminUpgradeabilityProxy, ERC20__factory, } from '../types'; -import { getProxyImplementation } from '../helpers/contracts-helpers'; +import {convertToCurrencyDecimals, getProxyImplementation} from '../helpers/contracts-helpers'; declare var hre: HardhatRuntimeEnvironment; +// Setup function to have 1 user with DAI deposits, and another user with WETH collateral +// and DAI borrowings at an indicated borrowing mode +const setupPositions = async (testEnv: TestEnv, borrowingMode: RateMode) => { + const { + pool, + dai, + weth, + oracle, + users: [depositor, borrower], + } = testEnv; + + // mints DAI to depositor + await dai + .connect(depositor.signer) + ['mint(uint256)'](await convertToCurrencyDecimals(dai.address, '20000')); + + // approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, MAX_UINT_AMOUNT); + + // user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '10000'); + + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); + // user 2 deposits 1 ETH + const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); + + // mints WETH to borrower + await weth + .connect(borrower.signer) + ['mint(uint256)'](await convertToCurrencyDecimals(weth.address, '1000')); + + // approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, MAX_UINT_AMOUNT); + + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); + + //user 2 borrows + + const userGlobalData = await pool.getUserAccountData(borrower.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const amountDAIToBorrow = await convertToCurrencyDecimals( + dai.address, + userGlobalData.availableBorrowsBase.div(daiPrice).mul(5000).div(10000).toString() + ); + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, borrowingMode, '0', borrower.address); +}; + makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { const { NO_MORE_RESERVES_ALLOWED, @@ -59,7 +113,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { dai, users: [user0], } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {deployer: deployerName} = await hre.getNamedAccounts(); // Deploy the mock Pool with a `dropReserve` skipping the checks const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('MockPoolInheritedDropper', { @@ -129,7 +183,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { addressesProvider, users: [deployer], } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {deployer: deployerName} = await hre.getNamedAccounts(); const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('Pool', { contract: 'Pool', @@ -155,7 +209,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Check initialization', async () => { - const { pool } = testEnv; + const {pool} = testEnv; expect(await pool.MAX_STABLE_RATE_BORROW_SIZE_PERCENT()).to.be.eq( MAX_STABLE_RATE_BORROW_SIZE_PERCENT @@ -164,7 +218,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to initialize a reserve as non PoolConfigurator (revert expected)', async () => { - const { pool, users, dai, helpersContract } = testEnv; + const {pool, users, dai, helpersContract} = testEnv; const config = await helpersContract.getReserveTokensAddresses(dai.address); @@ -249,7 +303,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Call `mintToTreasury()` on a pool with an inactive reserve', async () => { - const { pool, poolAdmin, dai, users, configurator } = testEnv; + const {pool, poolAdmin, dai, users, configurator} = testEnv; // Deactivate reserve expect(await configurator.connect(poolAdmin.signer).setReserveActive(dai.address, false)); @@ -259,7 +313,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to call `finalizeTransfer()` by a non-aToken address (revert expected)', async () => { - const { pool, dai, users } = testEnv; + const {pool, dai, users} = testEnv; await expect( pool @@ -269,7 +323,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to call `initReserve()` with an EOA as reserve (revert expected)', async () => { - const { pool, deployer, users, configurator } = testEnv; + const {pool, deployer, users, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -284,7 +338,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address', async () => { - const { pool, deployer, dai, configurator } = testEnv; + const {pool, deployer, dai, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -302,7 +356,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address for asset 0', async () => { - const { pool, deployer, dai, configurator } = testEnv; + const {pool, deployer, dai, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -315,7 +369,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('PoolConfigurator updates the ReserveInterestRateStrategy address for an unlisted asset (revert expected)', async () => { - const { pool, deployer, dai, configurator, users } = testEnv; + const {pool, deployer, dai, configurator, users} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -330,14 +384,14 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Activates the zero address reserve for borrowing via pool admin (expect revert)', async () => { - const { configurator } = testEnv; + const {configurator} = testEnv; await expect(configurator.setReserveBorrowing(ZERO_ADDRESS, true)).to.be.revertedWith( ZERO_ADDRESS_NOT_VALID ); }); it('Initialize an already initialized reserve. ReserveLogic `init` where aTokenAddress != ZERO_ADDRESS (revert expected)', async () => { - const { pool, dai, deployer, configurator } = testEnv; + const {pool, dai, deployer, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -363,7 +417,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { * `_addReserveToList()` is called from `initReserve`. However, in `initReserve` we run `init` before the `_addReserveToList()`, * and in `init` we are checking if `aTokenAddress == address(0)`, so to bypass that we need this odd init. */ - const { pool, dai, deployer, configurator } = testEnv; + const {pool, dai, deployer, configurator} = testEnv; // Impersonate PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -406,8 +460,8 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { it('Initialize reserves until max, then add one more (revert expected)', async () => { // Upgrade the Pool to update the maximum number of reserves - const { addressesProvider, poolAdmin, pool, dai, deployer, configurator } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {addressesProvider, poolAdmin, pool, dai, deployer, configurator} = testEnv; + const {deployer: deployerName} = await hre.getNamedAccounts(); // Impersonate the PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -475,7 +529,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { * 3. Init a new asset. * Intended behaviour new asset is inserted into one of the available spots in */ - const { configurator, pool, poolAdmin, addressesProvider } = testEnv; + const {configurator, pool, poolAdmin, addressesProvider} = testEnv; const reservesListBefore = await pool.connect(configurator.signer).getReservesList(); @@ -574,8 +628,8 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { */ // Upgrade the Pool to update the maximum number of reserves - const { addressesProvider, poolAdmin, pool, dai, deployer, configurator } = testEnv; - const { deployer: deployerName } = await hre.getNamedAccounts(); + const {addressesProvider, poolAdmin, pool, dai, deployer, configurator} = testEnv; + const {deployer: deployerName} = await hre.getNamedAccounts(); // Impersonate the PoolConfigurator await topUpNonPayableWithEther(deployer.signer, [configurator.address], utils.parseEther('1')); @@ -707,7 +761,7 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { }); it('Tries to initialize a reserve with an AToken, StableDebtToken, and VariableDebt each deployed with the wrong pool address (revert expected)', async () => { - const { pool, deployer, configurator, addressesProvider } = testEnv; + const {pool, deployer, configurator, addressesProvider} = testEnv; const NEW_POOL_IMPL_ARTIFACT = await hre.deployments.deploy('DummyPool', { contract: 'Pool', @@ -793,4 +847,110 @@ makeSuite('Pool: Edge cases', (testEnv: TestEnv) => { initInputParams[0].variableDebtTokenImpl = variableDebtTokenImp.address; expect(await configurator.initReserves(initInputParams)); }); + + it('LendingPool Reserve Factor 100%. Only variable borrowings. Validates that variable borrow index accrue, liquidity index not, and the Collector receives accruedToTreasury allocation after interest accrues', async () => { + const { + configurator, + pool, + aDai, + dai, + users: [depositor], + } = testEnv; + + await setupPositions(testEnv, RateMode.Variable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await advanceTimeAndBlock(10000); + + // Deposit to "settle" the liquidity index accrual from pre-RF increase to 100% + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter1 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.gt(reserveDataBefore.variableBorrowIndex); + expect(reserveDataAfter1.accruedToTreasury).to.be.gt(reserveDataBefore.accruedToTreasury); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await advanceTimeAndBlock(10000); + + // "Clean" update, that should not increase the liquidity index, only variable borrow + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter2 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.gt(reserveDataAfter1.variableBorrowIndex); + expect(reserveDataAfter2.accruedToTreasury).to.be.gt(reserveDataAfter1.accruedToTreasury); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); + + it('LendingPool Reserve Factor 100%. Only stable borrowings. Validates that neither variable borrow index nor liquidity index increase, but the Collector receives accruedToTreasury allocation after interest accrues', async () => { + const { + configurator, + pool, + aDai, + dai, + users: [depositor], + } = testEnv; + + await setupPositions(testEnv, RateMode.Stable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await advanceTimeAndBlock(10000); + + // Deposit to "settle" the liquidity index accrual from pre-RF increase to 100% + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter1 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.eq(reserveDataBefore.variableBorrowIndex); + expect(reserveDataAfter1.accruedToTreasury).to.be.gt(reserveDataBefore.accruedToTreasury); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await advanceTimeAndBlock(10000); + + // "Clean" update, that should not increase the liquidity index, only stable borrow + await pool + .connect(depositor.signer) + .deposit( + dai.address, + await convertToCurrencyDecimals(dai.address, '1'), + depositor.address, + '0' + ); + + const reserveDataAfter2 = await pool.getReserveData(dai.address); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.eq(reserveDataAfter1.variableBorrowIndex); + expect(reserveDataAfter2.accruedToTreasury).to.be.gt(reserveDataAfter1.accruedToTreasury); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); });