Core Contracts

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

Double Scaling in RToken's Dust Calculation Leads to Incorrect Dust Amount

Summary

In the RToken contract, the calculateDustAmount function incorrectly scales the total supply twice when calculating the total real balance, leading to an artificially inflated denominator and thus underreporting of dust amounts. This affects the protocol's ability to accurately track and manage excess tokens.

Vulnerability Details

The issue lies in the fundamental understanding of how RToken balances are scaled and how the total supply is calculated. Let's break this down:

The protocol uses a ray-based scaling system (similar to Aave) where:

  • Raw balances are scaled by the liquidity index to account for earned interest

  • The totalSupply() function is already overridden to return the scaled amount:

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

In calculateDustAmount():

function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress)
.balanceOf(address(this))
.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
uint256 currentTotalSupply = totalSupply(); // @audit Already scaled!
// @audit Here's the issue - scaling an already scaled value,
// currentTotalSupply is already the "total real balance", no need to multiply with index
uint256 totalRealBalance = currentTotalSupply.rayMul(
ILendingPool(_reservePool).getNormalizedIncome()
);
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

The problem occurs because:

  1. totalSupply() returns a value already scaled by the liquidity index

  2. The function then takes this scaled value and scales it again with rayMul(getNormalizedIncome())

  3. This results in the totalRealBalance being artificially large by a factor of the liquidity index

For example:

  • If raw total supply is 1000

  • Liquidity index is 1.1

  • The correct scaled total supply should be: 1000 * 1.1 = 1100

  • But the function calculates: (1000 * 1.1) * 1.1 = 1210

This means the dust amount from calculateDustAmount is wrong, which means when the reserve pool tries to transferAccruedDust the capped transferAmount is wrong;

function transferAccruedDust(
address recipient,
uint256 amount
) external onlyReservePool {
// ...
// @audit when amount >= poolDustBalance, then the transferAmount = poolDustBalance will be wrong.
uint256 transferAmount = (amount < poolDustBalance)
? amount
: poolDustBalance;
// Transfer the amount to the recipient
// @audit will revert, since transferAmount will be larger than the actual balance.
IERC20(_assetAddress).safeTransfer(recipient, transferAmount);

Impact

  • Inability to recover legitimate dust tokens

  • Incorrect reporting of the dust amounts in the RToken

Tools Used

Manual code review

Recommendations

Remove the second scaling operation in calculateDustAmount:

function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress)
.balanceOf(address(this))
.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
uint256 currentTotalSupply = totalSupply(); // @audit Already scaled
return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}
Updates

Lead Judging Commences

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

RToken::calculateDustAmount incorrectly applies liquidity index, severely under-reporting dust amounts and permanently trapping crvUSD in contract

Support

FAQs

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

Give us feedback!