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

0 stars 0 forks source link

Issue M-1: settleUpnl function can be DOSed by other PartyBs/hedgers

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/39

Found by

xiaoming90

Summary

settleUpnl function can be DOSed. Users will not be able to close their positions, which could lead to a loss to the users due to the inability to close their positions. In the report's example, PartyA wants to close its losing position to stop its account from incurring further losses due to unfavorable market conditions. However, since PartyA is unable to close its losing position, PartyA continues to incur more losses and might eventually be liquidated. Furthermore, trading is a time-sensitive activity. Thus, any blockage or delay would eventually lead to a loss of assets.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Assume the following scenario:

PartyA wants to close its losing position to stop its account from incurring further losses due to unfavorable market conditions.

However, the close position transaction cannot go through due to an insufficient allocated balance on PartyA's account. This is a similar scenario described in the Symm IO v0.8.4 documentation.

In order for PartyB_1 to close PartyA's position, it first has to execute the settleUpnl function to settle PartyA's unrealized profit so that PartyA's allocated balance will be "refilled" with more funds. Once there are sufficient funds in PartyA's allocated balance, PartyB_1 can proceed to fulfill PartyA's closing request.

The only option to increase the PartyA's allocated balance is to close the PartyA's winning position with PartyB_2.

When the settleUpnl function is executed, it will verify the signature provided by Muon via the verifySettlement function below. Line 20 below shows that the encoded data in the signature includes the nonce of the PartyBs of the positions/quotes where the PnL is to be settled. In this case, PartyB_2's nonce will be included in the signature.

Assume that the time when PartyB_1 fetches the signature is T0. At T0, PartyB_2's nonce is 890. Thus, the signature is signed against encoded data where PartyB_2's nonce is 890.

At T1, PartyB_1 executes the settleUpnl function along with the signature. However, PartyB_2 could front-run the transaction and perform certain actions (e.g., open/close position, charge funding rate) that will increment its own nonce to 891. In this case, the nonce in PartyB_1's signature and PartyB_2's nonce will differ, and the settleUpnl transaction will revert. PartyB_2 could repeat this continuously DOS or block PartyB_1 attempts to settle UPNL and close PartyA's positions.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/muon/LibMuonSettlement.sol#L12

