Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

Stale Liquidity Index causes Incorrect rToken balance reflection

Summary

The protocol relies on a dynamically updating liquidity index to calculate user balances and accrued interest. However, if no user actions trigger a state update, the liquidity index remains stale, leading to incorrect balance calculations. This creates a scenario where users may believe they have not accrued interest when they actually have, potentially impacting their financial decisions and trust in the system.

Vulnerability Details

rToken::balanceOf contains the following code:

/**
* @notice Returns the scaled balance of the user
* @param account The address of the user
* @return The user's balance (scaled by the liquidity index)
*/
function balanceOf(
address account
) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return
scaledBalance.rayMul(
ILendingPool(_reservePool).getNormalizedIncome()
);
}

This function scales the normalized debt into the actual debt by multiplying it by the current liquidity index. LendingPool::getNormalizedIncome is what returns the liquidity index. See below:

/**
* @notice Gets the reserve's normalized income
* @return The normalized income (liquidity index)
*/
function getNormalizedIncome() external view returns (uint256) {
return reserve.liquidityIndex;
}

The combination of these 2 functions allow for dynamic balance updates of every user's rtokens which reflect the amount of interest gained by any user at any particular time. The issue is that the liquidity index returned by LendingPool::getNormalizedIncome can be stale. The liquidity index is updated in a time based format where interest accrues every period and this is updated in almost every function in the protocol including depositing, withdrawals, liquidations, etc. The state can also be upodated by any user by calling LendingPool::updateState. The issue lies when none of these actions have been called and a user decides to make an action based on their current rtoken balance, the balance reflected will not be accurate as no action to update the state has occured which leads users to believe that their position has not accumulated the required amount of interest.

Proof Of Code (POC)

This test was run in LendingPool.test.js in the "Borrow and Repay" describe block

it("if updateState function is not called, a user's balance is not correctly reflected due to reserve.liquidityIndex staleness", async function () {
//c for testing purposes
//c first borrow that updates liquidity index and interest rates as deposits dont update it
const depositAmount = ethers.parseEther("100");
await lendingPool.connect(user2).borrow(depositAmount);
await time.increase(365 * 24 * 60 * 60);
await ethers.provider.send("evm_mine");
//c user deposits tokens into lending pool to get rtokens
await lendingPool.connect(user1).deposit(depositAmount);
//c time passes and user1 wants to check their balance to see how much interest they have accrued
await time.increase(365 * 24 * 60 * 60);
await ethers.provider.send("evm_mine");
//c user1 checks their balance and finds no accrued interest as reserve.liquidityIndex is stale
const prestateupdateuser1RTokenBalance = await rToken.balanceOf(
user1.address
);
console.log(
"prestateupdateuser1RTokenBalance",
prestateupdateuser1RTokenBalance
);
await lendingPool.updateState();
const expecteduser1RTokenBalance = await rToken.balanceOf(user1.address);
console.log("expecteduser1RTokenBalance", expecteduser1RTokenBalance);
assert(prestateupdateuser1RTokenBalance < expecteduser1RTokenBalance);
});

Impact

Incorrect Balance Display: Users checking their balanceOf may believe they have not accrued interest due to the stale liquidity index.
Misleading Financial Decisions: Users might make incorrect decisions regarding withdrawals, additional deposits, or interest expectations.
Trust and Transparency Issues: Users may lose confidence in the protocol if their balance does not update accurately over time.

Tools Used

Manual Review, Hardhat

Recommendations

Auto-Update Liquidity Index on balanceOf Calls

Modify balanceOf to first update the liquidity index before returning a balance.

function balanceOf(address account) public view override returns (uint256) {
ILendingPool(_reservePool).updateState(); // Ensure state is updated
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

Force Periodic Updates

Introduce a heartbeat function that updates the liquidity index automatically at set intervals.
Example: A keeper bot calls updateState() every few blocks.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::getNormalizedIncome() and getNormalizedDebt() returns stale data without updating state first, causing RToken calculations to use outdated values

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!