sherlock-audit / 2024-09-symmio-v0-8-4-update-contest-judging

0 stars 0 forks source link

safdie - Immediate insolvency vulnerability in `isSolventAfterOpenPosition` function in `LibSolvency.sol` #30

Open sherlock-admin3 opened 1 week ago

sherlock-admin3 commented 1 week ago

safdie

High

Immediate insolvency vulnerability in isSolventAfterOpenPosition function in LibSolvency.sol

Summary

The isSolventAfterOpenPosition function in the smart contract has a critical vulnerability that can lead to immediate insolvency after opening a position. This issue arises due to an inaccurate solvency check that does not account for locked balance adjustments, potentially causing users to lose their collateral value adjustment (CVA) and liquidation fees.

Root Cause

The root cause of this vulnerability is the failure to consider the locked balance adjustments when performing the solvency check. The function only checks the available balances after adjusting for the position’s profit or loss, without accounting for the locked balances that should be reserved for collateral and fees. https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibSolvency.sol#L26-L70

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. User opens a position: A user opens a new position by calling the isSolventAfterOpenPosition function.
  2. Solvency check: The function performs a solvency check based on the available balances after adjusting for the position’s profit or loss.
  3. Inaccurate solvency status: Due to the omission of locked balance adjustments, the function may incorrectly determine that the user is solvent.
  4. Immediate insolvency: The user becomes immediately insolvent as the actual locked balances are not sufficient to cover the position, leading to potential liquidation and loss of collateral.

Also:

  1. User can select a quote with a high leverage ratio, where even small market fluctuations could significantly affect profitability.
  2. Submit artificially inflated or deflated market prices through rapid transactions to create misleading profit/loss scenarios.
  3. Open and close positions rapidly, presenting a façade of being solvent while actually being in a negative balance. Example code snippet demonstrating manipulated price submission:
    // Example of submitting manipulated prices
    uint256[] memory quoteIds = [1];
    uint256[] memory filledAmounts = [100];
    uint256[] memory closedPrices = [120]; // Artificially high price
    uint256[] memory marketPrices = [100]; // Actual market price

Impact

  1. Users may lose their collateral and incur liquidation fees.
  2. The platform may experience increased liquidation events, leading to instability and loss of trust among users.

PoC

  1. Deploy the smart contract: Deploy the contract containing the isSolventAfterOpenPosition function.
  2. Initialize balances: Set up initial balances for partyA and partyB.
  3. Open a position: Call the isSolventAfterOpenPosition function to open a position with specific quoteIds, filledAmounts, and marketPrices.
  4. Observe insolvency: Verify that the function incorrectly determines solvency and leads to immediate insolvency upon opening the position.

For example, here’s a simple test contract to demonstrate the immediate insolvency risk in the isSolventAfterOpenPosition function. This script will deploy the contract, set up initial balances, and attempt to open a position, showing how the function can lead to insolvency.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TestSolvency {
    enum PositionType { LONG, SHORT }

    struct Quote {
        PositionType positionType;
        uint256 openedPrice;
    }

    mapping(uint256 => Quote) public quotes;
    mapping(address => uint256) public balances;

    function isSolventAfterOpenPosition(
        uint256[] memory quoteIds,
        uint256[] memory filledAmounts,
        uint256[] memory marketPrices,
        int256 upnlPartyB,
        int256 upnlPartyA,
        address partyB,
        address partyA
    ) public view returns (bool) {
        int256 partyBAvailableBalance = int256(balances[partyB]) + upnlPartyB;
        int256 partyAAvailableBalance = int256(balances[partyA]) + upnlPartyA;

        for (uint8 i = 0; i < quoteIds.length; i++) {
            uint256 quoteId = quoteIds[i];
            uint256 filledAmount = filledAmounts[i];
            uint256 marketPrice = marketPrices[i];
            Quote storage quote = quotes[quoteId];

            if (quote.positionType == PositionType.LONG) {
                if (quote.openedPrice >= marketPrice) {
                    uint256 diff = (filledAmount * (quote.openedPrice - marketPrice)) / 1e18;
                    partyAAvailableBalance -= int256(diff);
                    partyBAvailableBalance += int256(diff);
                } else {
                    uint256 diff = (filledAmount * (marketPrice - quote.openedPrice)) / 1e18;
                    partyBAvailableBalance -= int256(diff);
                    partyAAvailableBalance += int256(diff);
                }
            } else if (quote.positionType == PositionType.SHORT) {
                if (quote.openedPrice >= marketPrice) {
                    uint256 diff = (filledAmount * (quote.openedPrice - marketPrice)) / 1e18;
                    partyBAvailableBalance -= int256(diff);
                    partyAAvailableBalance += int256(diff);
                } else {
                    uint256 diff = (filledAmount * (marketPrice - quote.openedPrice)) / 1e18;
                    partyAAvailableBalance -= int256(diff);
                    partyBAvailableBalance += int256(diff);
                }
            }
        }

        require(partyBAvailableBalance >= 0 && partyAAvailableBalance >= 0, "LibSolvency: Available balance is lower than zero");
        return true;
    }

    function testImmediateInsolvency() public {
        // Set up initial balances
        balances[msg.sender] = 1000 ether;
        balances[address(0x1)] = 1000 ether;

        // Set up quotes
        quotes[1] = Quote(PositionType.LONG, 2000 ether);
        quotes[2] = Quote(PositionType.SHORT, 2000 ether);

        // Attempt to open a position
        uint256[] memory quoteIds = new uint256;
        quoteIds[0] = 1;
        quoteIds[1] = 2;

        uint256[] memory filledAmounts = new uint256;
        filledAmounts[0] = 500 ether;
        filledAmounts[1] = 500 ether;

        uint256[] memory marketPrices = new uint256;
        marketPrices[0] = 1500 ether;
        marketPrices[1] = 2500 ether;

        // This should fail due to immediate insolvency
        require(isSolventAfterOpenPosition(quoteIds, filledAmounts, marketPrices, 0, 0, address(0x1), msg.sender), "Test failed: Immediate insolvency detected");
    }
}

Explanation:

  1. The TestSolvency contract includes the isSolventAfterOpenPosition function and a testImmediateInsolvency function to demonstrate the issue.
  2. Initial balances are set for two parties.
  3. Two quotes are set up, one LONG and one SHORT.
  4. The testImmediateInsolvency function attempts to open positions with specific quoteIds, filledAmounts, and marketPrices.
  5. The require statement in testImmediateInsolvency will fail if the isSolventAfterOpenPosition function leads to immediate insolvency.

Mitigation

Modify the solvency check to include locked balance adjustments, ensuring that the function accurately reflects the actual locked balances.