Core Contracts

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

Flawed Bad Debt Handling in Liquidation Process Will Lead To Lose Of Funds

Summary

An issue was identified in the LendingPool.sol contract regarding how the protocol handles bad debt during the liquidation process. The final step of liquidation is executed by the Stability Pool, which calls finalizeLiquidation() to transfer reserve assets (crvUSD) to reserveRTokenAddress. However, the protocol lacks a proper mechanism to absorb bad debt, leading to a risk of insolvency and potential bank runs.

Vulnerability Details

Affected Functions:

  • initiateLiquidation() in LendingPool.sol

  • finalizeLiquidation()

When a borrower’s health factor falls below the healthFactorLiquidationThreshold, liquidation is triggered via initiateLiquidation(). The final step of liquidation occurs when the Stability Pool calls finalizeLiquidation(), which transfers crvUSD to reserveRTokenAddress to cover the bad debt. However, the mechanism for handling bad debt is flawed due to the following reasons:

  1. Lack of Liquidator Incentive: There is no direct incentive for liquidators, which discourages participation and increases the probability of accumulating bad debt.

  2. No Defined Source of crvUSD Deposits: The protocol assumes that crvUSD will always be available in the Stability Pool, but there is no deposit mechanism for crvUSD in the codebase.

  3. Risk of Insolvency & Bank Runs: Since crvUSD is not properly socialized among lenders, early lenders can withdraw their funds, leaving later lenders at risk of losing their deposits in case of large-scale bad debt accumulation.

POC

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.19;
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {crvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {console, Test} from "../../lib/forge-std/src/Test.sol";
contract LendingPoolTest is Test {
crvUSDToken crvusd;
crvUSDToken token;
RAACToken raacToken;
RAACHousePrices raacHousePrices;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACMinter raacMinter;
address owner = address(1);
address lender = address(2);
address borrower = address(3);
address lenderTwo = address(4);
address liquidator = address(5);
function setUp() external {
crvusd = new crvUSDToken(owner);
vm.prank(owner);
crvusd.setMinter(owner);
token = crvusd;
raacHousePrices = new RAACHousePrices(owner);
// Deploy NFT
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
stabilityPool = new StabilityPool(owner);
// Deploy pool tokens
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
// Deploy pools
uint256 initialPrimeRate = 0.1 * 1e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
vm.startPrank(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
vm.stopPrank();
// Mint initial tokens and setup approvals
uint256 initialBalance = 1000 ether;
// Mint crvUSD to users
crvusd.mint(lender, initialBalance);
crvusd.mint(borrower, initialBalance);
crvusd.mint(lenderTwo, initialBalance);
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
raacHousePrices.setHousePrice(1, 100 ether);
vm.stopPrank();
vm.roll(block.number + 1);
uint256 tokenId = 1;
uint256 amountToPay = 100 ether;
token.mint(borrower, amountToPay);
vm.startPrank(borrower);
token.approve(address(raacNFT), amountToPay);
raacNFT.mint(tokenId, amountToPay);
vm.stopPrank();
}
modifier depositAndNFT() {
uint256 depositAmount = 50 ether;
vm.startPrank(lender);
crvusd.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
vm.startPrank(lenderTwo);
crvusd.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
uint256 tokenId = 1;
vm.startPrank(borrower);
raacNFT.approve(address(lendingPool), tokenId);
lendingPool.depositNFT(tokenId);
vm.stopPrank();
_;
}
function test_PoorBadDebtHandling() external depositAndNFT {
uint256 borrowedAmount = 50 ether;
vm.startPrank(borrower);
lendingPool.borrow(borrowedAmount);
uint debtBalance = debtToken.balanceOf(borrower);
vm.stopPrank();
vm.prank(owner);
raacHousePrices.setHousePrice(1, 60 ether);
vm.prank(liquidator);
lendingPool.initiateLiquidation(borrower);
//Grace Period
vm.warp(block.timestamp + 7 days);
// Set Stability Pool address
lendingPool.setStabilityPool(address(stabilityPool));
vm.startPrank(address(stabilityPool));
crvusd.approve(address(lendingPool), type(uint256).max);
vm.expectRevert(); // No enough funds to cover for the bad debt
lendingPool.finalizeLiquidation(borrower);
vm.stopPrank();
// withdraw
vm.prank(lender);
lendingPool.withdraw(50 ether);
vm.expectRevert(); // lenderTwo suffers for bad debt
vm.prank(lenderTwo);
lendingPool.withdraw(50 ether);
}
}

Impact

  • The protocol’s failure to properly mitigate bad debt can lead to insolvency.

  • Potential for Bank Runs: If a significant amount of bad debt accumulates, early lenders can withdraw while later lenders may suffer losses.

Tools Used

Manual

Recommendations

  1. Introduce Liquidator Incentives: Provide a liquidation bonus or discount to encourage liquidators to participate in the process.

  2. Implement a Socialized Loss Mechanism: Ensure bad debt is distributed among all lenders rather than affecting only late withdrawers.

  3. Establish a Clear Deposit Mechanism for crvUSD: Ensure that crvUSD is funded adequately through deposits or a reserve mechanism before being relied upon for covering bad debt.

Updates

Lead Judging Commences

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

StabilityPool design flaw where liquidations will always fail as StabilityPool receives rTokens but LendingPool expects it to provide crvUSD

Support

FAQs

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

Give us feedback!