Core Contracts

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

Incomplete User Data State Update in initiateLiquidation Causes Inconsistent Liquidation Behavior

Summary

The initiateLiquidation function is intended to flag a user for liquidation by setting their liquidation status and start time. While it correctly updates global mappings (isUnderLiquidation and liquidationStartTime), it fails to update the corresponding fields within the user's UserData struct (e.g. user.underLiquidation and user.liquidationStartTime). This discrepancy can lead to inconsistent state and unexpected behavior in downstream processes that rely on the UserData struct for accurate user status.

Vulnerability Details

How It Begins

  1. Function Behavior:

    The initiateLiquidation function first updates the reserve state and retrieves the user's current health factor. If the health factor is below the liquidation threshold and the user is not already flagged for liquidation, the function sets the global state:

    isUnderLiquidation[userAddress] = true;
    liquidationStartTime[userAddress] = block.timestamp;

    However, the function does not update the corresponding fields in the UserData struct:

    UserData storage user = userData[userAddress];
    // Missing: user.underLiquidation = true;
    // Missing: user.liquidationStartTime = block.timestamp;
  2. Consequences:

    As a result, any logic or functions that depend on the UserData struct (rather than the global mappings) to determine if a user is under liquidation or to calculate time-dependent liquidation parameters will use stale data. This can lead to an incorrect or inconsistent liquidation process, as the actual user data remains unmodified even though the global state indicates that liquidation has been initiated.

Proof of Concept

Scenario Example

  • Setup:

    • Alice deposits reserve assets and mints an NFT as collateral.

    • She then borrows an amount (e.g., 900e18), which reduces her health factor below the liquidation threshold.

  • Liquidation Initiation:

    • Bob calls initiateLiquidation(ALICE), which updates the global mappings but fails to update Alice's UserData struct.

  • Test Verification:

    • A test retrieves Alice's UserData before and after initiating liquidation and finds that the fields underLiquidation and liquidationStartTime remain unchanged.

