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 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!