From ce4d51af500b35b2b06cb9521a05228423e98f55 Mon Sep 17 00:00:00 2001 From: eboado Date: Thu, 20 Oct 2022 12:22:51 +0200 Subject: [PATCH] Fix of 100% RF for #315 --- .../protocol/libraries/logic/ReserveLogic.sol | 33 +-- package.json | 1 + test/__setup.spec.ts | 1 + test/reserve-factor.spec.ts | 192 ++++++++++++++++++ 4 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 test/reserve-factor.spec.ts diff --git a/contracts/protocol/libraries/logic/ReserveLogic.sol b/contracts/protocol/libraries/logic/ReserveLogic.sol index 3b0884c26..eea9a5735 100644 --- a/contracts/protocol/libraries/logic/ReserveLogic.sol +++ b/contracts/protocol/libraries/logic/ReserveLogic.sol @@ -108,6 +108,12 @@ library ReserveLogic { * @param reserve the reserve object **/ function updateState(DataTypes.ReserveData storage reserve) internal { + // If time didn't pass since last stored timestamp, skip state update + //solium-disable-next-line + if (reserve.lastUpdateTimestamp == uint40(block.timestamp)) { + return; + } + uint256 scaledVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply(); uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex; @@ -343,7 +349,9 @@ library ReserveLogic { uint256 newLiquidityIndex = liquidityIndex; uint256 newVariableBorrowIndex = variableBorrowIndex; - //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 (currentLiquidityRate > 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp); @@ -351,19 +359,18 @@ library ReserveLogic { require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW); reserve.liquidityIndex = uint128(newLiquidityIndex); + } - //as the liquidity rate might come only from stable rate loans, we need to ensure - //that there is actual variable debt before accumulating - if (scaledVariableDebt != 0) { - uint256 cumulatedVariableBorrowInterest = - MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); - newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); - require( - newVariableBorrowIndex <= type(uint128).max, - Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW - ); - reserve.variableBorrowIndex = uint128(newVariableBorrowIndex); - } + // Variable borrow side only gets updated if there is any accrual of variable debt + if (scaledVariableDebt > 0) { + uint256 cumulatedVariableBorrowInterest = + MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); + newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); + require( + newVariableBorrowIndex <= type(uint128).max, + Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW + ); + reserve.variableBorrowIndex = uint128(newVariableBorrowIndex); } //solium-disable-next-line diff --git a/package.json b/package.json index 1f8d30434..9631e40a2 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "console:fork": "MAINNET_FORK=true hardhat console", "test": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test/*.spec.ts", "test-scenarios": "npx hardhat test test/__setup.spec.ts test/scenario.spec.ts", + "test-reserve-factor": "npm run compile && npx hardhat test test/__setup.spec.ts test/reserve-factor.spec.ts", "test-repay-with-collateral": "hardhat test test/__setup.spec.ts test/repay-with-collateral.spec.ts", "test-liquidate-with-collateral": "hardhat test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", "test-liquidate-underlying": "hardhat test test/__setup.spec.ts test/liquidation-underlying.spec.ts", diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 37ff9cfc2..de50732b3 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -165,6 +165,7 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { REN: mockTokens.REN.address, UNI: mockTokens.UNI.address, ENJ: mockTokens.ENJ.address, + xSUSHI: mockTokens.xSUSHI.address, USD: USD_ADDRESS, }, fallbackOracle diff --git a/test/reserve-factor.spec.ts b/test/reserve-factor.spec.ts new file mode 100644 index 000000000..e38e0a937 --- /dev/null +++ b/test/reserve-factor.spec.ts @@ -0,0 +1,192 @@ +import BigNumber from 'bignumber.js'; + +import { DRE, increaseTime } from '../helpers/misc-utils'; +import { APPROVAL_AMOUNT_LENDING_POOL, oneEther } from '../helpers/constants'; +import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { RateMode } from '../helpers/types'; +import { ConfigNames, getTreasuryAddress, loadPoolConfig } from '../helpers/configuration'; + +const chai = require('chai'); + +const { expect } = chai; + +// 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 { dai, weth, users, pool, oracle } = testEnv; + const depositor = users[0]; + const borrower = users[1]; + + // mints DAI to depositor + await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '2000')); + + // approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + // user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + + 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(await convertToCurrencyDecimals(weth.address, '1000')); + + // approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + 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, + new BigNumber(userGlobalData.availableBorrowsETH.toString()) + .div(daiPrice.toString()) + .multipliedBy(0.95) + .toFixed(0) + ); + + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, borrowingMode, '0', borrower.address); +}; + +makeSuite('LendingPool Reserve Factor 100%. Only variable borrowings', (testEnv) => { + before('Before LendingPool Reserve Factor accrual: set config', () => { + BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); + }); + + after('After LendingPool Reserve Factor accrual: reset config', () => { + BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); + }); + + it('Validates that variable borrow index accrue, liquidity index not, and the Collector receives aTokens after interest accrues', async () => { + const { configurator, dai, users, pool, aDai } = testEnv; + + await setupPositions(testEnv, RateMode.Variable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const depositor = users[0]; + + const collectorAddress = await getTreasuryAddress(loadPoolConfig(ConfigNames.Aave)); + + const collectorADAIBalanceBefore = await aDai.scaledBalanceOf(collectorAddress); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await increaseTime(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); + const collectorADAIBalanceAfter1 = await aDai.balanceOf(collectorAddress); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.gt(reserveDataBefore.variableBorrowIndex); + expect(collectorADAIBalanceAfter1).to.be.gt(collectorADAIBalanceBefore); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await increaseTime(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); + const collectorADAIBalanceAfter2 = await aDai.balanceOf(collectorAddress); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.gt(reserveDataAfter1.variableBorrowIndex); + expect(collectorADAIBalanceAfter2).to.be.gt(collectorADAIBalanceAfter1); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); +}); + +makeSuite('LendingPool Reserve Factor 100%. Only stable borrowings', (testEnv) => { + before('Before LendingPool Reserve Factor accrual: set config', () => { + BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); + }); + + after('After LendingPool Reserve Factor accrual: reset config', () => { + BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); + }); + + it('Validates that neither variable borrow index nor liquidity index increase, but the Collector receives aTokens after interest accrues', async () => { + const { configurator, dai, users, pool, aDai } = testEnv; + + await setupPositions(testEnv, RateMode.Stable); + + // Set the RF to 100% + await configurator.setReserveFactor(dai.address, '10000'); + + const depositor = users[0]; + + const collectorAddress = await getTreasuryAddress(loadPoolConfig(ConfigNames.Aave)); + + const collectorADAIBalanceBefore = await aDai.scaledBalanceOf(collectorAddress); + + const reserveDataBefore = await pool.getReserveData(dai.address); + + await increaseTime(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); + const collectorADAIBalanceAfter1 = await aDai.balanceOf(collectorAddress); + + expect(reserveDataAfter1.variableBorrowIndex).to.be.eq(reserveDataBefore.variableBorrowIndex); + expect(collectorADAIBalanceAfter1).to.be.gt(collectorADAIBalanceBefore); + expect(reserveDataAfter1.liquidityIndex).to.be.gt(reserveDataBefore.liquidityIndex); + + await increaseTime(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); + const collectorADAIBalanceAfter2 = await aDai.balanceOf(collectorAddress); + + expect(reserveDataAfter2.variableBorrowIndex).to.be.eq(reserveDataAfter1.variableBorrowIndex); + expect(collectorADAIBalanceAfter2).to.be.gt(collectorADAIBalanceAfter1); + expect(reserveDataAfter2.liquidityIndex).to.be.eq(reserveDataAfter1.liquidityIndex); + }); +});