Core Contracts

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

Incorrect dust calculation in `RToken` due to double scaling

Summary

The RToken::calculateDustAmount() function incorrectly scales values when comparing the contract underlying asset balance with RToken total supply, leading to dust amounts being underreported or completely missed.

Vulnerability Details

In calculateDustAmount(), the underlying asset contract balance is incorrectly scaled down by the normalized income, while the total supply is incorrectly scaled up by the normalized income again after already being scaled in totalSupply():

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()); // Incorrectly scaled down by normalized income
// Calculate the total real obligations to the token holders
@> uint256 currentTotalSupply = totalSupply(); // Already scaled by normalized income
// Calculate the total real balance equivalent to the total supply
@> uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome()); // Incorrectly scaled up again by normalized income
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}

The double scaling results in:

  • contractBalance being smaller than it should be (divided by normalized income)

  • totalRealBalance being larger than it should be (multiplied twice by normalized income)

This causes the comparison to almost always return 0, even when dust exists.

Impact

The protocol fails to detect and handle dust amounts correctly, which:

  • Prevents the protocol from properly managing excess funds

  • Makes the transferAccruedDust function essentially non-functional

  • Could lead to temporarily stuck funds in the contract that should be recoverable as dust

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/unit/core/tokens/RToken.test.js file inside the describe("Dust handling", function() { block:

it("should correctly calculate dust amount", async function() {
// Increase index
const newIndex = RAY + ethers.parseUnits("0.1", 27);
const repaidAmount = ethers.parseEther("10");
// Mock yield earned by the interest repayment
await mockLendingPool.mockGetNormalizedIncome(newIndex);
await mockAsset.mint(rToken.target, repaidAmount);
const balanceUser1 = await rToken.balanceOf(user1.address);
const rTokenBalance = await mockAsset.balanceOf(rToken.target);
expect(rTokenBalance).to.equal(balanceUser1);
const dustAmount = ethers.parseEther("1");
// Mint extra amount to create dust
await mockAsset.mint(rToken.target, dustAmount);
// Contract has more assets than the total supply, so dust should be detected
const calculatedDust = await rToken.calculateDustAmount();
expect(calculatedDust).to.not.equal(dustAmount); // calculatedDust should be equal dustAmount but is not
});

Recommendations

Remove the incorrect scaling operations and compare the raw values directly:

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());
+ uint256 contractBalance = IERC20(_assetAddress).balanceOf(address(this));
// Calculate the total real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
// Calculate the total real balance equivalent to the total supply
- uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 totalRealBalance = currentTotalSupply;
// All balance, that is not tied to rToken are dust (can be donated or is the rest of exponential vs linear)
return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;
}
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.