Core Contracts

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

### Issues in LendingPool:: (`initiateLiquidation` and `finalizeLiquidation`)

Author Revealed upon completion

Overview

The initiateLiquidation and finalizeLiquidation functions are designed to handle the liquidation process for users whose health factor falls below a certain threshold. However, there are several issues in the implementation that can lead to incorrect behavior, potential economic losses, and vulnerabilities. Below is a detailed analysis of the issues.


Vulnerability Details

  1. Incorrect Health Factor Check:

    • The function checks if healthFactor >= healthFactorLiquidationThreshold to determine if liquidation should be initiated.

    • This logic is backwards. Liquidation should be initiated when the health factor is below the threshold, not above it.

    • Impact: Users who should be liquidated will not be liquidated, leading to under-collateralized positions and potential losses for the protocol.

  2. Lack of Access Control:

    • The function does not restrict who can call it. Any external address can initiate liquidation for any user.

    • Impact: Malicious actors could abuse this function to initiate unnecessary liquidations, causing disruption and potential losses for users.

  3. Missing Validation for userAddress:

    • The function does not validate whether userAddress is a valid user (e.g., whether userData[userAddress] exists).

    • Impact: Calling the function with an invalid address could lead to unexpected behavior or wasted gas.

function initiateLiquidation(address userAddress) external nonReentrant whenNotPaused {
if (isUnderLiquidation[userAddress]) revert UserAlreadyUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
UserData storage user = userData[userAddress];
uint256 healthFactor = calculateHealthFactor(userAddress);
// Incorrect health factor check
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooLow();
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
emit LiquidationInitiated(msg.sender, userAddress);
}
  • Issue 1: The health factor check is incorrect. It should be healthFactor < healthFactorLiquidationThreshold.

  • Issue 2: No access control is applied to restrict who can call this function.

  • Issue 3: No validation is performed to ensure userAddress is a valid user.


2. Issues in finalizeLiquidation

Vulnerability Details

  1. Incorrect Grace Period Check:

    • The function checks if block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod to determine if the grace period has expired.

    • This logic is backwards. The liquidation should proceed only if the grace period has expired (i.e., block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod).

    • Impact: Liquidations will fail if the grace period has expired, allowing under-collateralized positions to persist.

  2. Lack of Validation for userAddress:

    • Similar to initiateLiquidation, the function does not validate whether userAddress is a valid user.

    • Impact: Calling the function with an invalid address could lead to unexpected behavior or wasted gas.

  3. Incorrect Debt Burning Logic:

    • The function burns the user's debt using IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex).

    • However, the amountBurned returned from the burn function is not validated to ensure it matches the expected userDebt.

    • Impact: If the burn function does not burn the full debt, the user's scaledDebtBalance will not be updated correctly, leading to incorrect accounting.

  4. Incorrect Asset Transfer Logic:

    • The function transfers reserve assets from the msg.sender (Stability Pool) to the reserve.reserveRTokenAddress using safeTransferFrom.

    • However, it does not ensure that the msg.sender has sufficient balance or allowance to cover the transfer.

    • Impact: The transfer could fail, causing the liquidation to revert and leaving the protocol with under-collateralized positions.

  5. Incorrect Update of scaledDebtBalance:

    • The function subtracts amountBurned from user.scaledDebtBalance, but it does not ensure that amountBurned is less than or equal to user.scaledDebtBalance.

    • Impact: If amountBurned exceeds user.scaledDebtBalance, the subtraction will underflow, causing the transaction to revert.

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
// Incorrect grace period check
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// Transfer NFTs to Stability Pool
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer reserve assets from Stability Pool to cover the debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update user's scaled debt balance
user.scaledDebtBalance -= amountBurned; // Potential underflow
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}
  • Issue 1: The grace period check is incorrect. It should be block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod.

  • Issue 2: No validation is performed to ensure userAddress is a valid user.

  • Issue 3: The burn function's output is not validated to ensure the full debt is burned.

  • Issue 4: The safeTransferFrom function does not check the msg.sender's balance or allowance.

  • Issue 5: The subtraction user.scaledDebtBalance -= amountBurned could underflow if amountBurned > user.scaledDebtBalance.


Recommendations

For initiateLiquidation:

  1. Fix Health Factor Check:

    • Change the condition to if (healthFactor >= healthFactorLiquidationThreshold) to if (healthFactor < healthFactorLiquidationThreshold).

  2. Add Access Control:

    • Restrict the function to authorized roles (e.g., onlyStabilityPool or onlyLiquidator).

  3. Validate userAddress:

    • Add a check to ensure userData[userAddress] exists.

For finalizeLiquidation:

  1. Fix Grace Period Check:

    • Change the condition to if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) to if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod).

  2. Validate userAddress:

    • Add a check to ensure userData[userAddress] exists.

  3. Validate Debt Burning:

    • Ensure amountBurned matches userDebt or handle partial burns appropriately.

  4. Check msg.sender Balance and Allowance:

    • Add checks to ensure msg.sender has sufficient balance and allowance for the transfer.

  5. Prevent Underflow:

    • Add a check to ensure amountBurned <= user.scaledDebtBalance.


function initiateLiquidation(address userAddress) external nonReentrant whenNotPaused onlyLiquidator {
if (isUnderLiquidation[userAddress]) revert UserAlreadyUnderLiquidation();
if (userData[userAddress].scaledDebtBalance == 0) revert InvalidUser();
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 healthFactor = calculateHealthFactor(userAddress);
if (healthFactor >= healthFactorLiquidationThreshold) revert HealthFactorTooHigh();
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
emit LiquidationInitiated(msg.sender, userAddress);
}

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
if (userData[userAddress].scaledDebtBalance == 0) revert InvalidUser();
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// Transfer NFTs to Stability Pool
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
delete user.nftTokenIds;
// Burn DebtTokens from the user
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) = IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
require(amountBurned == userDebt, "Debt not fully burned");
// Transfer reserve assets from Stability Pool to cover the debt
require(
IERC20(reserve.reserveAssetAddress).balanceOf(msg.sender) >= amountScaled,
"Insufficient balance in Stability Pool"
);
require(
IERC20(reserve.reserveAssetAddress).allowance(msg.sender, address(this)) >= amountScaled,
"Insufficient allowance"
);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
// Update user's scaled debt balance
require(user.scaledDebtBalance >= amountBurned, "Underflow in scaledDebtBalance");
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}

Conclusion

The initiateLiquidation and finalizeLiquidation functions contain several critical issues, including incorrect health factor and grace period checks, lack of access control, and improper debt burning logic. By addressing these issues, the liquidation process will function as intended, ensuring the protocol's stability and fairness.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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