Core Contracts

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

Raac NFT will be stuck in the StabilityPool contract .

Summary

During Liquidation, Raac NFT will be sent to the StabilityPool contract and it will be permanently stuck

Vulnerability Details

The StabilityPool contract does not inherit or implement ERC721Holder or IERC721Receiver as recommneded by OpenZeppelin when a contract will be recieving or holding an NFT token.

contract StabilityPool is IStabilityPool, Initializable, ReentrancyGuard, OwnableUpgradeable, PausableUpgradeable {

During liquidation, the owner or the manager of the StabilityPool contract can call the liquidateBorrower function to liquidate a borrower's debt.

function liquidateBorrower(address userAddress) external onlyManagerOrOwner nonReentrant whenNotPaused {
_update();
// Get the user's debt from the LendingPool.
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();
// Approve the LendingPool to transfer the debt amount
bool approveSuccess = crvUSDToken.approve(address(lendingPool), scaledUserDebt);
if (!approveSuccess) revert ApprovalFailed();
// Update lending pool state before liquidation
lendingPool.updateState();
// Call finalizeLiquidation on LendingPool
@>> lendingPool.finalizeLiquidation(userAddress);
emit BorrowerLiquidated(userAddress, scaledUserDebt);
}

The part marked @>> transfers the NFT from the LendingPool contract to the StabilityPool contract.

The issue is that after NFT has been transferred, there is no way to withdraw it from the StabilityPool contract.

Proof Of Concept

See how to intigrate foundry to hardhat project.
Create a new file POC.t.sol in project /test/ folder . Paste the poc below and run forge test --mt test_POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {LendingPool} from "../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACNFT} from "../contracts/core/tokens/RAACNFT.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {RToken} from "../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../contracts/core/tokens/DebtToken.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {ILendingPool} from "contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "../contracts/core/tokens/DEToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import {RAACMinter} from "../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract Audit_Test is Test {
LendingPool public lendingPool;
StabilityPool public stabilityPool;
RAACNFT public raacNFT;
RAACHousePrices public priceOracle;
RToken public rToken;
DebtToken public debtToken;
crvUSDToken public crvusd;
DEToken public deToken;
RAACToken public raacToken;
RAACMinter public raacMinter;
address owner = makeAddr("owner");
uint256 constant INITIAL_PRIME_RATE = 1e27; // 1 RAY
function setUp() public {
vm.startPrank(owner);
// Base tokens
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
// Price oracle
priceOracle = new RAACHousePrices(owner);
priceOracle.setOracle(owner);
// NFT
raacNFT = new RAACNFT(address(crvusd), address(priceOracle), owner);
// Pool tokens
rToken = new RToken("RToken", "RT", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DET", owner, address(rToken));
// Deploy LendingPool
uint256 initialPrimeRate = 0.1e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(priceOracle),
initialPrimeRate
);
// Deploy RAAC token with correct tax rates
raacToken = new RAACToken(owner, 1000, 1000);
// Deploy StabilityPool first
stabilityPool = new StabilityPool(owner);
// Deploy RAACMinter last
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
// Initialize StabilityPool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
// Setup cross-contract references
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
vm.stopPrank();
}
function test_POC() public {
uint256 HOUSE_PRICE = 50;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 depositAmount = 100;
uint256 BORROW_AMOUNT = 50;
uint8 HOUSE_TOKEN_ID = 1;
vm.prank(owner);
priceOracle.setHousePrice(HOUSE_TOKEN_ID, HOUSE_PRICE);
// mint some tokens to respective actors
crvusd.mint(alice, depositAmount);
crvusd.mint(bob, HOUSE_PRICE);
crvusd.mint(address(this), 1000 ether);
crvusd.transfer(address(stabilityPool), 1000 ether);
// alice deposits
vm.startPrank(alice);
crvusd.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
// bob deposits nft as collaterial to borrow asset
vm.startPrank(bob);
crvusd.approve(address(raacNFT), HOUSE_PRICE);
raacNFT.mint(HOUSE_TOKEN_ID, HOUSE_PRICE);
// Deposit NFT as collateral and borrow
raacNFT.approve(address(lendingPool), HOUSE_TOKEN_ID);
//deposit nft as collaterial
lendingPool.depositNFT(HOUSE_TOKEN_ID);
assertEq(raacNFT.ownerOf(HOUSE_TOKEN_ID), address(lendingPool));
// borrow asset
lendingPool.borrow(BORROW_AMOUNT);
// initiate liquidation process
lendingPool.initiateLiquidation(bob);
vm.stopPrank();
// owner liquidates borrower
vm.warp(block.timestamp + 4 days);
vm.prank(owner);
stabilityPool.liquidateBorrower(bob);
// Verify NFT is stuck in StabilityPool
assertEq(raacNFT.ownerOf(HOUSE_TOKEN_ID), address(stabilityPool));
}
}

Impact

Raac NFT will be permanently stuck in the StabilityPool contract

Tools Used

Manual Review

Recommendations

Raac NFT should be transferred to the NFTLiquidator contract since there is a function that is intended to be called only by the stability pool contract to pull the NFT from the StabilityPool contract.

function liquidateNFT(uint256 tokenId, uint256 debt) external {
// only the stability pool can call this contract
@>> if (msg.sender != stabilityPool) revert OnlyStabilityPool();

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/StabilityPool/NFTLiquidator.sol#L97

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.