Usage and liquidity indices are supposed to be updated on every deposit and borrow but this fails to happen if there are multiple of these function calls in the same block. This results in the indices becoming stale.
The indices will be updated only on the first borrow/deposit/repay in the block, with each consecutive one working with stale data.
pragma solidity ^0.8.19;
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/libraries/pools/ReserveLibrary.sol";
import "../../lib/forge-std/src/Test.sol";
import "../../lib/forge-std/src/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../contracts/core/tokens/DebtToken.sol";
import "../../contracts/core/tokens/RToken.sol";
import "../../contracts/core/tokens/RAACNFT.sol";
import "../../contracts/core/primitives/RAACHousePrices.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
}
contract ReserveLibraryTest is Test {
address owner;
address user;
LendingPool lendingPool;
MockERC20 reserveAsset;
RToken rToken;
DebtToken debtToken;
RAACNFT raacNFt;
address priceOracle;
RAACHousePrices housePrices;
ReserveLibrary.ReserveData reserve;
ReserveLibrary.ReserveRateData rateData;
function setUp() public {
owner = makeAddr("owner");
initializeContracts(owner);
configureContracts(owner);
user = makeAddr("user");
}
function initializeContracts(address _owner) internal {
reserveAsset = new MockERC20("reserveMock", "mkTkn");
rToken = new RToken("rMock", "mkTkn", _owner, address(reserveAsset));
debtToken = new DebtToken("debtMock", "mkTkn", _owner);
priceOracle = makeAddr("priceOracle");
housePrices = new RAACHousePrices(_owner);
raacNFt = new RAACNFT(address(reserveAsset), address(housePrices), _owner);
lendingPool = new LendingPool(
address(reserveAsset), address(rToken), address(debtToken), address(raacNFt), address(housePrices), 1e26
);
}
function configureContracts(address _owner) internal {
vm.startPrank(_owner);
housePrices.setOracle(priceOracle);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.prank(priceOracle);
housePrices.setHousePrice(1, 10_000);
}
function updateReserveStates() public {
(
address reserveRTokenAddress,
address reserveAssetAddress,
address reserveDebtTokenAddress,
uint256 totalLiquidity,
uint256 totalUsage,
uint128 liquidityIndex,
uint128 usageIndex,
uint40 lastUpdateTimestamp
) = lendingPool.reserve();
reserve = ReserveLibrary.ReserveData(
reserveRTokenAddress,
reserveAssetAddress,
reserveDebtTokenAddress,
totalLiquidity,
totalUsage,
liquidityIndex,
usageIndex,
lastUpdateTimestamp
);
(
uint256 currentLiquidityRate,
uint256 currentUsageRate,
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 protocolFeeRate
) = lendingPool.rateData();
rateData = ReserveLibrary.ReserveRateData(
currentLiquidityRate,
currentUsageRate,
primeRate,
baseRate,
optimalRate,
maxRate,
optimalUtilizationRate,
protocolFeeRate
);
}
function testReserveIndicesNotUpdatedInSameBlock() public {
uint256 amount = 1e18;
address borrower = makeAddr("borrower");
updateReserveStates();
uint256 initialLiquidityIndex = reserve.liquidityIndex;
uint256 initialUsageIndex = reserve.usageIndex;
console.log("initial liquidity index: %d", initialLiquidityIndex);
console.log("initial usage index: %d", initialUsageIndex);
reserveAsset.mint(user, 20_000);
reserveAsset.mint(borrower, 10_000);
vm.startPrank(user);
deposit(10_000);
vm.stopPrank();
updateReserveStates();
uint256 liquidityIndexAfterFirstDeposit = reserve.liquidityIndex;
console.log("liquidity index after first deposit: %d", liquidityIndexAfterFirstDeposit);
assertEq(initialLiquidityIndex, liquidityIndexAfterFirstDeposit);
vm.startPrank(borrower);
mintNFT();
depositNFT();
lendingPool.borrow(1000);
vm.stopPrank();
updateReserveStates();
assertEq(initialUsageIndex, reserve.usageIndex);
vm.startPrank(user);
deposit(10_000);
vm.stopPrank();
updateReserveStates();
uint256 liquidityIndexAfterSecondDeposit = reserve.liquidityIndex;
console.log("liquidity index after second deposit: %d", liquidityIndexAfterSecondDeposit);
assertEq(liquidityIndexAfterFirstDeposit, liquidityIndexAfterSecondDeposit);
}
function mintNFT() internal {
reserveAsset.approve(address(raacNFt), 10_000);
raacNFt.mint(1, 10_000);
}
function depositNFT() internal {
raacNFt.approve(address(lendingPool), 1);
lendingPool.depositNFT(1);
}
function deposit(uint256 amount) internal {
reserveAsset.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
}
}
Consider creating a second method for updating the indices that doesn't perform the time delta check (forces update) and use it after any reserve state updating logic while keeping the current logic for refreshing indices before any reserve state changes.