Core Contracts

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

Dust Calculation and Retrieval Failure Due to Incorrect Scaling and Buffer Misalignment

Summary

The calculateDustAmount() function in RToken.sol is designed to compute excess reserve tokens (dust) held beyond obligations and allow their transfer via transferAccruedDust(). However, due to incorrect calculations other than relying on totalSupply() being scaled twice, the function always returns zero, rendering the entire dust transfer mechanism non-functional.

This issue results in unclaimed excess funds accumulating indefinitely, preventing any dust from being transferred. While our proposed fix highlights the problem and revives the functionality, further refinements may be needed by the protocol team.

Vulnerability Details

** calculateDustAmount() **currently uses an incorrect logic to compare contractBalance (tokens held in RToken.sol) against totalRealBalance (expected obligations). However, totalSupply() is already scaled by rayMul(),
RToken.sol#L199-L205

/**
* @notice Returns the scaled total supply
* @return The total supply (scaled by the liquidity index)
*/
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return super.totalSupply().rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

but it is scaled again within calculateDustAmount(), leading to a miscalculation.

RToken.sol#L324-L325

// Calculate the total real balance equivalent to the total supply
uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());

Additionally, the liquidity buffer (typically 20% of totalLiquidity as frequently calibrated by LendingPool._rebalanceLiquidity() for desredBuffer) is ignored, preventing proper dust determination.

LendingPool.sol#L778-L790

uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
// Withdraw shortage from the Curve vault
_withdrawFromVault(shortage);
}

Consequently, the current check (that's already exacerbated by the aforementioned double scaling) always evaluates to 0 because contractBalance is typically just the buffer amount whereas totalRealBalance references total withdrawable underlying asset, making the dust amount non-existent.

RToken.sol#L327

return contractBalance <= totalRealBalance ? 0 : contractBalance - totalRealBalance;

Since calculateDustAmount() always returns zero, transferAccruedDust() in RToken.sol:

function transferAccruedDust(address recipient, uint256 amount) external onlyReservePool {
if (recipient == address(0)) revert InvalidAddress();
uint256 poolDustBalance = calculateDustAmount();
if(poolDustBalance == 0) revert NoDust();
// Cap the transfer amount to the actual dust balance
uint256 transferAmount = (amount < poolDustBalance) ? amount : poolDustBalance;
// Transfer the amount to the recipient
IERC20(_assetAddress).safeTransfer(recipient, transferAmount);
emit DustTransferred(recipient, transferAmount);
}

that's onlyReservePool is never functional.

function transferAccruedDust(address recipient, uint256 amount) external onlyOwner {
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
require(recipient != address(0), "LendingPool: Recipient cannot be zero address");
IRToken(reserve.reserveRTokenAddress).transferAccruedDust(recipient, amount);
}

Because the returned dust amount is always zero, any attempt to call transferAccruedDust() reverts with NoDust(). This effectively renders the dust transfer system useless, allowing any excess reserve tokens to accumulate permanently.

Impact

  • Unclaimed excess reserve tokens: The inability to transfer dust results in the protocol losing access to unaccounted excess reserves, potentially impacting protocol efficiency.

  • Dead and misleading code: The presence of a non-functional dust transfer mechanism creates confusion and wasted execution paths.

  • Failed dust retrieval attempts: Any owner-initiated attempts to claim dust will always revert, making transferAccruedDust() completely unusable.

Tools Used

Manual

Recommendations

Consider implementing the following refactoring (Note: It may not seem the correct fix because contractBalance is typically 20% of reserve.totalLiquidity whereas totalRealBalance is the sacled total supply):

+ import "../../../libraries/math/PercentageMath.sol";
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 real obligations to the token holders
uint256 currentTotalSupply = totalSupply();
- // Calculate the total real balance equivalent to the total supply
+ // Calculate the total real balance equivalent to the buffer amount of total supply
- uint256 totalRealBalance = currentTotalSupply.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
+ uint256 totalRealBalance = currentTotalSupply.percentMul(ILendingPool(_reservePool).liquidityBufferRatio());
// 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.