Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refined Bone Bat - Unchecked Liquidation Outputs in liquidateBadDebt #237

Open
sherlock-admin4 opened this issue Dec 31, 2024 · 0 comments

Comments

@sherlock-admin4
Copy link

Refined Bone Bat

High

Unchecked Liquidation Outputs in liquidateBadDebt

Summary

The lack of post-liquidation collateral checks in liquidateBadDebt will cause protocol insolvency for the vault as an attacker will exploit collateral valuation changes to leave the vault undercollateralized and retain bad debt.

Root Cause

In NumaVault.sol:920, the liquidateBadDebt function assumes that the collateral recovered during liquidation is sufficient to cover the repaid debt. However, there is no explicit check to verify this assumption, leaving the vault exposed to bad debt if the collateral value drops significantly.
NumaVault.sol:920:
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L920

NumaVault.sol:947-948:
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L947-L948

NumaVault.sol:950-954:
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L950-L954

  • No check ensures the received collateral is sufficient to cover the liquidated debt.

Internal pre-conditions

1-The borrower must have an outstanding debt (borrowAmountFull > 0).
2-The _percentagePosition1000 parameter must allow partial liquidation.
3-The attacker must have sufficient underlyingBorrow tokens to repay the debt.

External pre-conditions

1-The price of the collateral token must drop significantly (e.g., due to market volatility or oracle manipulation).
2-The collateral token must be redeemable for less than the repaid debt value during liquidation.

Attack Path

1-Create a Vulnerable Borrower Position:

  • The attacker or a third party opens a position with just enough collateral to meet the liquidation threshold.

2-Trigger a Collateral Value Drop:

  • The attacker manipulates the oracle price to deflate the collateral’s value or waits for natural market volatility to achieve the same effect.

3-Execute a Partial Liquidation:

  • The attacker calls liquidateBadDebt with _percentagePosition1000 set to a high value (e.g., 900/1000), ensuring significant collateral is redeemed.
  • The collateral redeemed (received) is insufficient to cover the debt repaid (repayAmount), leaving the vault with bad debt.

4-Retain Bad Debt:

  • Post-liquidation, the vault retains undercollateralized debt, effectively leading to protocol insolvency.

Impact

Affected Party: The vault and its users.
Loss: Protocol insolvency due to retained bad debt, making it impossible for the vault to meet future obligations.

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "forge-std/Test.sol";
import "../contracts/NumaVault.sol";
import "../contracts/mocks/MockOracle.sol";
import "../contracts/mocks/MockToken.sol";
import "../contracts/mocks/MockLendingProtocol.sol";

contract NumaVaultLiquidationTest is Test {
    NumaVault vault;
    MockOracle oracle;
    MockToken lstToken;
    MockToken numaToken;
    MockLendingProtocol lendingProtocol;
    address borrower = address(0x123);
    address liquidator = address(this);

    function setUp() public {
        // Deploy mock dependencies
        lstToken = new MockToken("LST Token", "LST", 18);
        numaToken = new MockToken("NUMA Token", "NUMA", 18);
        oracle = new MockOracle(1e18); // Initial price of 1
        lendingProtocol = new MockLendingProtocol(address(lstToken), address(numaToken));
        vault = new NumaVault(
            address(numaToken),
            address(lstToken),
            18,
            address(oracle),
            liquidator,
            0,
            0
        );

        // Fund vault and borrower
        lstToken.mint(address(vault), 1_000e18);
        lstToken.mint(borrower, 500e18);

        // Borrower opens a position
        lendingProtocol.borrow(borrower, 300e18, address(lstToken));
    }

    function testUncheckedLiquidation() public {
        // Manipulate oracle price to deflate collateral value
        oracle.setPrice(0.1e18); // Set price to 0.1 (90% drop)

        // Liquidator executes partial liquidation
        vault.liquidateBadDebt(borrower, 900, lendingProtocol.getCollateralToken());

        // Verify bad debt retained
        uint256 vaultDebt = vault.getDebt();
        console.log("Vault Debt Post-Liquidation:", vaultDebt);
        assertGt(vaultDebt, 0, "Vault should retain bad debt");
    }
}

Results

  • Borrower debt is 300 LST; vault collateral covers this amount.
  • After Exploit: Oracle price drops by 90%, and partial liquidation redeems insufficient collateral. Vault retains 200 LST of bad debt.

Mitigation

1- Enforce Collateral Sufficiency Checks:

  • After liquidation, validate that the collateral recovered (received) is sufficient to cover the repaid debt (repayAmount):
    require(received >= repayAmount, "Insufficient collateral recovered");

2- Cap Liquidation Size:

  • Limit _percentagePosition1000 to prevent excessive liquidations that could destabilize the vault.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant