Core Contracts

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

Residual Dust Issue in Withdrawals Due to Index Scaling Discrepancy

Summary

Users attempting to withdraw their full balance of reserve tokens may fail to fully redeem all RTokens due to a scaling mismatch between the queried balance and the actual balance at burn time. This issue arises because the liquidity index is likely to increase between the time a user queries their balance and when the withdrawal is executed, resulting in a small residual dust amount that remains in their balance. This forces users to either manually overshoot their withdrawal request or repeatedly withdraw smaller amounts to eliminate dust, leading to poor user experience and inefficiencies.

Vulnerability Details

The issue occurs in LendingPool.withdraw(), which burns RTokens based on the user’s input amount:

LendingPool.sol#L251-L257

// Perform the withdrawal through ReserveLibrary
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve, // ReserveData storage
rateData, // ReserveRateData storage
amount, // Amount to withdraw
msg.sender // Recipient
);

Users typically query their balance using balanceOf() before initiating a withdrawal. However, balanceOf() scales up the user’s balance using the liquidity index at the time of querying, which may differ from the index used when burning RTokens:

RToken.sol#L189-L197

/**
* @notice Returns the scaled balance of the user
* @param account The address of the user
* @return The user's balance (scaled by the liquidity index)
*/
function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedIncome());
}

Later, in RToken.burn(), the contract ensures users cannot burn more than their updated balance:

RToken.sol#L164-L170

uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}

Since balanceOf(from) now uses an updated liquidity index, the user's originally queried balance (from an earlier index) is lower than the real-time balance, causing a mismatch. This results in a small unburned dust balance remaining in the user’s account.

Impact

Users Cannot Fully Withdraw Their Funds

  • Users attempting to withdraw their entire balance may always leave a small dust amount unless they manually adjust for scaling.
    Repeated Transactions Needed to Eliminate Dust

  • Users must manually repeat withdrawals or overshoot their input to clear their balance, causing inefficiencies.
    Degraded UX and Poor Protocol Design

  • Users unfamiliar with liquidity index scaling may assume the contract is malfunctioning when their full balance is not withdrawable.

Tools Used

Manual

Recommendations

With onlyValidAmount visibility in withdraw(), implementing the following function will be the best and cleanest fix:

function withdrawMax() external {
withdraw(type(uint256).max);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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