Core Contracts

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

Usage index inversion causes debt tokens to accrue a discount instead of interest

Summary

The protocol uses a usageIndex to track the accrual of interest on outstanding debt. This index is updated over time using compounded interest with the formula:

reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
function calculateCompoundedInterest(uint256 rate,uint256 timeDelta) internal pure returns (uint256) {
if (timeDelta < 1) {
return WadRayMath.RAY;
} // rate * 1e27 / 1 year in seconds
uint256 ratePerSecond = rate.rayDiv(SECONDS_PER_YEAR);
uint256 exponent = ratePerSecond.rayMul(timeDelta);
// Will use a taylor series expansion (7 terms)
return WadRayMath.rayExp(exponent);
}
function calculateUsageIndex(uint256 rate, uint256 timeDelta ,uint256 lastIndex) internal pure returns (uint128) {
uint256 interestFactor = calculateCompoundedInterest(rate, timeDelta);
return lastIndex.rayMul(interestFactor).toUint128();
}
);

Since compounded interest (via a Taylor series expansion) produces a factor greater than one for positive rates and time deltas, the usageIndex increases over time as intended to reflect accumulating interest. However, the subsequent use of this increasing usageIndex in the normalized debt calculation leads to an unintended effect: it causes the normalized debt to be lower than expected. When the DebtToken functions scale debt amounts using this normalized debt, borrowers end up with lower effective debt values, effectively receiving a discount instead of being charged the appropriate interest. This miscalculation undermines the protocol’s revenue model and shifts economic risk away from borrowers.

Vulnerability Details

How the Usage Index Is Updated

The update function recalculates the usageIndex as follows:

uint256 ratePerSecond = rate.rayDiv(SECONDS_PER_YEAR);
uint256 exponent = ratePerSecond.rayMul(timeDelta);
// Will use a taylor series expansion (7 terms)
return WadRayMath.rayExp(exponent);
);
  • Intended Behavior:
    The compounded interest factor (computed via calculateCompoundedInterest) is greater than 1, so multiplying the last index by this factor increases the usageIndex over time to capture accrued interest on the debt.

Misuse in Normalized Debt Calculation

Normalized debt is derived using the updated usageIndex:

return calculateCompoundedInterest(rateData.currentUsageRate, timeDelta).rayMul(reserve.usageIndex);
  • Issue:
    Although the usageIndex is correctly increased to represent accrued interest, its use in this calculation inadvertently lowers the normalized debt value. DebtToken functions subsequently rely on this normalized debt to scale user balances. For instance, when updating debt balances, the protocol computes:

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

Because the normalized debt is lower than it should be, the scaling operation results in a smaller debt being recorded. Consequently, borrowers effectively pay less interest—or even receive a discount—on their debt.

  • Balance Calculations:
    The balanceOf function multiplies a scaled balance by the normalized debt. With a lower-than-expected normalized debt, users’ effective debt balances are underrepresented.

  • Burning Debt Tokens:
    In the burn function, the difference between the updated usageIndex and the user’s stored index is used to calculate accrued interest. However, because the normalized debt is understated, the additional debt due to interest accrual is not fully captured, allowing users to burn tokens without paying the full accrued interest.

  • Total Supply Calculation:
    The total supply of debt tokens is scaled by the normalized debt. An understated normalized debt means the overall debt in the system is lower than intended, potentially compromising the protocol's financial stability.

Impact

  • Economic Exploitation:
    Borrowers benefit from an effective discount on their debt, paying less interest than the protocol intends. This mispricing can lead to significant revenue losses for the platform.

  • Interest Accrual Failure:
    The miscalculation prevents proper interest accrual on borrowed assets, undermining the fundamental lending mechanics and potentially destabilizing the reserve.

  • Accounting Inconsistencies:
    Downstream functions that rely on normalized debt (e.g., balance updates, burns, and total supply calculations) yield inaccurate results. This inconsistency affects risk assessments and may lead to unexpected liquidations or solvency issues.

Proof-of-Concept (PoC)

Scenario Setup

Assume the following initial conditions:

  • Initial usageIndex: 1e27 (this is the RAY constant representing “1” in 27-decimal precision).

  • Total Debt (totalUsage): 1,000 underlying units.

  • Usage Rate: 10% per year (expressed in RAY as 0.1 × 1e27, i.e. 1e26).

  • Time Delta: 31,536,000 seconds (approximately one year).

  • Compounded Interest Calculation:
    For a 10% rate over one year, the compounded interest factor computed by calculateCompoundedInterest is approximately 1.10517e27 (i.e. a 10.517% increase).

