Core Contracts

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

LendingPool::getNormalizedIncome doesn't return an updated value; Users can transfer more rTokens than intended

Summary

LendingPool::getNormalizedIncome doesn't return the latest normalized income. Users can transfer more rTokens

Vulnerability Details

RToken is a rebasing token, meaning it increases in balance not in value. rToken balance is derived from the scaledBalance, which remains constant for a user unless they deposit, withdraw or transfer.

The key relationship is rToken balance = scaledBalance * liquidityIndex.
liquidityIndex is updated before any interaction with the rToken to get the correct ratio between balance and scaledBalance.

In RToken::_update, LendingPool::getNormalizedIncome is used to get the scaledAmount, the actual amount transferred.

// RToken.sol
function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

getNormalizedIncome returns the latest stored value for liquidityIndex:

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

The problem is the returned liquidityIndex can have an obsolete value. It's not the latest calculated value.
Consider the following example.

Let's consider 1 rToken = 1 asset (crvUsd) = 1 USD

  • alice deposits 1100 assets and get back 1100 rToken; liquidityIndex is 1.1

  • no more interactions with LendingPool take places for some time; liquidityIndex from storage is still 1.1

  • alice transfer 110 rTokens to Bob. RToken::_update gets called. scaledAmount is caluclated as: scaledAmount = amount / liquidityIndex <=> scaledAmount = 110 / 1.1 = 100;
    100 of scaledAmount is transferred to Bob;

  • bob observes that pool's data wasn't updated for a long time and calls LendignPool::updateState. liquidityIndex is updated to 1.11.

  • bob check his rToken balance and calls balanceOf(bob) which returns scaledBalance * liquidityIndex = 100 *1.11 = 111.
    Alice wanted to transfer 110 USD value but she actually transferred 110.

In interactions with the LendingPool, liquidityIndex is updated before it's used, but, since getNormalizedIncome doesn't return an updated value, users can give away tokens.

Same issue is present for getNormalizeDebt.

Impact

Users may loose tokens by transferring more than they wanted.

Tools Used

Recommendations

Use ReserveLibrary::getNormalizedIncome to return latest value for liquidityIndex:

function getNormalizedIncome() external view returns (uint256) {
- return reserve.liquidityIndex;
+ return ReserveLibrary.getNormalizedIncome(reserve, rateData);
}
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.