Core Contracts

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

Double Scaling Bug in `transfer()` and `transferFrom()` Causes Incorrect Token Transfers

Summary

An issue exists in the transfer() and transferFrom() functions of RToken.sol, where token amounts are incorrectly scaled twice before being transferred. This results in users transferring fewer tokens than intended, leading to unexpected dust balances, failed full transfers or incorrect amount transferred to the recipient, and inefficient fund movements. This bug directly violates user expectations and disrupts accurate balance accounting.

Vulnerability Details

The RToken contract integrates interest accrual through scaling, meaning balances and transfers should always adjust according to the reserve’s liquidityIndex. This is done via _update(), which already applies scaling to all operations.

RToken.sol#L301-L311

/**
* @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)
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}

This ensures that all mint, burn, and transfer operations correctly account for interest accrual, as already implied in the function NatSpec.

The transfer() and transferFrom functions each scales down via rayDiv() the amount before calling super.transfer(), which will eventually trigger _update(), applying the same scaling down again.

RToken.sol#L207-L226

/**
* @dev Overrides the ERC20 transfer function to use scaled amounts
* @param recipient The recipient address
* @param amount The amount to transfer (in underlying asset units)
*/
function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
/**
* @dev Overrides the ERC20 transferFrom function to use scaled amounts
* @param sender The sender address
* @param recipient The recipient address
* @param amount The amount to transfer (in underlying asset units)
*/
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}

The recipient receives less than the intended amount, and the sender is left with unexpected dust amounts. As a result,

  • This makes full transfers impossible without multiple transactions that's pretty much never ending.

  • It also makes sending exact amount to the recipient impossible.

The avid users could probably circumvent the issue by manually scaling up the intended recipient amount twice via rayMul() prior to inputting amount, but this pretty much breaks the intended functionality of the overriden transfer() and transferFrom().

Impact

  • Users end up transferring fewer tokens than intended.

  • Dust balances accumulate, preventing full balance transfers.

  • Repeated transactions are required to clean up balances.

  • Confusing user experience due to unexpected discrepancies.

  • Breaks compatibility with external integrations expecting precise token transfers.

Tools Used

Manual

Recommendations

Consider implementing the following refactoring:

RToken.sol#L207-L226

function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
- 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) {
- uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 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 about 2 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.