Core Contracts

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

Incorrect Scaled Amount Calculation and Liquidity Index Usage in RToken Contract Causing Incorrect Token Balances

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/tokens/RToken.sol#L212-L215
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/tokens/RToken.sol#L223-L226
https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/tokens/RToken.sol#L307-L311

Summary

The RToken contract contains two issues related to the calculation of scaled amounts and the usage of the liquidity index:

  1. Double Calculation of Scaled Amounts: The _update function recalculates scaled amounts even though they are already calculated in functions like transfer and transferFrom. This leads to incorrect token balances and potential financial discrepancies.

  2. Incorrect Liquidity Index Usage in transferFrom: The transferFrom function uses the _liquidityIndex instead of the normalized income from the LendingPool. This results in incorrect scaling of amounts, as the _liquidityIndex is never updated since RToken::updateLiquidityIndex was not implemented in the LendingPool, it will always return its initialized value.


Vulnerability Details

Explanation

1. Double Calculation of Scaled Amounts

The _update function, which is called during token transfers, mints, and burns, recalculates the scaled amount using the normalized income from the LendingPool. However, functions like transfer and transferFrom already calculate the scaled amount before calling _update. This results in the scaled amount being calculated twice, leading to incorrect token balances.

2. Incorrect Liquidity Index Usage in transferFrom

The transferFrom function uses the _liquidityIndex to calculate the scaled amount instead of the normalized income from the LendingPool. This is problematic because:

  • The _liquidityIndex is not updated, leading to outdated values.

  • The normalized income from the LendingPool is the correct value to use for scaling, as it reflects the current state of the reserve pool.

Root Cause in the Contract Function

  1. Double Calculation of Scaled Amounts:

    • The _update function recalculates the scaled amount:

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()); //@audit-issue still calculating scaled amount
super._update(from, to, scaledAmount);
}
  • However, functions like transfer and transferFrom already calculate the scaled amount before calling _update:

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
  1. Incorrect Liquidity Index Usage in transferFrom:

    • The transferFrom function uses the _liquidityIndex instead of the normalized income:

function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(_liquidityIndex); //@audit-issue Why using liquidity index? _liquidityIndex will never update since updateLiquidityIndex was not called in the LendingPool which is the reserve pool
return super.transferFrom(sender, recipient, scaledAmount);
}

Proof of Concept

Scenario Example

  1. Double Calculation of Scaled Amounts:

    • A user transfers 100 tokens. The transfer function calculates the scaled amount as 90 tokens (assuming a normalized income of 1.1).

    • The _update function recalculates the scaled amount again, reducing it further to 81 tokens.

    • The recipient receives 81 tokens instead of the expected 90 tokens.

  2. Incorrect Liquidity Index Usage in transferFrom:

    • A user transfers 100 tokens using transferFrom. The function uses the outdated _liquidityIndex (which is, 1.0 since it was initialized as WadRayMath.RAY) instead of the normalized income (e.g., 1.1).

    • The scaled amount is calculated as 100 tokens instead of 90 tokens, leading to incorrect token balances.

Impact

  • Incorrect Token Balances: Double calculation of scaled amounts leads to incorrect token balances, causing financial discrepancies.

  • Outdated Scaling: Using the _liquidityIndex instead of the normalized income results in outdated scaling, leading to incorrect token transfers.


Tools Used

  • Manual Code Review: The vulnerabilities were identified through a manual review of the RToken contract.

  • Solidity: The smart contract language used to write the contract.


Recommendations.

Choose either of these 2 mitigations, depends on what the protocol prefers

  1. Remove Double Calculation of Scaled Amounts:

function _update(address from, address to, uint256 scaledAmount) internal override {
- uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
  1. Use Normalized Income in transferFrom:

    • Update the transferFrom function to use the normalized income from the LendingPool instead of the _liquidityIndex.

function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
+ uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transferFrom(sender, recipient, scaledAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::updateLiquidityIndex() has onlyReservePool modifier but LendingPool never calls it, causing transferFrom() to use stale liquidity index values

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::updateLiquidityIndex() has onlyReservePool modifier but LendingPool never calls it, causing transferFrom() to use stale liquidity index values

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!