Core Contracts

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

Inadequate Liquidation State Checks Allow Unintended NFT Seizure

Summary

In the liquidation process where users under liquidation can deposit additional NFTs as collateral. These NFTs are immediately seized, even if the user never borrowed against them. This violates the protocol's intended behavior where users under liquidation should only lose collateral tied to their debt, not newly added assets.

Vulnerability Details

The depositNFT function lacks a check for the user's liquidation status (isUnderLiquidation). This allows users to add NFTs to their position after liquidation has been initiated but before finalization. During finalizeLiquidation(), all deposited NFTs (including those added post-liquidation) are transferred to the Stability Pool, regardless of their relationship to the debt.

  1. Missing Liquidation Check in depositNFT:

    function depositNFT(uint256 tokenId) external nonReentrant whenNotPaused {
    // ❌ No check for isUnderLiquidation[msg.sender]
    UserData storage user = userData[msg.sender];
    user.nftTokenIds.push(tokenId);
    user.depositedNFTs[tokenId] = true;
    raacNFT.safeTransferFrom(msg.sender, address(this), tokenId);
    }
  2. NFT Seizure in finalizeLiquidation:

    function finalizeLiquidation(address userAddress) external nonReentrant onlyStabilityPool {
    // Transfers ALL NFTs, including those deposited post-liquidation
    for (uint256 i = 0; i < user.nftTokenIds.length; i++) {
    raacNFT.transferFrom(address(this), stabilityPool, user.nftTokenIds[i]);
    }
    }

Proof of Concept (PoC)

// Test Setup:
// - User has 1 NFT (TokenID 1, Value 100 crvUSD), borrows 60 crvUSD.
// - Price drops → liquidation initiated.
// - User deposits TokenID 2 (Value 200 crvUSD).
// PoC Code:
it("Seizes post-liquidation NFTs unfairly", async () => {
// Initiate liquidation
await lendingPool.initiateLiquidation(user.address);
// User deposits new NFT during liquidation
await lendingPool.connect(user).depositNFT(2); // ✅ Currently allowed
// Finalize liquidation after grace period
await time.increase(4 * 86400);
await stabilityPool.liquidateBorrower(user.address);
// Verify NFTs seized
const nft1Owner = await raacNFT.ownerOf(1);
const nft2Owner = await raacNFT.ownerOf(2);
expect(nft1Owner).to.equal(stabilityPool.address); // Expected
expect(nft2Owner).to.equal(stabilityPool.address); // ❌ Unfair seizure
});

Impact

  • Critical Severity: Users can lose NFTs deposited after liquidation initiation, even if those NFTs were never used as collateral for the debt.

  • Example Attack Flow:

    1. User A has 1 NFT (Value: 100 crvUSD) and borrows 60 crvUSD (within 80% LTV).

    2. NFT prices drop → liquidation is initiated on User A.

    3. Before liquidation finalization, User A deposits a new NFT (Value: 200 crvUSD) via depositNFT().

    4. Stability Pool calls finalizeLiquidation()both NFTs are seized, even though the debt (60 crvUSD) only required 75 crvUSD (80% of 100 crvUSD) in collateral.

Users lose uncorrelated collateral, violating the principle that liquidation should only affect assets tied to the debt.

Tools Used

Manual review

Recommendations

Add a liquidation status check to depositNFT():

function depositNFT(uint256 tokenId) external nonReentrant whenNotPaused {
if (isUnderLiquidation[msg.sender]) revert CannotDepositDuringLiquidation(); // ✅
// Rest of the function
}
Updates

Lead Judging Commences

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

Users can deposit NFTs using LendingPool::depositNFT while under liquidation, leading to unfair liquidation of NFTs that weren't part of original position

Support

FAQs

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