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;
}
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;
}
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 () {
await raacNFT.connect(user1).approve(lendingPool.target, 1);
await lendingPool.connect(user1).depositNFT(1);
await lendingPool.connect(user1).borrow(ethers.parseEther("100"));
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
await ethers.provider.send("evm_mine");
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);
}