Core Contracts

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

NFTs transferred during liquidation are permanently locked in `StabilityPool`

Summary

The StabilityPool contract lacks functionality to handle NFTs received during liquidations, resulting in permanent loss of these assets as they become locked in the contract.

Vulnerability Details

In the StabilityPool::liquidateBorrower() function when a liquidation is finalized through lendingPool.finalizeLiquidation(userAddress), NFTs from the liquidated position are transferred to the StabilityPool. However, the contract has no functionality to manage, withdraw, or handle these NFTs in any way.

The issue is particularly concerning because:

  • There are no functions to retrieve NFTs

  • No NFT transfer/management capabilities exist

  • Even the contract owner cannot rescue locked NFTs

  • The contract is upgradeable but lacks NFT handling in its initial design, which is needed from the beginning to tokenize the NFTs for users participating in the StabilityPool.

Impact

When borrowers are liquidated, their NFT collateral becomes permanently locked in the StabilityPool contract with no possibility of recovery. This leads to:

  • Permanent loss of valuable NFT assets

  • Value loss for both the protocol and users

Tools Used

Manual review

Proof of Concept

Add the following test case to the test/e2e/protocols-tests.js file:

describe("StabilityPool", function() {
// other tests...
it('should handle liquidation absorption', async function () {
// Setup stability pool deposit
await contracts.stabilityPool.connect(user1).deposit(STABILITY_DEPOSIT);
await contracts.crvUSD.connect(user3).approve(contracts.stabilityPool.target, STABILITY_DEPOSIT);
await contracts.crvUSD.connect(user3).transfer(contracts.stabilityPool.target, STABILITY_DEPOSIT);
// Create position to be liquidated
const newTokenId = HOUSE_TOKEN_ID + 2;
await contracts.housePrices.setHousePrice(newTokenId, HOUSE_PRICE);
await contracts.crvUSD.connect(user2).approve(contracts.nft.target, HOUSE_PRICE);
await contracts.nft.connect(user2).mint(newTokenId, HOUSE_PRICE);
await contracts.nft.connect(user2).approve(contracts.lendingPool.target, newTokenId);
await contracts.lendingPool.connect(user2).depositNFT(newTokenId);
await contracts.lendingPool.connect(user2).borrow(BORROW_AMOUNT);
// Trigger and complete liquidation
await contracts.housePrices.setHousePrice(newTokenId, HOUSE_PRICE * 10n / 100n);
await contracts.lendingPool.connect(user3).initiateLiquidation(user2.address);
await time.increase(73 * 60 * 60);
// Will call the lendingPool.finalizeLiquidation(user2.address)
// We need stability pool to have crvUSD to cover the debt
const initialBalance = await contracts.crvUSD.balanceOf(contracts.stabilityPool.target);
await contracts.lendingPool.connect(owner).updateState();
await contracts.stabilityPool.connect(owner).liquidateBorrower(user2.address);
// Check if the NFT is transferred to the stability pool, but there is no way handle it later
const nftBalance = await contracts.nft.balanceOf(contracts.stabilityPool.target);
expect(nftBalance).to.be.equal(1);
const ownerOfTokenId = await contracts.nft.ownerOf(newTokenId);
expect(ownerOfTokenId).to.be.equal(contracts.stabilityPool.target);
});
});

Recommendations

  1. Add NFT management functionality

  2. Implement a systematic NFT handling mechanism during liquidations

    • Auction system for NFTs

    • Distribution to stability providers

    • Direct sale mechanism

  3. Add NFT recovery functions for emergency situations

Updates

Lead Judging Commences

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

Liquidated RAACNFTs are sent to the StabilityPool by LendingPool::finalizeLiquidation where they get stuck

Support

FAQs

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