It is possible to decrease the position size of a liquidatable account (marginBalance < requiredMaintenanceMargin ). This is not expected by the protocol.
In other words the following code does not properly disallow decrease of a liquidatable position:
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/OrderBranch.sol#L128-L137
When decreasing size of a liquidatable position, it checks that the position is liquidatable or not in the following code:
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/OrderBranch.sol#L126
To calculate previousRequiredMaintenanceMarginUsdX18, it assumes that the entire position is closed. By this assumption, the mark price is calculated, let's name it markPriceA.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/leaves/TradingAccount.sol#L278
Moreover, another mark price is playing a role which is calculated based on the size delta (the amount of size that is going to be decreased from the position). It is calculated in the following code, let's call it markPriceB.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/OrderBranch.sol#L113
The issue is that although the protocol disallows liquidatable accounts from decreasing their position, it can be done.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/OrderBranch.sol#L133
The root cause of this issue is that, in case of a long liquidatable positions, the mark price used to calculate previousRequiredMaintenanceMarginUsdX18 is lower than the mark price used to calculate marginBalanceUsdX18. I.e. markPriceB > markPriceA. This is becasue the markPriceA is calculated based on assumption that the entire position is closed, while the markPriceB is calculated based on the size that is going to be decreased.
As a result, when the accountTotalUnrealizedPnlUsdX18 is calculated, it subtracts the lastInteractionPrice from the mark price. Since markPriceB is higher than markPriceA, the absolute value of accountTotalUnrealizedPnlUsdX18 associated with markPriceB is lower than the absolute value of accountTotalUnrealizedPnlUsdX18 associated with markPriceA. Note that since it is a long position, and we are decreasing the size, lastInteractionPrice > mark price.
For the case of a short liquidatable positions, the mark price used to calculate previousRequiredMaintenanceMarginUsdX18 is higher than the mark price used to calculate marginBalanceUsdX18. And the same scenario explained above is valid for short positions as well.
It can be seen from another point of view that the marginBalanceUsdX18 which is compared with previousRequiredMaintenanceMarginUsdX18 is calculated after the impact of size delta. I.e. previous marginBalanceUsdX18 should be compared with previousRequiredMaintenanceMarginUsdX18, not marginBalanceUsdX18 after the impact of size delta.
Very simply let's give a example before the PoC. Note that the numbers are simplified for better understanding of the issue. The complete PoC includes the real numbers.
Suppose:
ETH price = 1000$
Collateral value = 1_000_000$
position size = 80k
markPrice:
So, the fill price would be 1004$.
Then, suppose the price of ETH decreases by 8$, so the index price would be 992$. This position is liquidatable based on the function checkLiquidatableAccounts where it assumes that the entire position is closed when calculating the mark price.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/LiquidationBranch.sol#L42
If the user creates an order to decrease its liquidatable position, it is expected that it will be reverted. Because, the protocol intends to disallow any liquidatable position from being decreased. But, this is not implemented properly.
Suppose the user intends to reduce its liquidatable position by 50k.
In the following test, the account's collateral value is 1M, and the position created is 80k. After the ETH price is decreased from 1000$ to 992$, the account is liquidatable. Then the account creates an order to decrease size by 50k. It is expected to revert, because the account is liquidatable, but it does not revert due to the bug explained. So, the account position is decreased, and afterwards it is not liquidatable any more. Note that if the account decreases its position by small amount 1k, then decreasing position is still allowed, but after the reduction still the account is liquidatable.
Please note that the collateral value is the large number 1M. This is not necessary for validity of this vulnerability. For example, assuming to have just a collateral of 1000$, and long position size 90, then if ETH price decreases by 5.319$, the account becomes liquidatable but it is still possible to reduce the position size by 10.
Liquidatable positions can reduce their position size against the protocol expectation
The following modification is recommended:
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/perpetuals/branches/OrderBranch.sol#L128-L137
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.