Core Contracts

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

Incomplete Liquidation State Validation Allows Premature Finalization

Summary

The liquidation process in the RAAC protocol allows premature finalization without verifying complete debt repayment, leaving borrowers' NFT collateral transferred while debt remains outstanding.

// Key State Variables
borrower: active loan holder
isUnderLiquidation: true
userDebt: remains non-zero after finalization

The StabilityPool can trigger finalization before fully repaying the borrower's debt, breaking the protocol's liquidation invariants.

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
// Basic validation but no check if StabilityPool has sufficient funds to cover debt
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
ReserveLibrary.updateReserveState(reserve, rateData);
// Grace period check is correct but insufficient for ensuring proper liquidation
if (block.timestamp <= liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodNotExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
// State is updated before ensuring successful transfer of funds
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
// NFTs are transferred before confirming debt repayment
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;
// Debt tokens are burned before confirming successful transfer
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Transfer happens after state changes, could revert and leave system in bad state
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit LiquidationFinalized(stabilityPool, userAddress, userDebt, getUserCollateralValue(userAddress));
}

The issue is the ordering of operations. NFT transfers and state updates occur before confirming successful debt repayment, which could leave the system in an inconsistent state if the transfer fails.

Picture this attack. A liquidation begins when a borrower's position becomes unhealthy. The StabilityPool initiates liquidation but can call finalize before completing debt repayment. The LendingPool transfers the NFT collateral while debt remains, creating an inconsistent state.

Vulnerability Details

Think of the liquidation process like a three-step dance between the LendingPool and StabilityPool. When a borrower's position becomes unhealthy, the protocol should gracefully transfer their NFT collateral while clearing their debt. However, there's a critical misstep in this choreography.

The story begins when the LendingPool marks a borrower for liquidation. Their prized NFT collateral, perhaps representing a valuable real estate asset stands ready for transfer. The StabilityPool steps in to handle the debt repayment, but here's where things get interesting.

Just like a bank must ensure a check has cleared before releasing assets, the protocol should verify debt clearance before transferring NFT collateral. Instead, the finalizeLiquidation() function in LendingPool.sol eagerly releases the NFT:

function finalizeLiquidation(address borrower) external {
// Transfers the NFT without waiting for debt confirmation
}

This creates a race condition where the StabilityPool could fail to fully repay the debt after receiving the collateral. The exact impact? A borrower with $100,000 in debt could lose their $150,000 NFT collateral while still owing $50,000 to the protocol.

Impact

The liquidation process could leave borrowers with residual debt while transferring their NFT collateral, creating an inconsistent state.

Recommendations

function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
// First validate liquidation status
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// Update reserve state for accurate debt calculation
ReserveLibrary.updateReserveState(reserve, rateData);
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
// First transfer funds from StabilityPool to cover debt
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender,
reserve.reserveRTokenAddress,
amountScaled
);
// Then burn debt tokens after successful transfer
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(userAddress, userDebt, reserve.usageIndex);
// Update user state after successful debt clearing
user.scaledDebtBalance -= amountBurned;
reserve.totalUsage = newTotalSupply;
// Only transfer NFTs after debt is fully cleared
for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
uint256 tokenId = user.nftTokenIds[i];
user.depositedNFTs[tokenId] = false;
raacNFT.transferFrom(address(this), stabilityPool, tokenId);
}
// Clear liquidation state last
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
delete user.nftTokenIds;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
}

This implementation ensures atomic execution by:

  1. Validating initial state

  2. Transferring debt coverage first

  3. Burning debt tokens

  4. Transferring NFT collateral only after debt clearance

  5. Updating liquidation state last

The ordering guarantees that collateral transfer only happens after successful debt resolution.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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