Careful Fiery Scallop
High
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.
- Unsafe casting of
reserveAmount
from uint256 to int256, leading to an incorrect value
No response
No response
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.
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:
- PartyB's position, which should not have been liquidated, gets liquidated instead, leading to a loss to PartyB because a certain portion of the position or its locked values need to be given to the liquidator as liquidation fees.
- The liquidator (which is the PartyA triggering the force closing) will receive more liquidation fees than expected, which is incorrect.
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.
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: }
Following is the list of possible negative impacts due to unsafe casting, summarized from the above report:
- PartyA cannot force close this position due to a revert
- PartyB's position, which should not have been liquidated, gets liquidated instead, leading to a loss to PartyB because a certain portion of the position or its locked values need to be given to the liquidator as liquidation fees.
- The liquidator (which is the PartyA triggering the force closing) will receive more liquidation fees than expected, which is incorrect.
- Quote incorrectly being closed instead of PartyB being liquidated.
No response
Consider implementing the following measures to mitigate the issue:
- Use safe casting libraries, such as the OpenZeppelin's SafeCast
- Limit the maximum deposit to the reserve vault to be within the range of int256.