Core Contracts

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

RToken Burning Scaling Error Causes Progressive Withdrawal due to Interest Accrual

Summary

A critical scaling error in RToken.burn function prevents users from withdrawing their full deposited amounts. The vulnerability stems from comparing non-scaled withdrawal values against scaled token balances, creating an artificial cap that grows more restrictive as interest accrues. This forces liquidity providers to execute multiple transactions to recover diminishing fractions of their funds, effectively trapping capital in the protocol over time. The flaw fundamentally breaks the core withdrawal guarantee of the lending pool system, representing a high-severity economic vulnerability.

Vulnerability Details

The vulnerability arises from incorrect balance scaling handling in the RToken.burn function (RToken.sol#L164-L170). The function improperly compares a non-scaled withdrawal amount against the user's scaled token balance, leading to artificial capping of valid withdrawals.

contract RToken is ERC20, ERC20Permit, IRToken, Ownable {
function burn(
address from,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256) {
if (amount == 0) {
return (0, totalSupply(), 0);
}
@> uint256 userBalance = balanceOf(from);
_userState[from].index = index.toUint128();
@> if(amount > userBalance){
@> amount = userBalance;
}
uint256 amountScaled = amount.rayMul(index);
_userState[from].index = index.toUint128();
@> _burn(from, amount.toUint128());
if (receiverOfUnderlying != address(this)) {
@> IERC20(_assetAddress).safeTransfer(receiverOfUnderlying, amount);
}
emit Burn(from, receiverOfUnderlying, amount, index);
return (amount, totalSupply(), amount);
}
function _update(address from, address to, uint256 amount) internal override {
// Scale amount by normalized income for all operations (mint, burn, transfer)
@> uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
}
contract LendingPool is ILendingPool, Ownable, ReentrancyGuard, ERC721Holder, Pausable {
function getNormalizedIncome() external view returns (uint256) {
return reserve.liquidityIndex;
}
}

Key issues:

  1. Scaling Mismatch:

  • RToken balances are stored in scaled form using a liquidity index

  • The amount parameter represents underlying asset units while userBalance returns scaled units

  • Direct comparison between scaled/non-scaled values creates mathematical inconsistency

  1. Incorrect Withdrawal Capping:

  • The check if(amount > userBalance) uses incompatible units

  • Should compare amount against userBalance.rayMul(index) to account for interest accrual

  1. Protocol Integration Impact:

  • Affects core LendingPool.withdraw functionality through ReserveLibrary interactions ReserveLibrary.withdraw

  • Fails to properly validate the maximum withdrawable amount when burning RTokens

contract LendingPool is ILendingPool, Ownable, ReentrancyGuard, ERC721Holder, Pausable {
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ...
// 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
);
// ...
}
}
library ReserveLibrary {
function withdraw(
ReserveData storage reserve,
ReserveRateData storage rateData,
uint256 amount,
address recipient
) internal returns (uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) {
// ...
// Burn RToken from the recipient - will send underlying asset to the recipient
@> (uint256 burnedScaledAmount, uint256 newTotalSupply, uint256 amountUnderlying) = IRToken(reserve.reserveRTokenAddress).burn(
recipient, // from
recipient, // receiverOfUnderlying
amount, // amount
reserve.liquidityIndex // index
);
// ...
}
}

This mismatch becomes progressively worse as the liquidity index increases over time, effectively creating a denial-of-service condition for liquidity providers trying to withdraw their assets.

Impact

This vulnerability creates a progressive denial-of-service for withdrawals, with demonstrated financial impact:

Exploit Example:

  1. Initial Deposit: 100 CRVUSD (liquidityIndex = 1.0e27)

  2. After 1 Year: Index grows to 1.1e27 (10% interest)

  3. User Attempts Withdrawal:

    • Entitled Amount: 110 CRVUSD (100 * 1.1)

    • Actual Withdrawable: 100 CRVUSD (capped by flawed comparison)

    • Immediate Loss: 10 CRVUSD (9.09% of entitled funds)

Compounding Effect:

  • At Index 2.0e27: Users can only withdraw 50% of true balance per transaction

  • At Index 10.0e27: Requires 10+ transactions to fully exit position

  • Gas costs multiply while funds remain partially trapped

Protocol-Level Consequences:

  1. Liquidity providers effectively subsidize the protocol with trapped funds

  2. Violates core protocol promise of fungible liquidity positions

  3. Creates irreversible loss vector that worsens with protocol success (higher utilization → faster index growth)

  4. Undermines trust in withdrawal guarantees critical for DeFi lending primitives

Tools Used

Manual Review

Recommendations

Implement proper scaling conversions in RToken.burn function with the following changes:

uint256 maxWithdrawable = userBalance.rayMul(index);
if (amount > maxWithdrawable) {
amount = maxWithdrawable;
}

This approach maintains consistency with the RToken's scaling mechanism while ensuring users can withdraw their full entitled amount in a single transaction.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Wrong balance check in RToken::burn

It's underlying vs underlying

Appeal created

elvin_a_block Submitter
3 months ago
inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Wrong balance check in RToken::burn

It's underlying vs underlying

Support

FAQs

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