Core Contracts

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

Incorrect DebtToken's totalSupply calculation disrupts LendingPool's accounting.

Summary

In the DebtToken contract, the totalSupply() function incorrectly uses rayDiv instead of rayMul when scaling the total supply with the normalized debt index, resulting in LendingPool accounting such as calculation of interest rates to be wrong.

Vulnerability Details

The DebtToken contract implements a debt accounting system where token balances are scaled by a usage index (normalized debt) to account for accumulated interest over time. This follows the same pattern as Aave's variable debt tokens.

The current implementation of totalSupply() is:

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
return scaledSupply.rayDiv(ILendingPool(_reservePool).getNormalizedDebt());
}

This is incorrect because:

  1. The normalized debt index (_usageIndex) starts at RAY (1e27) and increases over time to reflect accumulated interest

  2. When calculating debt balances, we need to multiply by the index to get the current debt amount including interest

  3. Using rayDiv instead of rayMul results in dividing by the index, which:

    • Decreases the total supply when it should increase

    • Returns an incorrect value that's inverse to the actual total debt

    • Creates inconsistency with individual balance calculations

This is incosistent with the balanceOf() function which correctly uses rayMul:

function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledBalance = super.balanceOf(account);
return scaledBalance.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}

The discrepancy between these two functions creates a situation where the sum of individual balances doesn't match the total supply, breaking a fundamental invariant of ERC20 tokens and the audit scope did say the tokens should be 100% EIP20 compliant.

Furthermore, in both the burn and mint functions of the DebtToken, the totalSupply is always returned as one of the return parameters;

function mint(...) {
// ...
return (scaledBalance == 0, amountToMint, totalSupply());
}
function burn(...) {
// ...
return (amount, totalSupply(), amountScaled, balanceIncrease);

In the LendingPool contract, the _repay, borrow, and finalizeLiquidation functions all call either the burn or mint function on the DebtToken and the returned newTotalSupply is used to updated reserve's totalUsage.

For instance in the LendingPool's borrow function;

function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// ...
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
// @audit that newTotalSupply is used to update the reserve's totalUsage
reserve.totalUsage = newTotalSupply;
// @audit since the reserve's totalUsage is used here, the interests rate will be wrongly calculated.
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);

Looking at ReserveLibrary.updateInterestRatesAndLiquidity;

// Calculate utilization rate
// @audit the reserve.totalUsage used to calculate utilizationRate will be wrong, hence, will have wrong utilization rate
uint256 utilizationRate = calculateUtilizationRate(reserve.totalLiquidity, reserve.totalUsage);
// Update current usage rate (borrow rate)
rateData.currentUsageRate = calculateBorrowRate(
rateData.primeRate,
rateData.baseRate,
rateData.optimalRate,
rateData.maxRate,
rateData.optimalUtilizationRate,
@> utilizationRate
);
// Update current liquidity rate
rateData.currentLiquidityRate = calculateLiquidityRate(
@> utilizationRate,
rateData.currentUsageRate,
rateData.protocolFeeRate,
totalDebt
);

From above analysis, we have done we can conclude that this issue causes the protocol to always use incorrect borrow rates as well as incorrect interest rates.

PoC

The PoC below just focuses on breaking the ERC20 compliance, but I hope the explanation for how the issue affects the interest and borrow rates has been clarified above.

  1. User A borrows 100 tokens when usage index = RAY (1e27)

  2. Time passes and usage index increases to 1.1 * RAY

  3. balanceOf(A) correctly returns 110 tokens (100 * 1.1)

  4. But totalSupply() incorrectly returns ~90.9 tokens (100 / 1.1)

  5. This creates an accounting discrepancy where total supply is less than individual balance

Impact

  • Incorrect borrow and interest rates negatively affect protocol's economics which leads to protocol's insolvency.

  • Incorrect reporting of total protocol debt

  • Broken ERC20 invariant where sum of balances ≠ total supply

Tools Used

Manual code review

Recommendations

Change the totalSupply() function to use rayMul:

function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
uint256 scaledSupply = super.totalSupply();
return scaledSupply.rayMul(ILendingPool(_reservePool).getNormalizedDebt());
}
Updates

Lead Judging Commences

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

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

DebtToken::totalSupply incorrectly uses rayDiv instead of rayMul, severely under-reporting total debt and causing lending protocol accounting errors

Support

FAQs

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

Give us feedback!