Core Contracts

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

Missing `_userState[user].index` Reset When Making Full Repayment or Finalizing Liquidation

Summary

In DebtToken.sol, _userState[user].index isn't deleted when user is making a full repayment or the position is finalized with liquidation. This could lead to unwanted balanceIncrease when the user is making a new borrow from scratch again in the future.

Vulnerability Details

A user is typically minted the extra balanceIncrease of debt tokens on top of the inputted amount when making the second or subsequent borrow on an existing position via the delta of indices multiplied by the user's balance. The logic will also have _userState[onBehalfOf].inde assigned the latest index.

DebtToken.sol#L150-L167

uint256 scaledBalance = balanceOf(onBehalfOf);
bool isFirstMint = scaledBalance == 0;
uint256 balanceIncrease = 0;
if (_userState[onBehalfOf].index != 0 && _userState[onBehalfOf].index < index) {
balanceIncrease = scaledBalance.rayMul(index) - scaledBalance.rayMul(_userState[onBehalfOf].index);
}
_userState[onBehalfOf].index = index.toUint128();
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());

Likewise, when it comes to burning of the debt tokens, _userState[from].index is correctly assigned the latest index if this entails a partial repayment. However, the function logic fails to delete or reset the index when it involves a full repayment or the finalization of a liquidation:

DebtToken.sol#L200

_userState[from].index = index.toUint128();

Consequently, when the same user were to come back and start a new borrow after a long time, instead of being minted the inputted amount, an excessive amount of balanceIncrease would be added to amountToMint. This is because _userState[onBehalfOf].index != 0 and that _userState[onBehalfOf].index is significantly or much smaller than the latest index this time.

In the end, like I have reported separately, totalSupply() will also be inflated significantly prior to getting itself assigned to reserve.totalUsage.

LendingPool.sol#L360-L363

reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);

In the end, when updateInterestRatesAndLiquidity() is invoked,

ReserveLibrary.sol#L198-L239

function updateInterestRatesAndLiquidity(ReserveData storage reserve,ReserveRateData storage rateData,uint256 liquidityAdded,uint256 liquidityTaken) internal {
// Update total liquidity
if (liquidityAdded > 0) {
reserve.totalLiquidity = reserve.totalLiquidity + liquidityAdded.toUint128();
}
if (liquidityTaken > 0) {
if (reserve.totalLiquidity < liquidityTaken) revert InsufficientLiquidity();
reserve.totalLiquidity = reserve.totalLiquidity - liquidityTaken.toUint128();
}
uint256 totalLiquidity = reserve.totalLiquidity;
uint256 totalDebt = reserve.totalUsage;
uint256 computedDebt = getNormalizedDebt(reserve, rateData);
uint256 computedLiquidity = getNormalizedIncome(reserve, rateData);
// 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);
emit InterestRatesUpdated(rateData.currentLiquidityRate, rateData.currentUsageRate);
}

utilizationRate, and hence rateData.currentUsageRate and rateData.currentLiquidityRate are going to be correspondingly inflated more than intended.

And, finally, the frequently referenced reserve.liquidityIndex and reserve.usageIndex will be inflated more than intended too via updateReserveInterests(), causing advantaged lending at the expense of disadvantaged borrowing.

Impact

Not deleting _userState[user].index when necessary could lead to a series of cascading effects, creating systemic imbalances. Although lenders benefit from the inflated liquidity index, borrowers would be prone to liquidation due to faster rate of debt piling associated with the inflated usage index.

Tools Used

Manual

Recommendations

Consider implementing a permissioned logic that will only allow the Lending Pool to delete _userState[user].index when user.scaledDebtBalance has be reduced to zero or less than DUST_THRESHOLD.

Updates

Lead Judging Commences

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

LendingPool::borrow tracks debt as user.scaledDebtBalance += scaledAmount while DebtToken mints amount+interest, leading to accounting mismatch and preventing full debt repayment

Support

FAQs

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

Give us feedback!