Core Contracts

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

Incorrect `amounToMint` in `DebtToken.mint()` Could Lead to Cascading Effects

Summary

The DebtToken.mint() function incorrectly inflates user debt by double-counting accrued interest since the last borrowing action when minting debt tokens. Specifically, the function adds balanceIncrease to amountToMint, causing users to receive more debt tokens than intended. This artificially increases the user’s outstanding debt, leading to excessive interest accrual and unfairly higher repayments over time. The issue worsens exponentially as interest compounds on an already inflated debt balance.

Vulnerability Details

balanceOf() in DebtToken.sol already applies interest scaling using rayMul(reserve.usageIndex), meaning that the returned balance includes all accumulated interest since the last borrowing action associated with the accumulatively minted debt tokens:

DebtToken.sol#L223-L226

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

balanceIncrease is calculated based on balanceOf() multiplied by the delta of indices, so it already reflects the accrued interest since the last mint. It's meant merely to be emitted for this specifically additional info.

However, instead of minting only amount, the function adds balanceIncrease to amountToMint, artificially increasing the user's debt balance.

DebtToken.sol#L160-L162

uint256 amountToMint = amount + balanceIncrease;
_mint(onBehalfOf, amountToMint.toUint128());
emit Transfer(address(0), onBehalfOf, amountToMint);
emit Mint(user, onBehalfOf, amountToMint, balanceIncrease, index);
return (scaledBalance == 0, amountToMint, totalSupply());

But fortunately, user.scaledDebtBalanc is incremented with scaledAmount, i.e. amount.rayDiv(reserve.usageIndex) instead of amountMinted (matched by amountToMint):

LendingPool.sol#L348-L358

// Update user's scaled debt balance
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// Mint DebtTokens to the user (scaled amount)
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
// Transfer borrowed amount to user
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;

Here's the first issue. When user attempting to make a full repayment, they will simply input an arbitrarily big amount (similar to some other protocol logic that will have an input amount of type(unit256).max) since it's going to be capped here:

DebtToken.sol#L202-L213

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);

So, amountScaled is going to match up with amountBurned and then deducted from user.scaledDebtBalance:

LendingPool.sol#L416-L425

// Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
// is not actualRepayAmount because we want to allow paying extra dust and we will then cap there
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
// Transfer reserve assets from the caller (msg.sender) to the reserve
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;

As expected, it's going to incur an underflow revert.

The next issue will be more pronounced. This is because debt token totalSupply() is also inflated by balanceIncrease. As such, reserve.totalUsage as assigned by it via the returned newTotalSupply when borrowing,

LendingPool.sol#L359-L360

// reserve.totalUsage += amount;
reserve.totalUsage = newTotalSupply;

is going affect the calculation when ReserveLibrary.updateInterestRatesAndLiquidity() is invoked at the end of the borrow logic:

ReserveLibrary.sol#L214-L236

// Calculate 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
);
// Update the reserve interests
updateReserveInterests(reserve, rateData);

calculateUtilizationRate() is going to return a larger utilizationRate than intended and correspondingly render rateData.currentUsageRate, rateData.currentLiquidityRate larger than expected:

ReserveLibrary.sol#L303-L309

function calculateUtilizationRate(uint256 totalLiquidity, uint256 totalDebt) internal pure returns (uint256) {
if (totalLiquidity < 1) {
return WadRayMath.RAY; // 100% utilization if no liquidity
}
uint256 utilizationRate = totalDebt.rayDiv(totalLiquidity + totalDebt).toUint128();
return utilizationRate;
}

In the end, reserve.liquidityIndex and reserve.usageIndex (the frequently referenced indices) will be calculated larger than expected too.

Impact

Borrowers are minted excessive debt tokens when multiple mintings of debt tokens are involved at different time, rendering likely revert when making full repayment that will be crucial in timing to close a liquidation.

This issue increases systemic debt while over benefiting the lenders due to the inflated indices, potentially making loans unrepayable, prone to liquidation, and destabilizing the lending/borrowing market.

Note: The only way a user could avoid such issue is borrowing the second time or subsequently using a different and new address, albeit with another set of NFTs needed as collateral.

Tools Used

Manual

Recommendations

Consider making the following refactoring:

- uint256 amountToMint = amount + balanceIncrease;
+ uint256 amountToMint = amount;
Updates

Lead Judging Commences

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

DebtToken::mint miscalculates debt by applying interest twice, inflating borrow amounts and risking premature liquidations

Support

FAQs

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

Give us feedback!