Current (Flawed) Calculation

  1. Usage Index Update:
    The current code updates the usageIndex as follows:

    // Existing code multiplies the last index by the interest factor.
    reserve.usageIndex = lastIndex.rayMul(interestFactor).toUint128();

    With our numbers:

    • lastIndex = 1e27

    • interestFactor = 1.10517e27
      → New usageIndex becomes ~1.10517e27.

  2. Normalized Debt Calculation:
    Later, the protocol calculates normalized debt by (simplified):

    normalizedDebt = calculateCompoundedInterest(rate, timeDelta).rayMul(reserve.usageIndex);

    Substituting the values:

    • Compounded interest factor = 1.10517e27

    • Updated usageIndex = 1.10517e27
      → Normalized Debt ≈ 1.10517e27 × 1.10517e27 / 1e27 ≈ 1.2214e27.

  3. Debt Token Repayment Impact:
    Suppose a borrower originally incurred a debt of 1,000 underlying units (scaled as 1,000 tokens, since initially usageIndex = RAY).
    After one year, the expected debt with proper interest would be roughly 1,000 × 1.10517 = 1,105.17 underlying units.
    However, when the borrower repays 1,105.17 units, the protocol scales this repayment using:

    scaledAmount = underlyingRepayment.rayDiv(normalizedDebt);

    Plugging in our numbers:

    • scaledAmount = 1,105.17e18 (if we assume underlying units in 1e18 precision) divided by 1.2214e27
      ≈ 903.4 tokens.
      Interpretation:
      The borrower ends up burning only ~903 tokens to repay 1,105.17 underlying units. Instead of fully accruing interest on the debt, the flawed calculation results in a “discount” on the debt repayment. Over time, this discrepancy could allow borrowers to repay less than they should, undermining the protocol’s revenue and risk model.

Tools Used

Manual Review

Recommendations

The root cause is that the usageIndex is updated by multiplying by the compounded interest factor. If the intended design is to have the normalized debt increase with accrued interest (so borrowers pay more), the usageIndex should decrease over time. One approach is to update the usageIndex using division instead of multiplication.

Revised calculateUsageIndex Function

Below is a sample fix. In this example, we change the update so that:

  • Instead of multiplying the previous index by the interest factor, we divide by the interest factor.

  • This adjustment causes the normalized debt (which is computed from the usageIndex) to be higher, properly reflecting the accrued interest.

function calculateUsageIndex(
uint256 rate,
uint256 timeDelta,
uint256 lastIndex
) internal pure returns (uint128) {
uint256 interestFactor = calculateCompoundedInterest(rate, timeDelta);
// Mitigation: Instead of increasing the usage index, we reduce it by dividing by the interest factor.
return lastIndex.rayDiv(interestFactor).toUint128();
}

Updated Interest Update Function

With the revised usage index update, the updateReserveInterests function will now compute:

function updateReserveInterests(ReserveData storage reserve, ReserveRateData storage rateData) internal {
uint256 timeDelta = block.timestamp - uint256(reserve.lastUpdateTimestamp);
if (timeDelta < 1) {
return;
}
uint256 oldLiquidityIndex = reserve.liquidityIndex;
if (oldLiquidityIndex < 1) revert LiquidityIndexIsZero();
// Update liquidity index using linear interest (unchanged)
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
// Update usage index (debt index) using compounded interest (fixed version)
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
// Update the last update timestamp
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveInterestsUpdated(reserve.liquidityIndex, reserve.usageIndex);
}

Expected Outcome with the Fix

Using the same numbers:

  • Initial usageIndex: 1e27

  • Interest Factor: 1.10517e27

  • New usageIndex (fixed): 1e27.rayDiv(1.10517e27) ≈ 0.9048e27

Then, when normalized debt is computed (assuming it uses the usageIndex without an extra compounded factor), the debt scaling will properly increase the effective debt. For example, if the borrower’s scaled debt remains 1,000 tokens:

  • New normalized debt ≈ 0.9048e27 (or used in scaling such that the effective underlying debt becomes closer to 1,105.17 units).

  • The borrower will need to repay more debt tokens (or the conversion factor will be adjusted so that the accrued interest is correctly reflected), thereby eliminating the unintended discount.

Updates

Lead Judging Commences

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

Support

FAQs

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

Give us feedback!