Core Contracts

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

Outdated usage index leads to incorrect debt calculations and failed liquidations

Summary

The LendingPool::getNormalizedDebt() function returns an outdated usage index that is used for critical debt calculations, leading to incorrect debt amounts and potential failed liquidations.

Vulnerability Details

The LendingPool contract exposes a function getNormalizedDebt() that returns the current usage index without updating it first. This index is used to calculate user debt amounts and is critical for liquidation checks.

The usage index increases with each second as shown in ReserveLibrary::updateReserveInterests():

function updateReserveInterests(ReserveData storage reserve, ReserveRateData storage rateData) internal {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
// Update usage index using compounded interest
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
reserve.lastUpdateTimestamp = uint40(block.timestamp);
}

However, the view function simply returns the stored value:

function getNormalizedDebt() external view returns (uint256) {
return reserve.usageIndex; // @audit This will be incorrect if we haven't synched the reserves
}

This means any external contracts or users relying on this function will get outdated debt calculations.

Proof of Concept

Add the following test to LendingPool.test.js:

describe("Stale normalized debt", function () {
it("should show that stale normalized debt 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 user1BalanceBeforeUpdate = await debtToken.balanceOf(user1.address);
console.log(user1BalanceBeforeUpdate);
await lendingPool.updateState();
const user1BalanceAfterUpdate = await debtToken.balanceOf(user1.address);
console.log(user1BalanceAfterUpdate);
expect(user1BalanceAfterUpdate).to.be.gt(user1BalanceBeforeUpdate);
});
});

Impact

  • Incorrect debt calculations for users

  • Failed liquidations due to wrong user debt calculations in StabilityPool

  • Incorrect accounting in external contracts integrating with the lending pool

Recommendations

Use the ReserveLibrary::getNormalizedDebt() function, which has a correct implementation:

function getNormalizedDebt() external view returns (uint256) {
- return reserve.usageIndex;
+ return ReserveLibrary.getNormalizedDebt(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.