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

0 stars 0 forks source link

xiaoming90 - Unsafe casting of `reserveAmount` from uint256 to int256 #53

Open sherlock-admin3 opened 1 week ago

sherlock-admin3 commented 1 week ago

xiaoming90

High

Unsafe casting of reserveAmount from uint256 to int256

Summary

Unsafe casting of reserveAmount from uint256 to int256, leading to an incorrect value. As a result, it could lead to DOS or a loss of assets, as shown in the report below.

Root Cause

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

At Line 118 below, the code performs an unsafe downcasting from uint256 to int256. If the reserveAmount exceeds the maximum value of an int256 (2^255 - 1), the conversion will not revert automatically. Instead, it will silently result in an incorrect wrapped value.

Note: It is possible for the reserveAmount to exceed 2^255 - 1 within the protocol as the depositToReserveVault function does not restrict users from depositing an amount larger than 2^255 - 1 and accountLayout.reserveVault[partyB] is uint256.

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

File: ForceActionsFacetImpl.sol
113:        if (partyBAvailableBalance >= 0) {
114:            if (updatedPrices.length > 0) {
115:                LibSettlement.settleUpnl(settlementSig, updatedPrices, msg.sender, true);
116:            }
117:            LibQuote.closeQuote(quote, quote.quantityToClose, closePrice);
118:        } else if (partyBAvailableBalance + int256(reserveAmount) >= 0) {
119:            uint256 available = uint256(-partyBAvailableBalance);
120:            accountLayout.reserveVault[quote.partyB] -= available;
121:            accountLayout.partyBAllocatedBalances[quote.partyB][quote.partyA] += available;
123:            if (updatedPrices.length > 0) {
124:                LibSettlement.settleUpnl(settlementSig, updatedPrices, msg.sender, true);
125:            }
126:            LibQuote.closeQuote(quote, quote.quantityToClose, closePrice);
127:        } else {
128:            accountLayout.reserveVault[quote.partyB] = 0;
129:            accountLayout.partyBAllocatedBalances[quote.partyB][quote.partyA] += reserveAmount;
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:        }

The following demonstrates a scenario where a reserveAmount that is larger than int256 (2^255 - 1) resulting in the condition (partyBAvailableBalance + int256(reserveAmount)) at Line 118 above to be evaluated to be incorrect. When reserveAmount is a positive value of 2**256 - 100, it will become a negative value (-100) when downcasted from uint256 to int256, as shown below.

➜  ~ chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ uint256 reserveAmount = 2**256 - 100;
➜ int256 reserveAmountDowncast = int256(reserveAmount)
➜ reserveAmountDowncast
Type: int256
├ Hex: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c
├ Hex (full word): 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c
└ Decimal: -100
➜ int256 partyBAvailableBalance = -150
➜ int256 result = partyBAvailableBalance + reserveAmountDowncast
➜ result
Type: int256
├ Hex: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff06
├ Hex (full word): 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff06
└ Decimal: -250

If there is no downcasting error, the condition at Line 118 should have been evaluated as True since:

partyBAvailableBalance + reserveAmount >= 0
-100 + (2**256 - 100) >= 0
2**256 >= 0
True

However, as shown in the chisel output above, the final result is a negative number (result = partyBAvailableBalance + reserveAmountDowncast = -250), which leads to the condition being incorrectly evaluated as false.

This PartyB, with a large amount of reserve (2**256 - 100), should allow any force close action against them to be executed smoothly (without reverting or being liquidated) by going into the second branch of the if-else branch (Code in Lines 118-126).

However, due to the downcasting error, the logic in the final-else branch is executed instead (Lines 128-137 above), which contains the code for liquidating the PartyB (Line 127 above). Eventually, it is likely that the liquidation against PartyB will revert as the availableBalance within the LibLiquidation.availableBalance function will likely be larger than 0. This also means that PartyA cannot force close this position due to this revert.

On the other hand, it is possible that the liquidation process will go through without a revert (If the availableBalance within liquidatePartyB function increases, but still remains as a negative value). In this case, the consequences are as follows:

There is also another possible scenario where an incorrect reserveAmount value after downcast at Line 118 causes the condition at Line 118 to be wrongly evaluated, leading to the quote being closed instead of PartyB being liquidated.

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

File: ForceActionsFacetImpl.sol
118:        } else if (partyBAvailableBalance + int256(reserveAmount) >= 0) {
119:            uint256 available = uint256(-partyBAvailableBalance);
120:            accountLayout.reserveVault[quote.partyB] -= available;
121:            accountLayout.partyBAllocatedBalances[quote.partyB][quote.partyA] += available;
122:            emit SharedEvents.BalanceChangePartyB(quote.partyB, quote.partyA, available, SharedEvents.BalanceChangeType.REALIZED_PNL_IN);
123:            if (updatedPrices.length > 0) {
124:                LibSettlement.settleUpnl(settlementSig, updatedPrices, msg.sender, true);
125:            }
126:            LibQuote.closeQuote(quote, quote.quantityToClose, closePrice);
127:        } else {
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:        }

Impact

Following is the list of possible negative impacts due to unsafe casting, summarized from the above report:

PoC

No response

Mitigation

Consider implementing the following measures to mitigate the issue: