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.
User opens a position: A user opens a new position by calling the isSolventAfterOpenPosition function.
Solvency check: The function performs a solvency check based on the available balances after adjusting for the position’s profit or loss.
Inaccurate solvency status: Due to the omission of locked balance adjustments, the function may incorrectly determine that the user is solvent.
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:
User can select a quote with a high leverage ratio, where even small market fluctuations could significantly affect profitability.
Submit artificially inflated or deflated market prices through rapid transactions to create misleading profit/loss scenarios.
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
Users may lose their collateral and incur liquidation fees.
The platform may experience increased liquidation events, leading to instability and loss of trust among users.
PoC
Deploy the smart contract: Deploy the contract containing the isSolventAfterOpenPosition function.
Initialize balances: Set up initial balances for partyA and partyB.
Open a position: Call the isSolventAfterOpenPosition function to open a position with specific quoteIds, filledAmounts, and marketPrices.
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:
The TestSolvency contract includes the isSolventAfterOpenPosition function and a testImmediateInsolvency function to demonstrate the issue.
Initial balances are set for two parties.
Two quotes are set up, one LONG and one SHORT.
The testImmediateInsolvency function attempts to open positions with specific quoteIds, filledAmounts, and marketPrices.
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.
safdie
High
Immediate insolvency vulnerability in
isSolventAfterOpenPosition
function inLibSolvency.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
isSolventAfterOpenPosition function
.Also:
Impact
PoC
isSolventAfterOpenPosition
function.partyA
andpartyB
.isSolventAfterOpenPosition
function to open a position with specificquoteIds
,filledAmounts
, andmarketPrices
.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.Explanation:
The TestSolvency
contract includes theisSolventAfterOpenPosition
function and atestImmediateInsolvency
function to demonstrate the issue.LONG
and oneSHORT
.testImmediateInsolvency
function attempts to open positions with specificquoteIds
, filledAmounts, andmarketPrices
.require
statement intestImmediateInsolvency
will fail if theisSolventAfterOpenPosition
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.