Core Contracts

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

Incorrect Scaling in `RToken.calculateDustAmount()` Breaks Dust Recovery Mechanism

Summary

The calculateDustAmount() function in RToken incorrectly mixes scaled and unscaled values when calculating excess tokens (dust). which make the function transferAccruedDust of the lendingPool effectively broken

Vulnerability Details

The vulnerability lies in the calculateDustAmount() function's incorrect handling of scaled values in RToken. This function is designed to identify excess tokens (dust) by comparing the contract's actual token balance against what it should hold based on the total supply. However, the implementation fundamentally mishandles the scaling mechanics.

First, let's look at how totalSupply() is implemented in RToken:

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

This shows that totalSupply() already returns an unscaled value (real token amount) by multiplying the scaled balance by the liquidity index.

However, in calculateDustAmount(), the function incorrectly handles scaling:

function calculateDustAmount() public view returns (uint256) {
// Calculate the actual balance of the underlying asset held by this contract
uint256 contractBalance =
IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
// Calculate the total balance that should be in the contract
uint256 totalRealBalance = totalSupply().rayMul(_liquidityIndex);
// If there is more in the contract than there should be, return the excess
if (contractBalance > totalRealBalance) {
return IERC20(_assetAddress).balanceOf(address(this))
.sub(totalRealBalance.rayMul(_liquidityIndex));
}
return 0;
}

The function makes two critical errors:

  1. Unnecessary Normalization of Contract Balance:

    • Takes the real token balance and divides it by liquidity index: balanceOf(address(this)).rayDiv(getNormalizedIncome())

    • This incorrectly converts real tokens into a normalized (scaled) form

  2. Double Scaling of Total Supply:

    • Uses totalSupply() which already returns unscaled amount

    • Then multiplies it again by liquidity index: totalSupply().rayMul(_liquidityIndex)

    • This results in double-scaling the total supply

Example

  • Contract actual balance = 200 tokens

  • Total supply = 150 tokens (already in real token terms)

  • Liquidity index = 1.5 RAY

The function performs:

  1. contractBalance = 200/1.5 = 133.34 (incorrectly scaling down real balance)

  2. totalRealBalance = 150 * 1.5 = 225 (double scaling)

  3. Compares: 133.34 > 225 (false)

This results in the function reporting no dust, when in reality there is 50 tokens excees

Impact

  • The dust recovery mechanism in the protocol becomes non-functional

  • Excess tokens become permanently locked in the contract

  • Protocol owners cannot recover legitimate dust amounts

Tools Used

  • Manual Review

  • Foundry

Recommendations

Remove the double scaling by comparing values in the same scale. Compare raw balance with scaled total supply:

function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
- uint256 normalizedBalance = contractBalance.rayDiv(_liquidityIndex);
- uint256 totalRealBalance = totalSupply().rayMul(_liquidityIndex);
+ uint256 totalRealBalance = totalSupply();
- if (normalizedBalance > totalRealBalance) {
- return contractBalance.sub(totalRealBalance.rayMul(_liquidityIndex));
+ if (contractBalance > totalRealBalance) {
+ return contractBalance.sub(totalRealBalance);
}
return 0;
}
Updates

Lead Judging Commences

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