Core Contracts

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

Incorrect double scaling in `RToken::transfer` leads to precision loss

Summary

The RToken::transfer and RToken::transferFrom function applies scaling when transferring tokens, but due to an additional scaling operation inside _update, the transfer amount is scaled down twice. This leads to severe precision errors, significantly reducing transferred values.

Vulnerability Details

Problem description

  • In RToken::transfer, the transferred amount is scaled down using ILendingPool(_reservePool).getNormalizedIncome().

  • Inside _update, which is called by super.transfer, the amount is scaled down again using the same getNormalizedIncome().

  • This results in unintended double scaling, compounding precision loss and reducing the actual transferred amount beyond what is expected.

Affected Code in RToken

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// @audit-issue scaling down by `ILendingPool(_reservePool).getNormalizedIncome()`
@> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// @audit-issue scaling down by `_liquidityIndex`
@> uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
/**
* @dev Internal function to handle token transfers, mints, and burns
* @param from The sender address
* @param to The recipient address
* @param amount The amount of tokens
*/
function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
@> // @audit-issue scaling down by `ILendingPool(_reservePool).getNormalizedIncome()` twice
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

Steps to reproduce

  1. Call transfer(recipient, amount) on RToken.

  2. The amount is first scaled down in transfer.

  3. The scaled amount is then passed into _update, which applies scaling again.

  4. The recipient receives significantly fewer tokens than expected due to compounded precision loss.

Impact

  • Severe precision loss: The transferred amount is much smaller than intended.

  • Token devaluation: Users may unknowingly lose value due to compounded scaling errors.

  • Broken token mechanics: The fundamental transfer logic is flawed, leading to unexpected and inconsistent token behavior.

Tools Used

Manual Review

Recommendations

Modify RToken::transfer and RToken::transferFrom to avoid scaling down before calling super.transfer:

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// @audit-issue scaling down by `ILendingPool(_reservePool).getNormalizedIncome()`
- uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
// @audit-issue scaling down by `_liquidityIndex`
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
Updates

Lead Judging Commences

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

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 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

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.