Core Contracts

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

Usage rate is increased even when no debt is present in `LendingPool`

Summary

Lending and borrowing protocols offer interest rates for depositors so that they provide liquidity. RAAC does the same, where the deposit interest rates are coming from the extra funds borrowers need to repay. The protocol uses a linear interest rate for deposit rates and a compounding rate for borrowers. These rates are updated after every meaningful action (i.e. deposit, withdraw, borrow, repay, and liquidate). However, in the current system, the borrowing rate increases even if there are no loans in the system, making it extremely unfavorable for borrowers.

Vulnerability Details

Let's look at how interest rates are updated in RAAC:

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
reserve.liquidityIndex = calculateLiquidityIndex(
rateData.currentLiquidityRate,
timeDelta,
reserve.liquidityIndex
);
// Update usage index (debt index) using compounded interest
reserve.usageIndex = calculateUsageIndex(
rateData.currentUsageRate,
timeDelta,
reserve.usageIndex
);
// Update the last update timestamp
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveInterestsUpdated(reserve.liquidityIndex, reserve.usageIndex);
}
function calculateLiquidityIndex(uint256 rate, uint256 timeDelta, uint256 lastIndex) internal pure returns (uint128) {
uint256 cumulatedInterest = calculateLinearInterest(rate, timeDelta, lastIndex);
return cumulatedInterest.rayMul(lastIndex).toUint128();
}
function calculateUsageIndex(uint256 rate, uint256 timeDelta ,uint256 lastIndex) internal pure returns (uint128) {
uint256 interestFactor = calculateCompoundedInterest(rate, timeDelta);
return lastIndex.rayMul(interestFactor).toUint128();
}

We can see that the update is happening only if sufficient time has passed, which is good. It also reverts if no proper rates are set. If we dig deeper to see how the liquidityIndex is updated, we would see that it depends on the currentLiquidityRate, which will always be 0 if there is no debt:

function calculateLiquidityRate(uint256 utilizationRate, uint256 usageRate, uint256 protocolFeeRate, uint256 totalDebt) internal pure returns (uint256) {
if (totalDebt < 1) {
@> return 0; // @audit - deposit rates won't increase if there are no loans (i.e. no utilization)
}
uint256 grossLiquidityRate = utilizationRate.rayMul(usageRate);
uint256 protocolFeeAmount = grossLiquidityRate.rayMul(protocolFeeRate);
uint256 netLiquidityRate = grossLiquidityRate - protocolFeeAmount;
return netLiquidityRate;
}

However, when calculating the usageIndex, there are no checks for the debt is 0, as the currentUsageRate will increase with the baseRate even with no loans in the system:

function calculateBorrowRate(
uint256 primeRate,
uint256 baseRate,
uint256 optimalRate,
uint256 maxRate,
uint256 optimalUtilizationRate,
uint256 utilizationRate
) internal pure returns (uint256) {
if (primeRate <= baseRate || primeRate >= maxRate || optimalRate <= baseRate || optimalRate >= maxRate) {
revert InvalidInterestRateParameters();
}
uint256 rate;
if (utilizationRate <= optimalUtilizationRate) {
uint256 rateSlope = primeRate - baseRate;
uint256 rateIncrease = utilizationRate.rayMul(rateSlope).rayDiv(optimalUtilizationRate);
@> rate = baseRate + rateIncrease; // @audit - even if the utilization rate is 0, the borrowing rate will increase with the base rate with time
} else {
uint256 excessUtilization = utilizationRate - optimalUtilizationRate;
uint256 maxExcessUtilization = WadRayMath.RAY - optimalUtilizationRate;
uint256 rateSlope = maxRate - primeRate;
uint256 rateIncrease = excessUtilization.rayMul(rateSlope).rayDiv(maxExcessUtilization);
rate = primeRate + rateIncrease;
}
return rate;
}

From the above, we can see that the borrowing rate will increase with a compounding rate, making it extremely unfavorable for borrowers.

If we look at how AAVE is doing it, we will see that these indexes increase only when debt is present.

Impact

Borrowers will pay increased rates from the very beginning, basically making them lose the incentive to join the protocol.

Tools Used

Manual review

Recommendations

Check if there is any debt in the system before updating the usageRate.

Updates

Lead Judging Commences

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

LendingPool's usageIndex compounds at baseRate even when no debt exists, creating artificially high starting interest for future borrowers and discouraging protocol adoption

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

LendingPool's usageIndex compounds at baseRate even when no debt exists, creating artificially high starting interest for future borrowers and discouraging protocol adoption

Appeal created

anonymousjoe Auditor
3 months ago
0xb0k0 Submitter
3 months ago
inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool's usageIndex compounds at baseRate even when no debt exists, creating artificially high starting interest for future borrowers and discouraging protocol adoption

Support

FAQs

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