File: LibMuonSettlement.sol
12:     function verifySettlement(SettlementSig memory settleSig, address partyA) internal view {
13:         MuonStorage.Layout storage muonLayout = MuonStorage.layout();
14:         // == SignatureCheck( ==
15:         require(block.timestamp <= settleSig.timestamp + muonLayout.upnlValidTime, "LibMuon: Expired signature");
16:         // == ) ==
17:         bytes memory encodedData;
18:         uint256[] memory nonces = new uint256[](settleSig.quotesSettlementsData.length);
19:         for (uint8 i = 0; i < settleSig.quotesSettlementsData.length; i++) {
20:             nonces[i] = AccountStorage.layout().partyBNonces[QuoteStorage.layout().quotes[settleSig.quotesSettlementsData[i].quoteId].partyB][partyA];
21:             encodedData = abi.encodePacked(
22:                 encodedData,  // Append the previously encoded data
23:                 settleSig.quotesSettlementsData[i].quoteId,
24:                 settleSig.quotesSettlementsData[i].currentPrice,
25:                 settleSig.quotesSettlementsData[i].partyBUpnlIndex
26:             );
27:         }
28:         bytes32 hash = keccak256(
29:             abi.encodePacked(
30:                 muonLayout.muonAppId,
31:                 settleSig.reqId,
32:                 address(this),
33:                 "verifySettlement",
34:                 nonces,
35:                 AccountStorage.layout().partyANonces[partyA],
..SNIP..
44:     }

Impact

Users will not be able to close their positions, which could lead to a loss to the users due to the inability to close their positions. In the above example, PartyA wants to close its losing position to stop its account from incurring further losses due to unfavorable market conditions. However, since PartyA is unable to close its losing position, PartyA continues to incur more losses and might eventually be liquidated. Furthermore, trading is a time-sensitive activity. Thus, any blockage or delay would eventually lead to a loss of assets.

PoC

No response

Mitigation

No response

Issue M-2: Unauthorized PartyB could settle PNL of other PartyBs and users in the system

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/40

The protocol has acknowledged this issue.

Found by

xiaoming90

Summary

The settleUpnl function does not include notPartyB modifier. As a result, unauthorized PartyB could settle PNL of other PartyBs and users in the system, causing disruption and breaking core protocol functionality. Unauthorized PartyB can prematurely settle PartyB's (victim) positions prematurely at times that are disadvantageous to PartyB, resulting in asset loss for them.

For example, in a highly volatile market, if PartyB’s positions temporarily incur a loss due to sudden market fluctuations, the Unauthorized PartyB could immediately settle these positions before the market has a chance to recover. This premature settlement forces PartyB to realize losses that might have otherwise been avoided if the positions had remained open.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

The settleUpnl function can only be accessed by PartyB, as per the comment below. However, the function is not guarded by the notPartyB modifier.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/facets/Settlement/SettlementFacet.sol#L26

File: SettlementFacet.sol
16:     /**
17:      * @notice Allows Party B to settle the upnl of party A position for the specified quotes.
18:      * @param settlementSig The data struct contains quoteIds and upnl of parties and market prices
19:      * @param updatedPrices New prices to be set as openedPrice for the specified quotes.
20:      * @param partyA Address of party A
21:      */
22:     function settleUpnl(
23:         SettlementSig memory settlementSig,
24:         uint256[] memory updatedPrices,
25:         address partyA
26:     ) external whenNotPartyBActionsPaused notLiquidatedPartyA(partyA) {
27:         uint256[] memory newPartyBsAllocatedBalances = SettlementFacetImpl.settleUpnl(settlementSig, updatedPrices, partyA);
..SNIP..
35:     }

Instead, it depends on the quoteLayout.partyBOpenPositions[msg.sender][partyA].length > 0 at Line 31 below, which is not a reliable method to determine whether the caller is a valid PartyB. The reason is that it is possible that a PartyB (e.g., one that might be removed due to malicious activities) that has already been removed from the system still has residual open positions. In this case, the position's length check will pass, and the unauthorized PartyB could continue to settle the PNL of other PartyBs and users in the system.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibSettlement.sol#L24

File: LibSettlement.sol
15:     function settleUpnl(
..SNIP..
30:         require(
31:             isForceClose || quoteLayout.partyBOpenPositions[msg.sender][partyA].length > 0,
32:             "LibSettlement: Sender should have a position with partyA"
33:         );

Impact

Unauthorized PartyB could settle PNL of other PartyBs and users in the system, causing disruption and breaking core protocol functionality. Unauthorized PartyB can prematurely settle PartyB's (victim) positions prematurely at times that are disadvantageous to PartyB, resulting in asset loss for them.

For example, in a highly volatile market, if PartyB’s positions temporarily incur a loss due to sudden market fluctuations, the Unauthorized PartyB could immediately settle these positions before the market has a chance to recover. This premature settlement forces PartyB to realize losses that might have otherwise been avoided if the positions had remained open.

PoC

No response

Mitigation

Include the notPartyB modifier to the settleUpnl function.

Discussion

MoonKnightDev

the check in the libSettlement.sol file prevents the scenario mentioned: https://github.com/SYMM-IO/protocol-core/blob/eac73bf1d97df96bcd5b19bcc972792ef96c70e1/contracts/libraries/LibSettlement.sol#L30

Issue M-3: Force Close can be DOSed by exploiting settleUpnl function

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/42

Found by

xiaoming90

Summary

PartyB can abuse the newly implemented settleUpnl function to increment the nonce to block PartyA's force close action.

Root Cause

  1. A SettlementSig signature with quotesSettlementsData and/or partyBs array being empty can be passed into settleUpnl function
  2. No cooldown if PartyB is settling its own positions
  3. No minimum PnL to be settled (Even 1 wei of PnL can be settled)

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

This report is similar to the report from the previous contest, but with a different attack vector.

Assume that PartyA requests to close a quote via requestToClosePosition function. If PartyB does not respond within the cooldown period, PartyA can call the PartyAFacetImpl.forceClosePosition to close the quote forcefully.

Within the forceClosePosition function:

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/facets/ForceActions/ForceActionsFacetImpl.sol#L86

File: ForceActionsFacetImpl.sol
51:     function forceClosePosition(
52:         uint256 quoteId,
53:         HighLowPriceSig memory sig,
54:         SettlementSig memory settlementSig,
55:         uint256[] memory updatedPrices
56:     ) internal returns (uint256 closePrice, bool isPartyBLiquidated, int256 upnlPartyB, uint256 partyBAllocatedBalance) {
..SNIP..
86:         LibMuonForceActions.verifyHighLowPrice(sig, quote.partyB, quote.partyA, quote.symbolId);
87:         if (updatedPrices.length > 0) {
88:             LibMuonSettlement.verifySettlement(settlementSig, quote.partyA);
89:         }

Attack Path 1 - Incrementing PartyA's nonce

The following describes the attack path:

  1. At this point, PartyA's nonce is 200. PartyA fetches the signatures from Muon and bundles them with the forceClosePosition transaction, and submits to the mempool to close the quote forcefully as PartyB did not respond to PartyA's close request for an extended period.
  2. PartyB front-run PartyA's transaction and exploit the new settleUpnl function to increment its nonce to 201.
  3. When PartyA's transaction gets executed, since the signature's PartyA nonce is 200, while the PartyA's nonce on-chain is 201, the transaction will revert.
  4. Party B could continuously grieve Party A, preventing them from being forced to close their positions.

There are several ways that PartyB can increment the PartyA's nonce via the settleUpnl function with no cost or minimum cost:

  1. Passing a SettlementSig signature with quotesSettlementsData and/or partyBs array being empty. In this case, the code at Line 34 will increment PartyA's nonce, but the rest of the code that involves the for-loop will be bypassed since the array (settleSig.quotesSettlementsData.length = 0) is empty

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibSettlement.sol#L40

File: LibSettlement.sol
15:     function settleUpnl(
16:         SettlementSig memory settleSig,
..SNIP..
34:         accountLayout.partyANonces[partyA] += 1;
..SNIP..
40:         for (uint8 i = 0; i < settleSig.quotesSettlementsData.length; i++) {
  1. The tricks mentioned in "Attack Path 2" section below for incrementing PartyB's nonce can also be applied here to increment PartyA's nonce. Refer to the next section for more details.

Attack Path 2 - Incrementing PartyB's nonce

The following describes the attack path:

  1. At this point, PartyB's nonce is 800. PartyA fetches the signatures from Muon and bundles them with the forceClosePosition transaction, and submits to the mempool to close the quote forcefully as PartyB did not respond to PartyA's close request for an extended period.
  2. PartyB front-run PartyA's transaction and exploit the new settleUpnl function to increment its nonce to 801.
  3. When PartyA's transaction gets executed, since the signature's PartyB nonce is 800, while the PartyB's nonce on-chain is 801, the transaction will revert.
  4. Party B could continuously grieve Party A, preventing them from being forced to close their positions.

There are several ways that PartyB can increment its own nonce via the settleUpnl function with no cost or minimum cost:

  1. PartyA is permissionless. PartyB can create a PartyA account and open some dummy positions with its account. When PartyB needs to increment its nonce, simply settle a minimum possible PnL between these two accounts. There is no cooldown since PartyB is settling its own positions.
  2. If PartyB already has existing positions, PartyB can settle a minimum possible PnL on these positions. There is no cooldown since PartyB is settling its own positions.

Impact

This impact is the same as the impact of a report in the previous Symm IO contest. Thus, the risk rating should be aligned to Medium.

PartyB can take advantage of it against PartyA, making themselves always profitable. For instance:

  1. If the current price goes for PartyB, then the quote is closed, and PartyB makes a profit.
  2. If the current price goes against PartyB, PartyB can front-run forceClosePosition and call settleUpnl to increase PartyA's nonces. In this way, PartyA's forceClosePosition will inevitably revert because the nonces are incorrect.

In addition, when PartyA cannot close its position promptly, it exposes PartyA to unnecessary market risks and potential losses and gives PartyB an unfair advantage by giving PartyB an opportunity to turn the table (e.g., loss -> profit).

PoC

No response

Mitigation

Consider implementing the following measures to mitigate the root causes:

  1. Ensure that SettlementSig signature with quotesSettlementsData and/or partyBs array being empty are rejected within settleUpnl function
  2. Implement some cooldown even if PartyB is settling its own positions
  3. Implement a minimum PnL to be settled so that no one can attempt to abuse the settle upnl feature by only settling 1 wei or small amount of PnL

Issue M-4: PartyB can settle PartyA’s losing positions even when it is unnecessary and detrimental to Party A’s allocated balance

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/46

The protocol has acknowledged this issue.

Found by

xiaoming90

Summary

PartyB can settle PartyA’s losing positions, even when it is unnecessary and only further decreases PartyA's allocated balance. This unnecessary and premature settling causes PartyA to realize losses at unfavorable market conditions and prices, without giving them the opportunity to react or potentially recover from the temporary downturn. As a result, Party A is unfairly exposed to significant financial harm, having their positions closed at the worst possible moments, leading to a loss of assets for them.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Per Symm IO v0.8.3 documentation, the purpose of the newly implemented settleUpnl function is as follows:

allows hedgers to settle a portion of the user's unrealized PNL, effectively converting it into realized PNL. This enables the hedger to fill the user's close requests, even when the user lacks sufficient allocated balance.

This new feature is intended to solve the problem where a hedger cannot fill PartyA's close request because PartyA does not have enough allocated balance, as described in the documentation's example. As shown in the documentation's example, only PartyA's winning positions (those with positive PnL) need to be settled. Once these winning positions are settled, PartyA's allocated balance will increase, and there will be sufficient allocated balance in PartyA's account to execute PartyA's close request for its losing positions.

With that in mind, it does not make sense to allow hedgers to settle the losing positions of PartyA under any circumstance because:

However, it was observed that the settleUpnl allows hedgers to settle the losing positions of PartyA.

Assume that PartyA's LONG position with symbol=ETH, quote.openedPrice = 3000 USDC, quote.quantity = 1, and data.currentPrice = 2000 USC. This position is losing position for PartyA as the ETH's price dropped after the position was opened. However, none of the price validation checks at Lines 57-67 below prevents PartyB from settling a losing position of PartyA.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibSettlement.sol#L57

File: LibSettlement.sol
15:     function settleUpnl(
..SNIP..
57:             if (quote.openedPrice > data.currentPrice) {
58:                 require(
59:                     updatedPrices[i] < quote.openedPrice && updatedPrices[i] >= data.currentPrice,
60:                     "LibSettlement: Updated price is out of range"
61:                 );
62:             } else {
63:                 require(
64:                     updatedPrices[i] > quote.openedPrice && updatedPrices[i] <= data.currentPrice,
65:                     "LibSettlement: Updated price is out of range"
66:                 );
67:             }
68:             if (quote.positionType == PositionType.LONG) {
69:                 settleAmounts[data.partyBUpnlIndex] +=
70:                     ((int256(updatedPrices[i]) - int256(quote.openedPrice)) * int256(LibQuote.quoteOpenAmount(quote))) /
71:                     1e18;
72:             } else {
73:                 settleAmounts[data.partyBUpnlIndex] +=
74:                     ((int256(quote.openedPrice) - int256(updatedPrices[i])) * int256(LibQuote.quoteOpenAmount(quote))) /
75:                     1e18;
76:             }
77:             quote.openedPrice = updatedPrices[i];

Impact

PartyB can settle PartyA’s losing positions, even when it is unnecessary and only further decreases PartyA's allocated balance. This unnecessary and premature settling causes PartyA to realize losses at unfavorable market conditions and prices, without giving them the opportunity to react or potentially recover from the temporary downturn. As a result, Party A is unfairly exposed to significant financial harm, having their positions closed at the worst possible moments, leading to a loss of assets for them.

PoC

No response

Mitigation

Update the settleUpnl function to ensure that only positions/quotes that increase the target account's allocated balance can be settled. In this report's example, PartyB should only be allowed to settle PartyA's winning positions, as only this settlement will increase PartyA's allocated balance.

Discussion

MoonKnightDev

There is no issue with settling positions that are in a loss. Settling a position means that the unrealized P&L (uPnL) is converted into realized P&L (PnL) for the parties involved, without affecting the trading rules. On the other hand, Party Bs may also need to realize their profits

Issue M-5: An excessive number of user positions can be settled even if the shortfall allocated balance needed for fulfilling the close request is only a small amount

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/47

The protocol has acknowledged this issue.

Found by

xiaoming90

Summary

An excessive number of user positions can be settled even if the shortfall allocated balance needed for fulfilling the close request is only a small amount. As a result, victim's losses will be realized prematurely at unfavorable market conditions and prices, without giving them the opportunity to react or potentially recover from the temporary downturn. As a result, the victim is unfairly exposed to significant financial harm, having their positions closed at the worst possible moments, leading to a loss of assets for them. In this scenario, the victims are Symm Hedger (Party B1), Capital Hedger (Party B2), and Rasa Hedger (Party B3) in the report.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Assuming a similar scenario mentioned in the Symm IO v0.8.3 documentation.

Bob (Party A) has five (5) open positions and zero allocated balance:

  1. Position 1 with Symm Hedger (Party B1): Unrealized Profit: $300
  2. Position 2 with Capital Hedger (Party B2): Unrealized Profit: $300
  3. Position 3 with Rasa Hedger (Party B3): Unrealized Profit: $200
  4. Position 4 with PerpsHub Hedger (Party B4): Unrealized Profit: $200
  5. Position 5 with PerpsHub Hedger (Party B4): Unrealized Loss: $250

Bob wants to close Position 5, which has an unrealized loss of \$250. However, since Bob has zero allocated balance, he cannot cover this loss upon closing.

PerpsHub Hedger (Party B4) cannot fill Bob's close request for Position 5 because Bob doesn't have enough allocated balance to cover the \$250 loss. Even though Bob has an overall positive UPNL (\$750 net profit), his profits are unrealized and cannot be used to settle the loss directly.

Thus, the solution provided by the newly implemented settleUpnl feature is to realized the \$200 profit for Position 4 and \$50 profit for Position 3. Settling these PnLs will increase Bob's allocated balance by \$250. PerpsHub can now successfully fill the close request for Position 5, as Bob has sufficient allocated balance (\$250) to cover the loss (\$250).

However, the issue is that within the settleUpnl function, there is no limit on how many positions the hedger/PartyB (PerpsHub) can settle the user's positions. It can effectively settle all of the user's positions (Position 1-5) even if the shortfall allocated balance needed for fulfilling the close request is only a small amount (\$250 shortfall), resulting in an excessive amount of PnL being settled for no good reason. In this case, the hedger can go ahead and settle the entire $1000 profit instead of only the \$250 that is required.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibSettlement.sol#L42

File: LibSettlement.sol
15:     function settleUpnl(
16:         SettlementSig memory settleSig,
17:         uint256[] memory updatedPrices,
18:         address partyA,
19:         bool isForceClose
20:     ) internal returns (uint256[] memory newPartyBsAllocatedBalances) {
..SNIP..
40:         for (uint8 i = 0; i < settleSig.quotesSettlementsData.length; i++) {
41:             QuoteSettlementData memory data = settleSig.quotesSettlementsData[i];
42:             Quote storage quote = quoteLayout.quotes[data.quoteId];
43:             require(quote.partyA == partyA, "LibSettlement: PartyA is invalid");
..SNIP..
68:             if (quote.positionType == PositionType.LONG) {
69:                 settleAmounts[data.partyBUpnlIndex] +=
70:                     ((int256(updatedPrices[i]) - int256(quote.openedPrice)) * int256(LibQuote.quoteOpenAmount(quote))) /
71:                     1e18;
72:             } else {
73:                 settleAmounts[data.partyBUpnlIndex] +=
74:                     ((int256(quote.openedPrice) - int256(updatedPrices[i])) * int256(LibQuote.quoteOpenAmount(quote))) /
75:                     1e18;
76:             }
77:             quote.openedPrice = updatedPrices[i];
78:         }

Impact

An excessive number of user positions can be settled even if the shortfall allocated balance needed for fulfilling the close request is only a small amount. As a result, victim's losses will be realized prematurely at unfavorable market conditions and prices, without giving them the opportunity to react or potentially recover from the temporary downturn. As a result, the victim is unfairly exposed to significant financial harm, having their positions closed at the worst possible moments, leading to a loss of assets for them. In this scenario, the victims are Symm Hedger (Party B1), Capital Hedger (Party B2), and Rasa Hedger (Party B3) in the report.

PoC

No response

Mitigation

Consider limiting the maximum amount of profits that the hedgers can settle to the shortfall. In this example, the shortfall is \$250. Thus, the hedgers should not be allowed to settle more than \$250 to prevent excessive settlement of PartyA's PnL.

This issue also applied to the other side, where Party A needed to settle Party B's PnL during force close. Thus, the relevant mitigation also needs to be applied for this.

Discussion

MoonKnightDev

There is no issue with settling more positions than needed. The settling of a position means that the unrealized P&L (uPnL) becomes realized P&L (PnL) for the parties involved, and it does not affect the trading rules.

Issue M-6: Emergency close might be blocked due to insufficient allocated balance

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/50

The protocol has acknowledged this issue.

Found by

xiaoming90

Summary

Emergency close might be blocked due to insufficient PartyA's allocated balance. During urgent situations where emergency mode is activated, the positions need to be promptly closed to avoid negative events that could potentially lead to serious loss of funds (e.g. the protocol is compromised, and the attacker is planning to or has started draining funds from the protocols). However, if the emergency closure of positions is blocked or delayed due to the above-mentioned issue, it might lead to unrecoverable losses.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Assuming a similar scenario mentioned in the Symm IO v0.8.3 documentation.

Bob (Party A) has three open positions and zero allocated balance:

  1. Position 1 with Rasa Hedger (Party B1):
    • Unrealized Profit: $300
    • Quote ID: 1
  2. Position 2 with PerpsHub Hedger (Party B2):
    • Unrealized Profit: $100
    • Quote ID: 2
  3. Position 3 with PerpsHub Hedger (Party B2):
    • Unrealized Loss: $250
    • Quote ID: 3

PartyB wants to emergency close Bob's Position 3 via the emergencyClosePosition function, which has an unrealized loss of $250. However, since Bob has zero allocated balance, the loss in Position 3 cannot cover this loss upon closing, and an attempt to emergency close will revert.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/facets/PartyBPositionActions/PartyBPositionActionsFacetImpl.sol#L99

File: PartyBPositionActionsFacetImpl.sol
099:    function emergencyClosePosition(uint256 quoteId, PairUpnlAndPriceSig memory upnlSig) internal {
100:        AccountStorage.Layout storage accountLayout = AccountStorage.layout();
101:        Quote storage quote = QuoteStorage.layout().quotes[quoteId];
102:        Symbol memory symbol = SymbolStorage.layout().symbols[quote.symbolId];
103:        require(
104:            GlobalAppStorage.layout().emergencyMode || GlobalAppStorage.layout().partyBEmergencyStatus[quote.partyB] || !symbol.isValid,
105:            "PartyBFacet: Operation not allowed. Either emergency mode must be active, party B must be in emergency status, or the symbol must be delisted"
106:        );
107:        require(quote.quoteStatus == QuoteStatus.OPENED || quote.quoteStatus == QuoteStatus.CLOSE_PENDING, "PartyBFacet: Invalid state");
108:        LibMuonPartyB.verifyPairUpnlAndPrice(upnlSig, quote.partyB, quote.partyA, quote.symbolId);
109:        uint256 filledAmount = LibQuote.quoteOpenAmount(quote);
110:        quote.quantityToClose = filledAmount;
111:        quote.requestedClosePrice = upnlSig.price;
112:        require(
113:            LibAccount.partyAAvailableBalanceForLiquidation(upnlSig.upnlPartyA, accountLayout.allocatedBalances[quote.partyA], quote.partyA) >= 0,
114:            "PartyBFacet: PartyA is insolvent"
115:        );
116:        require(
117:            LibAccount.partyBAvailableBalanceForLiquidation(upnlSig.upnlPartyB, quote.partyB, quote.partyA) >= 0,
118:            "PartyBFacet: PartyB should be solvent"
119:        );
120:        accountLayout.partyBNonces[quote.partyB][quote.partyA] += 1;
121:        accountLayout.partyANonces[quote.partyA] += 1;
122:        LibQuote.closeQuote(quote, filledAmount, upnlSig.price);
123:    }

Impact

During urgent situations where emergency mode is activated, the positions need to be promptly closed to avoid negative events that could potentially lead to serious loss of funds (e.g. the protocol is compromised, and the attacker is planning to or has started draining funds from the protocols). However, if the emergency closure of positions is blocked or delayed due to the above-mentioned issue, it might lead to unrecoverable losses.

PoC

No response

Mitigation

Consider implementing the settleUpnl function within the emergencyClosePosition function so that PartyB can proceed to settle PartyA's open positions to ensure that PartyA's allocated balance will have sufficient balance to allow the position to be emergency closed.

Discussion

MoonKnightDev

The emergency mode is designed for situations when a symbol is delisted or when a party B wants to exit. They can still use the settle unrealized profit and loss (uPnL) function separately to add to the user's allocated balance.

Issue M-7: Inconsistent in the liquidation fee leads to unfairness in liquidation process

Source: https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues/52

The protocol has acknowledged this issue.

Found by

xiaoming90

Summary

Reserve vault results in unfairness in the liquidation process. PartyA using force close mechanism to liquidate a position will receive more liquidation fees compared to normal liquidators as they have access to the funds in the reserve vault.

This creates unfairness to the protocol's liquidation process and to the existing liquidators, as they will receive lesser liquidation fees than Alice (PartyA). Without a fair and effective liquidation process, the protocol's solvent will be at risk, as bad positions will not be liquidated in a timely manner, and some liquidators might not want to participate in the process due to unfairness in the system.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

Assume that PartyB's reserve vault has 1000 USD. During force closing, if PartyB's account is underwater, even with the additional support from its reserve vault, PartyB's account will be liquidated, as per Lines 127-138 below.

In Lines 128 and 129, the protocol transfers all the existing funds (1000 USD) from PartyB's reserve vault to PartyB's allocated balance. Thus, before the liquidation process is executed at Line 137 below, the PartyB's allocated balance will increase by 1000 USD.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/facets/ForceActions/ForceActionsFacetImpl.sol#L127

File: ForceActionsFacetImpl.sol
051:    function forceClosePosition(
..SNIP..
112:        require(partyAAvailableBalance >= 0, "PartyAFacet: PartyA will be insolvent");
113:        if (partyBAvailableBalance >= 0) {
..SNIP..
118:        } else if (partyBAvailableBalance + int256(reserveAmount) >= 0) {
..SNIP..
127:        } else { // @audit-info Code block for liquidating position
128:            accountLayout.reserveVault[quote.partyB] = 0;
129:            accountLayout.partyBAllocatedBalances[quote.partyB][quote.partyA] += reserveAmount;
130:            emit SharedEvents.BalanceChangePartyB(quote.partyB, quote.partyA, reserveAmount, SharedEvents.BalanceChangeType.REALIZED_PNL_IN);
131:            int256 diff = (int256(quote.quantityToClose) * (int256(closePrice) - int256(sig.currentPrice))) / 1e18;
132:            if (quote.positionType == PositionType.LONG) {
133:                diff = diff * -1;
134:            }
135:            isPartyBLiquidated = true;
136:            upnlPartyB = sig.upnlPartyB + diff;
137:            LibLiquidation.liquidatePartyB(quote.partyB, quote.partyA, upnlPartyB, block.timestamp);
138:        }

As a result, when the LibLiquidation.liquidatePartyB is executed at Line 137 above, the computed availableBalance will be 1000 USD smaller when the liquidation process is triggered via the forceClosePosition function compared to the standard liquidation process that is triggered via LiquidationFacet.liquidatePartyB by the liquidators, as shown below. This can be proven via the formula within partyBAvailableBalanceForLiquidation function.

**Normal scenario without access to reserve vault's funds**
availableBalance = partyBAllocatedBalances - lockedValue + PnL
availableBalance = 1000 - 100 - 2000 = -1100

**Force close with access the reserve vault's funds => partyBAllocatedBalances will 1000 USDC higher (1000 => 2000)**
availableBalance = partyBAllocatedBalances - lockedValue + PnL
availableBalance = 2000 - 100 - 2000 = -100

Assume Alice is entitled to execute the force close function against PartyB, while Bob is a normal liquidator that can only liquidate PartyB via the standard LiquidationFacet.liquidatePartyB function.

When Alice triggers the liquidation process, the availableBalance will be -100 instead of -1000. Thus, when computing the liquidation fee that the liquidator is entitled to at Line 39 below, the computed liquidation fee (remainingLf) will be higher. This means that if Alice triggered the liquidation process, she would receive more liquidation fees compared to Bob because she has access to funds within PartyB's reserve vault. In this case, Alice is the liquidator and will receive the liquidation fee.

As a result, this creates unfairness within the system and to the existing liquidators as they (e.g., Bob) will receive lesser liquidation fees compared to Alice.

https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibLiquidation.sol#L29

File: LibLiquidation.sol
23:     function liquidatePartyB(address partyB, address partyA, int256 upnlPartyB, uint256 timestamp) internal {
24:         AccountStorage.Layout storage accountLayout = AccountStorage.layout();
25:         MAStorage.Layout storage maLayout = MAStorage.layout();
26:         QuoteStorage.Layout storage quoteLayout = QuoteStorage.layout();
27: 
28:         // Calculate available balance for liquidation
29:         int256 availableBalance = LibAccount.partyBAvailableBalanceForLiquidation(upnlPartyB, partyB, partyA);
30: 
31:         // Ensure Party B is insolvent
32:         require(availableBalance < 0, "LiquidationFacet: partyB is solvent");
..SNIP..
37:         // Determine liquidator share and remaining locked funds
38:         if (uint256(-availableBalance) < accountLayout.partyBLockedBalances[partyB][partyA].lf) {
39:             remainingLf = accountLayout.partyBLockedBalances[partyB][partyA].lf - uint256(-availableBalance);
40:             liquidatorShare = (remainingLf * maLayout.liquidatorShare) / 1e18;
41: 
42:             maLayout.partyBPositionLiquidatorsShare[partyB][partyA] =
43:                 (remainingLf - liquidatorShare) /
44:                 quoteLayout.partyBPositionsCount[partyB][partyA];
45:         } else {
46:             maLayout.partyBPositionLiquidatorsShare[partyB][partyA] = 0;
47:         }

Impact

This creates unfairness to the protocol's liquidation process and to the existing liquidators, as they will receive lesser liquidation fees than Alice. Without a fair and effective liquidation process, the protocol's solvent will be at risk, as bad positions will not be liquidated in a timely manner, and some liquidators might not want to participate in the process due to unfairness in the system.

PoC

No response

Mitigation

The liquidation process for Alice and existing liquidators should be consistent to ensure fairness. In the above case, consider either of the following solutions:

Discussion

MoonKnightDev

Normal liquidators can also call the settleAndForceClose functions.