Core Contracts

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

A healthy lending pool position can be wrongly liquidated

Summary

A healthy lending pool position can be wrongly liquidated.

Vulnerability Details

A unhealthy lending pool position can be liquidated.

Firstly, the liquidation is initiated against an unhealthy position by calling initiateLiquidation().

Then, after the liquidation grace period, StabilityPool owner/manager calls liquidateBorrower() to finalized the liqudiation.

finalizeLiquidation() in LendingPool performs the actual liquidation, the problem is that, it does not check if the position is healthy or not by the time of liquidating, as a result, a healthy position can be wrongly liquidated.

Impact

A healthy position is liquidated.

POC

Please run forge test --mt testAudit_LiquidateHealthyPosition:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
RAACMinter raacMinter;
function setUp() public {
vm.warp(1 days);
raacHousePrices = new RAACHousePrices(owner);
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
raacToken = new RAACToken(owner, 100, 50);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
lendingPool.transferOwnership(owner);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
));
stabilityPool = StabilityPool(stabilityPoolProxy);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
raacToken.setMinter(address(raacMinter));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
}
function testAudit_LiquidateHealthyPosition() public {
// Deposit liquidity
uint256 depositAmount = 1000e18;
address alice = makeAddr("Alice");
crvUSD.mint(alice, depositAmount);
vm.startPrank(alice);
crvUSD.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
// Deposit NFT
uint256 nftTokenId = 1;
uint256 nftPrice = 2000e18;
vm.prank(owner);
raacHousePrices.setHousePrice(nftTokenId, nftPrice);
address bob = makeAddr("Bob");
crvUSD.mint(bob, nftPrice);
vm.startPrank(bob);
crvUSD.approve(address(raacNft), nftPrice);
raacNft.mint(nftTokenId, nftPrice);
raacNft.approve(address(lendingPool), nftTokenId);
lendingPool.depositNFT(nftTokenId);
vm.stopPrank();
// Borrow liquidity
uint256 borrowAmount = 1000e18;
vm.prank(bob);
lendingPool.borrow(borrowAmount);
vm.warp(block.timestamp + 365 days);
lendingPool.updateState();
// NFT price drop
vm.prank(owner);
raacHousePrices.setHousePrice(nftTokenId, nftPrice / 2);
// Initiate liquidation
lendingPool.initiateLiquidation(bob);
vm.warp(block.timestamp + 4 days);
// NFT price drop
vm.prank(owner);
raacHousePrices.setHousePrice(nftTokenId, nftPrice * 2);
// Position becomes healthy
uint256 healthFactorLiquidationThreshold = lendingPool.healthFactorLiquidationThreshold();
uint256 bobHealthFactor = lendingPool.calculateHealthFactor(bob);
assertGt(bobHealthFactor, healthFactorLiquidationThreshold);
crvUSD.mint(address(stabilityPool), 10000e18);
uint256 balanceBefore = crvUSD.balanceOf(address(stabilityPool));
// Positon is liquidated anyway
vm.prank(owner);
stabilityPool.liquidateBorrower(bob);
uint256 balanceAfter = crvUSD.balanceOf(address(stabilityPool));
}
}

Tools Used

Manaul Review

Recommendations

It is necessary to check if a position is heathy or not when performs the actual liquidating.

Updates

Lead Judging Commences

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

LendingPool::finalizeLiquidation() never checks if debt is still unhealthy

Support

FAQs

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

Give us feedback!