Core Contracts

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

`DebtToken` burn scaling revert

Summary

In the contract DebtToken.sol the update function is overridden and the following line is added:

uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
super._update(from, to, scaledAmount);

After these lines of code the amount will be increased. This will cause the burn function to revert in a case when amount > userBalance. We also have the same case in the contract RToken.

Vulnerability Details

In the burn function we see the following code:

function burn(
address from,
uint256 amount,
uint256 index
) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
if (from == address(0)) revert InvalidAddress();
if (amount == 0) {
return (0, totalSupply(), 0, 0);
}
uint256 userBalance = balanceOf(from);
uint256 balanceIncrease = 0;
if (_userState[from].index != 0 && _userState[from].index < index) {
uint256 borrowIndex = ILendingPool(_reservePool).getNormalizedDebt();
balanceIncrease = userBalance.rayMul(borrowIndex) - userBalance.rayMul(_userState[from].index);
amount = amount;
}
_userState[from].index = index.toUint128();
if(amount > userBalance){
amount = userBalance;
}
uint256 amountScaled = amount.rayDiv(index);
if (amountScaled == 0) revert InvalidAmount();
_burn(from, amount.toUint128());
emit Burn(from, amountScaled, index);
return (amount, totalSupply(), amountScaled, balanceIncrease);
}

In a case where amount is bigger than the user balance we make the amount equal to userBalance. After that we call the function _burn with the amount that is equal to userBalance. You probably see where I am going, as the update function adds additional value the function _burn will revert because the user will not have enough tokens.

Impact

  • Reversion of Burn Transactions:
    The additional conversion in the _update function increases the burn amount unexpectedly, leading to transaction reverts when a user’s burn amount is capped to their balance.

  • Denial of Service:
    If burn operations consistently revert, users may be unable to reduce their debt positions, potentially disrupting protocol operations.

Tools Used

Manual Code Review

Recommendations

  1. Use a Consistent Normalized Debt Index Across Calculations
    Ensure that the same normalized debt value is used throughout the burn operation to avoid discrepancies between scaled and non‐scaled amounts. For example, capture the normalized debt index once at the start of the function and use it for all scaling operations:

    function burn(
    address from,
    uint256 amount,
    uint256 index
    ) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
    if (from == address(0)) revert InvalidAddress();
    if (amount == 0) {
    return (0, totalSupply(), 0, 0);
    }
    // Capture the normalized debt index once and use it consistently
    uint256 normalizedDebt = ILendingPool(_reservePool).getNormalizedDebt();
    uint256 userBalance = balanceOf(from);
    uint256 balanceIncrease = 0;
    if (_userState[from].index != 0 && _userState[from].index < index) {
    balanceIncrease = userBalance.rayMul(normalizedDebt) - userBalance.rayMul(_userState[from].index);
    }
    _userState[from].index = index.toUint128();
    if(amount > userBalance){
    amount = userBalance;
    }
    // Use the captured normalizedDebt for scaling conversion
    uint256 amountScaled = amount.rayDiv(normalizedDebt);
    if (amountScaled == 0) revert InvalidAmount();
    _burn(from, amount.toUint128());
    emit Burn(from, amountScaled, index);
    return (amount, totalSupply(), amountScaled, balanceIncrease);
    }
  2. Refactor the Internal _update Function
    Modify _update to optionally bypass additional scaling during burn operations. One approach is to add a flag to control whether scaling should occur:

    // Overloaded internal update with a flag indicating burn operations
    function _update(address from, address to, uint256 amount, bool skipScaling) internal {
    uint256 normalizedDebt = ILendingPool(_reservePool).getNormalizedDebt();
    uint256 scaledAmount = skipScaling ? amount : amount.rayDiv(normalizedDebt);
    super._update(from, to, scaledAmount);
    emit Transfer(from, to, amount);
    }

    Then update the burn function to call _update without scaling:

    function burn(
    address from,
    uint256 amount,
    uint256 index
    ) external override onlyReservePool returns (uint256, uint256, uint256, uint256) {
    // ... (same preliminary logic as before)
    // Instead of directly calling _burn which triggers _update,
    // perform burn and then update internal state without additional scaling.
    _burn(from, amount.toUint128());
    _update(from, address(0), amount, true); // 'true' indicates skip scaling during burn
    emit Burn(from, amount.rayDiv(index), index);
    return (amount, totalSupply(), amount.rayDiv(index), balanceIncrease);
    }
Updates

Lead Judging Commences

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

DebtToken::burn caps amount to userBalance but internal _update applies further scaling, causing transaction reverts when burning full balances and breaking repayment/liquidation

The amount and userBalance are both in actual debt units, making the comparison valid. No reverts occur - the burn function works as intended by burning the correct scaled amount from storage.

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

DebtToken::burn caps amount to userBalance but internal _update applies further scaling, causing transaction reverts when burning full balances and breaking repayment/liquidation

The amount and userBalance are both in actual debt units, making the comparison valid. No reverts occur - the burn function works as intended by burning the correct scaled amount from storage.

Support

FAQs

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