Core Contracts

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

Incorrect Debt Scaling in DebtToken Burn During Liquidation Finalization

Summary

an inconsistency exists in the way the LendingPool contract calculates and passes the debt amount to the DebtToken's burn function during liquidation finalization. In the liquidation flow, the contract computes the user's debt by applying a multiplication factor (using rayMul) to the user's scaled debt, whereas the DebtToken's burn function expects an unscaled amount that it converts internally using rayDiv in the DebtToken::_update. This mismatch leads to an incorrect debt value being passed for burning, which can result in erroneous debt accounting and potential financial imbalances within the protocol.

Vulnerability Details

The issue arises due to the following discrepancies:

  • Debt Calculation in Finalization:
    In the finalizeLiquidation function, the contract computes the user's debt as follows:

    uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);

    This calculation converts the user's scaled debt into an absolute debt value by applying the reserve's usage index.

  • Incompatible Burn Function Conversion:
    The DebtToken's burn function is called the _burn --> _update performs its own scaling conversion. Specifically, it calculates the scaled amount to burn using:

    uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());

    This means that the burn --> _update function expects an amount that has not yet been converted. Passing an already scaled debt (via rayMul) results in the burn function applying an additional division by the normalized debt, effectively miscomputing the scaled amount.

  • Inconsistent Handling in Repay vs. Finalization:
    In the repay function, the LendingPool correctly passes the repayment amount without any prior scaling:

    IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);

    This ensures the burn function's internal conversion is applied correctly. However, during liquidation finalization, by pre-scaling the debt value with rayMul, the LendingPool ends up with a double conversion, leading to an erroneous burn of DebtTokens.

POC

Consider the following hypothetical example:

  1. Initial Setup:

    • Suppose a user has a scaled debt balance of X.

    • The reserve's usage index (normalized debt factor) is A.

    • The expected actual debt should be:
      Actual Debt = X.rayMul(A).

  2. Repayment Scenario (Correct Flow):

    • When the user repays their debt, the contract passes the unscaled repayment amount directly to the burn function:

      IDebtToken(...).burn(user, repaymentAmount, reserve.usageIndex);
    • Inside the update function, the repayment amount is converted using:
      scaledAmount = repaymentAmount.rayDiv(A),
      which correctly translates the unscaled repayment into the scaled units.

  3. Liquidation Finalization (Faulty Flow):

    • During liquidation finalization, the contract computes the user's debt as:

      uint256 userDebt = user.scaledDebtBalance.rayMul(A);

      Suppose this results in a value D.

    • This value D is then passed to the burn function:

      IDebtToken(...).burn(user, D, reserve.usageIndex);
    • The _update function then performs:
      scaledAmount = D.rayDiv(A).

    • Since D was already computed as X.rayMul(A), this extra conversion reduces the burned amount to:
      (X.rayMul(A)).rayDiv(A) ≈ X, effectively nullifying the intended conversion and misrepresenting the user’s actual debt.

  4. Outcome:

    • The debt accounting becomes inconsistent as the DebtToken burn operation does not correctly reflect the intended absolute debt value.

    • This discrepancy may lead to either an over-burn or under-burn of DebtTokens, ultimately causing inaccurate updates to reserve.totalUsage and the user's debt balance.

Impact

The miscalculation of burned DebtTokens results in a failure to accurately update the user’s debt and the protocol’s total usage, potentially leading to systemic imbalances.

Liquidation finalization might not reduce the user's debt as expected, or it could overly penalize the user, impacting collateral recovery and liquidation fairness.

Tools Used

Manual Review

Recommendations

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
- uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
+ uint256 userDebt = user.scaledDebtBalance;
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// Transfer NFTs to Stability Pool
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update user's scaled debt balance
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::burn incorrectly calculates amountScaled using rayMul instead of rayDiv, causing incorrect token burn amounts and breaking the interest accrual mechanism

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::burn incorrectly calculates amountScaled using rayMul instead of rayDiv, causing incorrect token burn amounts and breaking the interest accrual mechanism

Support

FAQs

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