Core Contracts

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

Double Scaling in Dust Calculation Causes Loss of Protocol Funds

Here:

function calculateDustAmount() public view returns (uint256) {
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this)).rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
uint256 currentTotalSupply = totalSupply();
uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

Description:
The calculateDustAmount() function incorrectly scales values when calculating dust, leading to permanent loss of protocol funds. The issue occurs because:

  1. totalSupply() already returns a value scaled by normalized income:

function totalSupply() public view returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}
  1. Then calculateDustAmount() scales this value again with rayMul(getNormalizedIncome()), resulting in double scaling of the total supply.

Impact:
This double scaling causes:

  • Protocol funds to be permanently locked as "invisible" dust

  • transferAccruedDust() fails to detect and transfer actual dust

  • Loss of accumulated interest and donations

  • Incorrect accounting of excess funds

Proof of Concept:

// Given:
normalizedIncome = 1.1e27 (10% interest)
contractBalance = 1000e18
super.totalSupply() = 900e18
// Current incorrect calculation:
1. contractBalance after rayDiv:
1000e18 / 1.1e27 = 909.09e18
2. currentTotalSupply (first scaling):
900e18 * 1.1e27 / 1e27 = 990e18
3. totalRealBalance (second scaling):
990e18 * 1.1e27 / 1e27 = 1089e18
4. Dust check:
909.09e18 <= 1089e18 ? 0 : 909.09e18 - 1089e18
= 0 // Real dust of 10e18 is lost
// Actual dust should be:
Actual dust = 1000e18 - (900e18 * 1.1e27 / 1e27)
= 1000e18 - 990e18
= 10e18

Recommended Mitigation:

function calculateDustAmount() public view returns (uint256) {
// Get raw balance without scaling
uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// Get scaled total supply (already includes one normalized income multiplication)
uint256 currentTotalSupply = totalSupply();
// Compare directly without additional scaling
return contractBalance <= currentTotalSupply ? 0 : contractBalance - currentTotalSupply;
}
Updates

Lead Judging Commences

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