Test Suite Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "../src/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "../src/core/pools/LendingPool/LendingPool.sol";
import {DEToken} from "../src/core/tokens/DEToken.sol";
import {RToken} from "../src/core/tokens/RToken.sol";
import {DebtToken} from "../src/core/tokens/DebtToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RAACMinter} from "../src/core/minters/RAACMinter/RAACMinter.sol";
import {RAACHousePrices} from "../src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../src/core/tokens/RAACNFT.sol";
import {CurveCrvUSDVaultMock} from "./mocks/CurveCrvUSDVaultMock.m.sol";
import {crvUSDToken} from "../src/mocks/core/tokens/crvUSDToken.sol";
import {ERC20Mock} from "../src/mocks/core/tokens/ERC20Mock.sol";
import {ILendingPool} from "../src/interfaces/core/pools/LendingPool/ILendingPool.sol";
import {PercentageMath} from "../src/libraries/math/PercentageMath.sol";
import {WadRayMath} from "../src/libraries/math/WadRayMath.sol";
contract PoolsTest is Test {
using PercentageMath for uint256;
using WadRayMath for uint256;
// contracts
StabilityPool stabilityPool;
LendingPool lendingPool;
DEToken deToken;
RToken rToken;
DebtToken debtToken;
RAACToken raacToken;
RAACMinter raacMinter;
RAACHousePrices raacHousePrices;
RAACNFT raacNft;
CurveCrvUSDVaultMock curveCrvUsdVault;
crvUSDToken crvUsdToken;
ERC20Mock erc20Mock;
// owners
address STABILITY_POOL_OWNER = makeAddr("STABILITY_POOL_OWNER");
address LENDING_POOL_OWNER = makeAddr("LENDING_POOL_OWNER");
address DETOKEN_OWNER = makeAddr("DETOKEN_OWNER");
address RTOKEN_OWNER = makeAddr("RTOKEN_OWNER");
address DEBT_TOKEN_OWNER = makeAddr("DEBT_TOKEN_OWNER");
address RAAC_TOKEN_OWNER = makeAddr("RAAC_TOKEN_OWNER");
address RAAC_MINTER_DUMMY = makeAddr("RAAC_MINTER_DUMMY");
address RAAC_HOUSE_PRICES_OWNER = makeAddr("RAAC_HOUSE_PRICES_OWNER");
address RAAC_HOUSE_PRICES_ORACLE = makeAddr("RAAC_HOUSE_PRICES_ORACLE");
address CRV_USD_TOKEN_OWENR = makeAddr("CRV_USD_TOKEN_OWENR");
address CURVE_CRV_USD_VAULT_OWENR = makeAddr("CURVE_CRV_USD_VAULT_OWENR");
address ERC20_MOCK_TOKEN_OWNER = makeAddr("ERC20_MOCK_TOKEN_OWNER");
address RAAC_NFT_TOKEN_OWNER = makeAddr("RAAC_NFT_TOKEN_OWNER");
address RAAC_MINTER_OWNER = makeAddr("RAAC_MINTER_OWNER");
address RAAC_OWNER = makeAddr("RAAC_OWNER");
// users
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
// managers
address MANAGER_1 = makeAddr("MANAGER_1");
address MANAGER_2 = makeAddr("MANAGER_2");
address MANAGER_3 = makeAddr("MANAGER_3");
address MANAGER_4 = makeAddr("MANAGER_4");
address MANAGER_5 = makeAddr("MANAGER_5");
address MANAGER_6 = makeAddr("MANAGER_6");
address MANAGER_7 = makeAddr("MANAGER_7");
address MANAGER_8 = makeAddr("MANAGER_8");
address MANAGER_9 = makeAddr("MANAGER_9");
address MANAGER_10 = makeAddr("MANAGER_10");
uint256 initialPrimeRate = 5e27;
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
function setUp() public {
vm.warp(block.timestamp + 1 days + 1);
vm.startPrank(CRV_USD_TOKEN_OWENR);
crvUsdToken = new crvUSDToken(CRV_USD_TOKEN_OWENR);
vm.stopPrank();
vm.startPrank(CURVE_CRV_USD_VAULT_OWENR);
curveCrvUsdVault = new CurveCrvUSDVaultMock(address(crvUsdToken), CURVE_CRV_USD_VAULT_OWENR);
vm.stopPrank();
vm.startPrank(RTOKEN_OWNER);
rToken = new RToken("R_Token_V1", "RTKNV1", RTOKEN_OWNER, address(crvUsdToken));
vm.stopPrank();
vm.startPrank(DETOKEN_OWNER);
deToken = new DEToken("DE_Token_V1", "DETKNV1", DETOKEN_OWNER, address(rToken));
vm.stopPrank();
vm.startPrank(DEBT_TOKEN_OWNER);
debtToken = new DebtToken("DEBT_TOKEN_V1", "DEBTKNV1", DEBT_TOKEN_OWNER);
vm.stopPrank();
vm.startPrank(ERC20_MOCK_TOKEN_OWNER);
erc20Mock = new ERC20Mock("ERC20_MOCK_TOKEN", "ERC20MTKN");
vm.stopPrank();
vm.startPrank(RAAC_HOUSE_PRICES_OWNER);
raacHousePrices = new RAACHousePrices(RAAC_HOUSE_PRICES_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_NFT_TOKEN_OWNER);
raacNft = new RAACNFT(address(erc20Mock), address(raacHousePrices), RAAC_NFT_TOKEN_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_OWNER);
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.stopPrank();
vm.startPrank(LENDING_POOL_OWNER);
lendingPool = new LendingPool(
address(crvUsdToken),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
initialPrimeRate
);
vm.stopPrank();
vm.startPrank(STABILITY_POOL_OWNER);
stabilityPool = new StabilityPool(STABILITY_POOL_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER_OWNER);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), RAAC_MINTER_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(address(raacMinter));
vm.stopPrank();
vm.startPrank(LENDING_POOL_OWNER);
lendingPool.setCurveVault(address(curveCrvUsdVault));
vm.stopPrank();
vm.startPrank(RTOKEN_OWNER);
rToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.startPrank(DEBT_TOKEN_OWNER);
debtToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.startPrank(STABILITY_POOL_OWNER);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvUsdToken),
address(lendingPool)
);
vm.stopPrank();
crvUsdToken.mint(address(lendingPool), 1000_000_000e18);
}
function testUserDataRemainsUnUpdated() public {
uint256 tokenId = 1;
uint256 housePrice = 1000e18;
vm.startPrank(RAAC_HOUSE_PRICES_OWNER);
// set oracle
raacHousePrices.setOracle(RAAC_HOUSE_PRICES_ORACLE);
vm.stopPrank();
vm.startPrank(RAAC_HOUSE_PRICES_ORACLE);
// house price in USD
raacHousePrices.setHousePrice(tokenId, housePrice);
vm.stopPrank();
// ensure alice has sufficient balance
crvUsdToken.mint(ALICE, 1000e18);
vm.startPrank(ALICE);
crvUsdToken.approve(address(lendingPool), 1000e18);
// deposit 1000e18 into lending pool to have some liquidity
lendingPool.deposit(1000e18);
vm.stopPrank();
// alice pays in erc20 to mint nft
erc20Mock.mint(ALICE, housePrice);
vm.startPrank(ALICE);
erc20Mock.approve(address(raacNft), housePrice);
// mint 1 nft with tokenId 1
raacNft.mint(tokenId, housePrice);
vm.stopPrank();
vm.startPrank(ALICE);
raacNft.approve(address(lendingPool), tokenId);
// deposit nft into lending pool
lendingPool.depositNFT(tokenId);
vm.stopPrank();
(, uint256 scaledDebtBalance,,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After depositNFT... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
uint256 borrowAmount = 900e18;
vm.startPrank(ALICE);
// borrow 900e18
lendingPool.borrow(borrowAmount);
vm.stopPrank();
(, scaledDebtBalance,,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After borrow... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
uint256 aliceHealthFactor = lendingPool.calculateHealthFactor(ALICE);
assert(aliceHealthFactor < lendingPool.BASE_HEALTH_FACTOR_LIQUIDATION_THRESHOLD());
(, bool underLiquidation, uint256 liquidationStartTime) = lendingPool.userData(ALICE);
console.log("alice data before initiate liquidation... ");
console.log("alice underLiquidation status: ", underLiquidation);
console.log("alice liquidationStartTime: ", liquidationStartTime);
vm.startPrank(BOB);
lendingPool.initiateLiquidation(ALICE);
vm.stopPrank();
(, bool underLiquidationAfter, uint256 liquidationStartTimeAfter) = lendingPool.userData(ALICE);
console.log("alice data after initiate liquidation... ");
console.log("alice underLiquidation status: ", underLiquidationAfter);
console.log("alice liquidationStartTime: ", liquidationStartTimeAfter);
assertEq(underLiquidationAfter, underLiquidation);
assertEq(liquidationStartTimeAfter, liquidationStartTime);
}
}

How to Run the Test

  1. Create a Foundry Project:
    Open your terminal and run:

    forge init my-foundry-project
  2. Place Contract Files:
    Place all relevant contract files (e.g., LendingPool.sol, RAACNFT.sol, RAACHousePrices.sol, etc.) in the src directory of your project.

  3. Create Test Directory:
    Create a directory named test adjacent to the src directory, and add the test file (e.g., PoolsTest.t.sol) that contains the provided test suite code.

  4. Run the Test:
    In your terminal, execute:

    forge test --mt testUserDataRemainsUnUpdated -vv

    This command will run the specific test and display verbose output, allowing you to verify that the UserData struct is correctly updated after liquidation is initiated.

Impact

  • Inconsistent Liquidation Process:
    The failure to update the UserData struct means that downstream processes relying on these fields may not correctly recognize that a user is under liquidation. This can lead to incorrect behavior during liquidation finalization or other related operations.

  • Unexpected Behavior:
    The mismatch between global state and user-specific data may cause errors in the liquidation mechanics, potentially preventing the Stability Pool from finalizing liquidation and leaving the user in a locked state.

Tools Used

  • Manual Review

  • Foundry (Forge)

Recommendations

Update the initiateLiquidation function to synchronize the global state with the user's UserData struct. Specifically, set the corresponding fields within the UserData struct to reflect that the user is now under liquidation and record the initiation timestamp.

Recommended Diff

@@ In LendingPool.sol, function initiateLiquidation:
isUnderLiquidation[userAddress] = true;
liquidationStartTime[userAddress] = block.timestamp;
+ user.underLiquidation = true;
+ user.liquidationStartTime = block.timestamp;

By applying this change, the user data will remain consistent with the global state, ensuring that all functions referencing the UserData struct can reliably determine the user's liquidation status.

After implementing the recommended changes, rerun the test suite to confirm that the user data now reflects the correct liquidation status, ensuring that the liquidation process functions as intended.

Updates

Lead Judging Commences

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

LendingPool::initiateLiquidation updates global mappings but not UserData struct fields, creating inconsistent liquidation state

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

LendingPool::initiateLiquidation updates global mappings but not UserData struct fields, creating inconsistent liquidation state

Support

FAQs

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

Give us feedback!