Core Contracts

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

Stale liquidity index in `LendingPool` leads to incorrect token balance calculations

Summary

The LendingPool::getNormalizedIncome() function returns a stale liquidity index that is used by the RToken contract to calculate user balances, leading to incorrect token balance calculations and potential loss of funds.

Vulnerability Details

The LendingPool::getNormalizedIncome() function returns the current liquidity index without updating it first:

function getNormalizedIncome() external view returns (uint256) {
return reserve.liquidityIndex;
}

This index is used by the RToken contract to calculate user balances and is supposed to represent the accumulated interest over time. However, the index is only updated when state-changing functions are called through ReserveLibrary::updateReserveInterests().

The liquidity index increases linearly with time according to ReserveLibrary::calculateLinearInterest():

function calculateLinearInterest(uint256 rate, uint256 timeDelta, uint256 lastIndex) internal pure returns (uint256) {
uint256 cumulatedInterest = rate * timeDelta;
cumulatedInterest = cumulatedInterest / SECONDS_PER_YEAR;
return WadRayMath.RAY + cumulatedInterest;
}

By returning a stale index, the RToken contract will use outdated values to calculate user balances, leading to incorrect token amounts being minted/burned/transferred.

Proof of Concept

Add the following test to LendingPool.test.js:

describe("Stale liquidity index", function () {
it("should show that stale liquidity index will harm the users", async function () {
// We already have user2 as depositor in beforeEach
//Provide collateral
await raacNFT.connect(user1).approve(lendingPool.target, 1);
await lendingPool.connect(user1).depositNFT(1);
// Create utilization of the funds, so we can accrue interest on the liquidity
await lendingPool.connect(user1).borrow(ethers.parseEther("100"));
// Advance time, so we can accrue interest on the liquidity
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
await ethers.provider.send("evm_mine");
// Show that the balance before is different than the balance after
const user2BalanceBeforeUpdate = await rToken.balanceOf(user2.address);
await lendingPool.updateState();
const user2BalanceAfterUpdate = await rToken.balanceOf(user2.address);
expect(user2BalanceAfterUpdate).to.be.gt(user2BalanceBeforeUpdate);
});
it("should transfer the yield together with the tokens, because of lack of correct index", async function () {
// We already have user2 as depositor in beforeEach
//Provide collateral
await raacNFT.connect(user1).approve(lendingPool.target, 1);
await lendingPool.connect(user1).depositNFT(1);
// Create utilization of the funds, so we can accrue interest on the liquidity
await lendingPool.connect(user1).borrow(ethers.parseEther("100"));
// Advance time, so we can accrue interest on the liquidity
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
await ethers.provider.send("evm_mine");
// Transfer 100 rTokens to user1
await rToken.connect(user2).transfer(user1.address, ethers.parseEther("100"));
const user1BalanceBeforeUpdate = await rToken.balanceOf(user1.address);
await lendingPool.updateState();
const user1BalanceAfterUpdate = await rToken.balanceOf(user1.address);
// We have transferred 100 rTokens to user1, but after the state update, we can see that we have sent more!
expect(user1BalanceAfterUpdate).to.be.gt(user1BalanceBeforeUpdate);
});
});

Impact

  • Incorrect calculation of user token balances in the RToken contract

  • Users receive incorrect amounts when transferring funds

  • Potential loss of yield for users

Recommendations

Update the index in getNormalizedIncome(), to use ReserveLibrary::getNormalizedIncome() as it has a correct implementation:

function getNormalizedIncome() external view returns (uint256) {
- return reserve.liquidityIndex;
+ return ReserveLibrary.getNormalizedIncome(reserve, rateData);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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.