Core Contracts

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

Incorrect NFT Balance Verification During Liquidation Process

Summary

The StabilityPool contract liquidation mechanism where NFT collateral transfers lack proper verification. This creates a potential attack vector in the real estate lending protocol's core stability mechanism.

StabilityPool.liquidateBorrower(borrower) ->
LendingPool.finalizeLiquidation(borrower) ->
RAACNFT.transferFrom(lendingPool, stabilityPool, tokenId)

The path shows the NFT transfer isn't properly tracked during liquidation. The StabilityPool contract receives the NFT but the balance verification fails.

When borrowers get liquidated, their NFT collateral should transfer from the LendingPool to the StabilityPool. However, the current implementation fails to verify these transfers, potentially allowing NFTs to be intercepted or lost during liquidation. StabilityPool.sol#liquidateBorrower()

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Missing pre-liquidation NFT balance check
uint256 initialNFTBalance = RAACNFT.balanceOf(address(this));
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Debt token approval happens but no NFT transfer approval check
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
// Calls finalizeLiquidation but doesn't verify NFT receipt
lendingPool.finalizeLiquidation(userAddress);
// Missing post-liquidation NFT balance verification
uint256 finalNFTBalance = RAACNFT.balanceOf(address(this));
require(finalNFTBalance > initialNFTBalance, "NFT transfer failed");
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

The function verifies debt amounts and crvUSD balances but lacks critical NFT transfer validation. Adding balance checks before and after the liquidation ensures proper collateral transfer.

Vulnerability Details

The RAAC protocol's stability mechanism resembles a real estate safety deposit box, when borrowers default, their NFT-tokenized properties should transfer seamlessly to the StabilityPool. However, a critical flaw in this transfer mechanism creates a dangerous gap.

Imagine a valet service that hands over car keys without checking the claim ticket. Similarly, the StabilityPool's liquidateBorrower() function processes NFT transfers without verifying receipt of the collateral. This oversight in the protocol's $1B TVL system leaves the entire liquidation process vulnerable.

Here's how an attack unfolds: When a $500,000 real estate NFT liquidation triggers, the LendingPool initiates the transfer. During this process, the StabilityPool blindly assumes successful receipt of the NFT collateral. A malicious actor could exploit this split-second vulnerability to intercept the NFT, leaving stability providers without their rightful collateral backing.

The technical root lies in the StabilityPool contract's missing balance verification:

function liquidateBorrower(address borrower) external {
// The protocol assumes NFT transfer success without verification
endingPool.finalizeLiquidation(userAddress);
}

This impacts the protocol's core stability mechanism where DEToken holders provide insurance against loan defaults. Without proper NFT transfer verification, the protocol risks losing valuable real estate collateral during liquidations.

Impact

This breaks the liquidation mechanism by allowing NFTs to be transferred without proper balance verification. A malicious actor could potentially intercept NFTs during the liquidation process.

Recommendations

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Track initial NFT balance before liquidation
uint256 preNFTBalance = RAACNFT.balanceOf(address(this));
// Get and validate user's debt
uint256 userDebt = lendingPool.getUserDebt(userAddress);
uint256 scaledUserDebt = WadRayMath.rayMul(userDebt, lendingPool.getNormalizedDebt());
if (userDebt == 0) revert InvalidAmount();
// Verify sufficient crvUSD balance
uint256 crvUSDBalance = crvUSDToken.balanceOf(address(this));
if (crvUSDBalance < scaledUserDebt) revert InsufficientBalance();
// Approve debt token transfer
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
lendingPool.updateState();
lendingPool.finalizeLiquidation(userAddress);
// Verify NFT transfer was successful
uint256 postNFTBalance = RAACNFT.balanceOf(address(this));
require(postNFTBalance > preNFTBalance, "NFT transfer failed");
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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