Core Contracts

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

User is unable to close Liquidation even if healthFactor is greater than healthFactorLiquidationThreshold

Summary

Users can interact with the depositNFT function to add collateral to the lending pool and increase their health factor. Notably, this function can be called even when a user's position is under liquidation. However, even if the user deposits enough collateral to raise their health factor above the healthFactorLiquidationThreshold, they still cannot cancel the liquidation.

Vulnerability Details

function depositNFT(uint256 tokenId) external nonReentrant whenNotPaused { //@audit when user is under lq can still depsosit NFT result in user lost of funds
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (raacNFT.ownerOf(tokenId) != msg.sender) revert NotOwnerOfNFT();
UserData storage user = userData[msg.sender];
if (user.depositedNFTs[tokenId]) revert NFTAlreadyDeposited();
user.nftTokenIds.push(tokenId);
user.depositedNFTs[tokenId] = true;
raacNFT.safeTransferFrom(msg.sender, address(this), tokenId);
emit NFTDeposited(msg.sender, tokenId);
}

depositNFT can be called when user's under liquidation.

function closeLiquidation() external nonReentrant whenNotPaused {
address userAddress = msg.sender;
if (!isUnderLiquidation[userAddress]) revert NotUnderLiquidation();
// update state
ReserveLibrary.updateReserveState(reserve, rateData);
if (block.timestamp > liquidationStartTime[userAddress] + liquidationGracePeriod) {
revert GracePeriodExpired();
}
UserData storage user = userData[userAddress];
uint256 userDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex);
if (userDebt > DUST_THRESHOLD) revert DebtNotZero();
isUnderLiquidation[userAddress] = false;
liquidationStartTime[userAddress] = 0;
emit LiquidationClosed(userAddress);
}

Close liquidation only check user's debt not check current healthFactor.

Consider the following scenio:

  1. bob deposit nft1

  2. bob borrow assets from lending pool

  3. nft1's price fall

  4. someone invoke initiateLiquidation

  5. user deposit nft2

  6. user's cancel liquidation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "contracts/core/collectors/FeeCollector.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/veRAACToken.sol";
import "forge-std/Test.sol";
import "contracts/mocks/core/oracles/TestRAACHousePriceOracle.sol";
import "contracts/mocks/core/tokens/crvUSDToken.sol";
import "contracts/mocks/core/tokens/MockUSDC.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "forge-std/Console2.sol";
import "contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/pools/StabilityPool/NFTLiquidator.sol";
import "contracts/core/pools/StabilityPool/MarketCreator.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract Pool2 is Test{
RAACToken public raccToken;
veRAACToken public veraacToken;
crvUSDToken public crv;
RToken public rToken;
DebtToken public debtToken;
RAACNFT public raccNFT;
TestRAACHousePriceOracle public oracle;
RAACHousePrices public housePrice;
MockUSDC public usdc;
LendingPool public pool;
NFTLiquidator nftLq;
StabilityPool sbPool;
TransparentUpgradeableProxy proxy;
MarketCreator public market;
uint256 NFTTokenId = 1;
address alice = address(0x1001);
address bob = address(0x1002);
address candy = address(0x1003);
function setUp() public {
crv = new crvUSDToken(address(this));
rToken = new RToken("rt","rt",address(this),address(crv));
debtToken = new DebtToken("db","db",address(this));
address router;
usdc = new MockUSDC(1_000_000e6);
housePrice = new RAACHousePrices(address(this));
oracle = new TestRAACHousePriceOracle(router,bytes32('1'),address(housePrice));
raccNFT = new RAACNFT(address(usdc),address(housePrice),address(this));
pool = new LendingPool(address(crv),address(rToken),address(debtToken),address(raccNFT),address(housePrice),1e26);
rToken.setReservePool(address(pool));
housePrice.setOracle(address(this));
debtToken.setReservePool(address(pool));
nftLq = new NFTLiquidator(address(crv),address(raccNFT),address(this),50);
sbPool = new StabilityPool(address(this));
//add proxy.
proxy = new TransparentUpgradeableProxy(address(sbPool),address(this),"");
raccToken = new RAACToken(address(this),1_000,1_000);
veraacToken = new veRAACToken(address(raccToken));
market = new MarketCreator(address(this),address(raccToken),address(crv));
StabilityPool(address(proxy)).initialize(address(rToken),address(rToken),address(raccToken),address(this),address(crv),address(pool));
}
function testUserDepositNFTWhileUnderHf() public {
pool.setStabilityPool(address(proxy));
crv.mint(address(proxy),10e18);
//alice deposit crv to pool.
crv.mint(alice,100e18);
vm.startPrank(alice);
crv.approve(address(pool), 100e18);
pool.deposit(100e18);
vm.stopPrank();
//bob mint nft.
housePrice.setHousePrice(NFTTokenId, 10e18);
housePrice.setHousePrice(NFTTokenId+1, 20e18);
usdc.mint(bob,30e18);
vm.startPrank(bob);
usdc.approve(address(raccNFT),30e18);
raccNFT.mint(NFTTokenId, 10e18);
raccNFT.mint(NFTTokenId+1, 20e18);
raccNFT.approve(address(pool), NFTTokenId);
raccNFT.approve(address(pool), NFTTokenId+1);
//bob deposit NFT.
pool.depositNFT(NFTTokenId);
//bob borrow.
pool.borrow(5e18);
vm.stopPrank();
//price change.
housePrice.setHousePrice(NFTTokenId, 1e18);
//init lq.
pool.initiateLiquidation(bob);
//user deposit another NFT.
vm.prank(bob);
pool.depositNFT(NFTTokenId+1);
//check user's hf.
assert(pool.calculateHealthFactor(bob) > pool.healthFactorLiquidationThreshold());
vm.prank(bob);
pool.closeLiquidation();
}

Out:

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 10.71ms (4.05ms CPU time)
Ran 1 test suite in 238.45ms (10.71ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in tests/Pool2.t.sol:Pool2
[FAIL: DebtNotZero()] testUserDepositNFTWhileUnderHf() (gas: 1340310)
Encountered a total of 1 failing tests, 0 tests succeeded

Impact

user can't cancel liquidation

Tools Used

Foundry

Recommendations

check user's healthFactor when close liquidation

Updates

Lead Judging Commences

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