Core Contracts

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

Inadequate NFT Liquidation Handling in finalizeLiquidation Function

Summary

The LendingPool::finalizeLiquidation function does not determine the number of NFTs required to cover the user's debt before transferring them to the StabilityPool. Instead, it transfers all deposited NFTs, potentially resulting in excessive collateral loss for the user.

Vulnerability Details

  • The function LendingPool::finalizeLiquidation iterates over all of user.nftTokenIds and transfers them to the StabilityPool.

  • There is no calculation to determine how many NFTs are actually needed to cover the outstanding debt.

  • This can result in more NFTs being transferred than necessary, leading to an unfair liquidation process.

Impact

  • Users may lose more collateral than required to settle their debt.

  • The system might unintentionally overcompensate the Stability Pool, creating an imbalance in liquidation rewards.

  • This could lead to reduced user trust and potential legal or regulatory issues.

PoC

Steps to Exploit:

  1. user2 deposits 1000 ether worth of crvusd into the lendingPool.

  2. user1 deposits one NFT into the lendingPool.

  3. user1 borrows 125 ether from the lendingPool.

  4. liquidation is triggered on user1's loan.

  5. user1 deposits additional two NFT after liquidation initiation.

  6. The contract time advances by 5 days.

  7. The stability pool finalizes the liquidation.

  8. All 3 NFTs of user1 are transfered from lending pool to the stability pool even though only the first one was used for collateral

contract LendingPoolTest is Test {
address owner;
address user1;
address user2;
crvUSDToken crvusd;
RAACNFT raacNFT;
RAACHousePrices raacHousePrices;
LendingPool lendingPool;
RToken rToken;
DebtToken debtToken;
StabilityPool stabilityPool;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacHousePrices = new RAACHousePrices(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RT", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
uint256 initialPrimeRate = (1 * RAY) / 10;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
stabilityPool = new StabilityPool(owner);
lendingPool.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
uint256 mintAmount = 1000 * WAD;
crvusd.mint(user1, mintAmount);
crvusd.mint(user2, 10000 * WAD);
crvusd.mint(address(stabilityPool), 10000 * WAD);
vm.prank(user1);
crvusd.approve(address(lendingPool), mintAmount);
vm.prank(user2);
crvusd.approve(address(lendingPool), mintAmount);
vm.prank(address(stabilityPool));
crvusd.approve(address(lendingPool), mintAmount);
raacHousePrices.setOracle(owner);
raacHousePrices.setHousePrice(1, 100 * WAD);
raacHousePrices.setHousePrice(2, 100 * WAD);
raacHousePrices.setHousePrice(3, 100 * WAD);
uint256 tokenId = 1;
uint256 tokenId2 = 2;
uint256 tokenId3 = 3;
uint256 amountToPay = 100 * WAD;
crvusd.mint(user1, amountToPay);
crvusd.mint(user1, amountToPay);
vm.prank(user1);
crvusd.approve(address(raacNFT), amountToPay);
vm.prank(user1);
raacNFT.mint(tokenId, amountToPay);
vm.prank(user1);
crvusd.approve(address(raacNFT), amountToPay);
vm.prank(user1);
raacNFT.mint(tokenId2, amountToPay);
vm.prank(user1);
crvusd.approve(address(raacNFT), amountToPay);
vm.prank(user1);
raacNFT.mint(tokenId3, amountToPay);
uint256 depositAmount = 1000 * WAD;
vm.prank(user2);
crvusd.approve(address(lendingPool), depositAmount);
vm.prank(user2);
lendingPool.deposit(depositAmount);
assertEq(crvusd.balanceOf(address(rToken)), 1000 * WAD);
uint256 depositAmount2 = 1000 ether;
vm.prank(user2);
crvusd.approve(address(lendingPool), depositAmount2);
vm.prank(user2);
lendingPool.deposit(depositAmount2);
vm.prank(user1);
raacNFT.approve(address(lendingPool), tokenId);
vm.prank(user1);
lendingPool.depositNFT(tokenId);
}
function testFinalizeTakeAllNFT() public {
uint256 borrowAmount = 125 ether;
vm.prank(user1);
lendingPool.borrow(borrowAmount);
lendingPool.initiateLiquidation(user1);
vm.prank(user1);
raacNFT.approve(address(lendingPool), 2);
vm.prank(user1);
lendingPool.depositNFT(2);
vm.prank(user1);
raacNFT.approve(address(lendingPool), 3);
vm.prank(user1);
lendingPool.depositNFT(3);
vm.warp(block.timestamp + 5 days);
vm.prank(address(stabilityPool));
lendingPool.finalizeLiquidation(user1);
}
}

Tools Used

  • Manual code review

Recommendations

  • Implement a mechanism to determine the minimum number of NFTs required to cover the user's debt.

  • Transfer only the required number of NFTs instead of all NFTs.

  • Introduce a fair liquidation model to ensure users do not lose excess collateral.

Updates

Lead Judging Commences

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

Support

FAQs

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