Closed sherlock-admin3 closed 3 weeks ago
Thank you for these scenario's, but they don't answer my questions. Please provide answers to the questions I asked in previous comment.
Sir, I am sure I tried to answer all the questions you asked. Here is my complete Poc https://github.com/sherlock-audit/2024-02-perpetual-judging/issues/112#issuecomment-2092028311. Here is my initial Poc for this problem https://github.com/sherlock-audit/2024-02-perpetual-judging/issues/112#issuecomment-2094141447.Increasing the margin is just to postpone the liquidation line. As for the price of 0 not being liquidated, only the margin meets the standard.Is this all, or am I missing something?
Yes, there are a couple of them. Thank you for the POC again, but I've asked you to explain this changes from the Neon's POC. I've asked them in this and this comments. Yes, you answered the question regarding makerData
, but still didn't explain why in your POC the trader is not liquidatable after the attack, when in Neon's POC they are if the price drops slightly. If I missed where you explain it, please forward me to that comment, cause still can't see where you explain the reason for such difference.
Secondly, I don't see where you provide counter arguments for @IllIllI000 assumption. I will copy and paste this quesiton here.
the increase in margin happens not because the trader opens a trade with themselves after trading with the maker. The LSW says that it happens because two trades have different aberage entry price, hence when you enter the second trade, the margin utilization is different, cause the entry price is different. That is the reason why they believe it should be low. Yes the margin utilization is better without adding any collateral, but you enter the trade at a different price, hence, the margin utilization is different. Therefore, it works correctly and as it should.
Again, if I miss a comment where you explain it, please forwrd me to it, cause I genuinely don't see it.
Lastly, could you explain your last POC about shorting 1 ether from this comment. Again, just copy and pasting my question:
The trader opens a short at price of 100. Then you assert they can be liquidatable at price of 193. Then you close the position and now the trader is not liquidatable at price of 193. Well, why it should be liquidatable if it's closed? Then you open a new position at the price of 50. I don't understand what the POC proves, hence, asking you to explain it please, cause from my point of view you close position, try to check if it's liquidatable and it's quite logical that it's not, since it's closed.
Of course, I genuinely miss where you answer these questions cause after I asked them, you provided three scenarios which don't give explicit answers to these questions. Excuse me, if I'm actually just blind and miss them.
In the meantime, I'll ask do I understand correctly what the flow of the attack here is? Please correct me if anything is missing or any assumption is wonrg. @oxneon @joicygiore @IllIllI000
We have a user (regular trader) and the attacker. They open longs at the same price, i.e. 100. Instantly after that, the attacker opens a second trade, but with himself as a maker. Because of that, their initial trade is closed and the new one has a differentl average entry price (even if they open the second trade instantly).
Then the price dropped to 50 and now both of them should be liquidated (assuming they opened two identical longs). The user gets liquidated, but the attacker is no liquidatable. The reason for that is because when they opened the second trade their average entry price was lower, therefore, their margin utilisation rate is better and they're not liquidated at this price.
Feel free to correct me.
The only thing kind of missing is that the closing of the attacker's first trade is at the price that the attacker specifies for the second trade, which means whatever gains/losses are associated with the closing of that first trade, are applied to the margin of the attacker, prior to the second trade opening at the price that the attacker specified for the second trade.
But what if the price of both first and second trade is the same? I assume the margin utilization will not change and in the scenario I laid out here, the attacker would be liquidatable at the price of 50?
If the price of the second trade is lower than of the first one (entry price), for example entry of the second trade is 98, then they close the first at 98 as well. Hence, they realize the loss and now they have less margin, lower entry price, so the utilisation ratio is different then to the user. The same for opening the second trade at 102 and realising gains, but vice versa. Correct?
This PoC shows the 98 case (uses 95 instead). Changing bytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: (amount * 95) / 100 }));
in that PoC to bytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: amount }));
shows that neither is liquidatable for the same price case. The test isn't set up to work for the 102 case (getting ExcessiveInputAmount
error when trying to do the second trade)
Yes, there are a couple of them. Thank you for the POC again, but I've asked you to explain this changes from the Neon's POC. I've asked them in this and this comments. Yes, you answered the question regarding
makerData
, but still didn't explain why in your POC the trader is not liquidatable after the attack, when in Neon's POC they are if the price drops slightly. If I missed where you explain it, please forward me to that comment, cause still can't see where you explain the reason for such difference.Secondly, I don't see where you provide counter arguments for @IllIllI000 assumption. I will copy and paste this quesiton here.
the increase in margin happens not because the trader opens a trade with themselves after trading with the maker. The LSW says that it happens because two trades have different aberage entry price, hence when you enter the second trade, the margin utilization is different, cause the entry price is different. That is the reason why they believe it should be low. Yes the margin utilization is better without adding any collateral, but you enter the trade at a different price, hence, the margin utilization is different. Therefore, it works correctly and as it should.
Again, if I miss a comment where you explain it, please forwrd me to it, cause I genuinely don't see it.
Lastly, could you explain your last POC about shorting 1 ether from this comment. Again, just copy and pasting my question:
The trader opens a short at price of 100. Then you assert they can be liquidatable at price of 193. Then you close the position and now the trader is not liquidatable at price of 193. Well, why it should be liquidatable if it's closed? Then you open a new position at the price of 50. I don't understand what the POC proves, hence, asking you to explain it please, cause from my point of view you close position, try to check if it's liquidatable and it's quite logical that it's not, since it's closed.
Of course, I genuinely miss where you answer these questions cause after I asked them, you provided three scenarios which don't give explicit answers to these questions. Excuse me, if I'm actually just blind and miss them.
1.I don't really know Neon's poc. Because only he himself is the clearest thinker about the basic idea of his setup. I think he should be able to answer your question. This is indeed beyond my POC and my thinking. Please understand. 2.This is my raw content and analysis of why I feel like I can do this and it works. While increasing the margin, it will not affect the subsequent closing of positions. https://github.com/sherlock-audit/2024-02-perpetual-judging/issues/61 facts have proved that it is indeed effective 3.It can be liquidated before the transaction, but cannot be liquidated after the transaction. Please note the state changes of the two transaction assertions(This is just to show the comparison before and after self-trading.), The position was not liquidated. When the price meets the attacker's expectations, the attacker closes the position to cash in the profit. This is a normal operation. Is there any problem with this? Please note that this is the third example in https://github.com/sherlock-audit/2024-02-perpetual-judging/issues/112#issuecomment-2094201004 margin increase, liquidation failed, maker suffered losses
////////////////////////////////////
/////// normal circumstances ///////
////////////////////////////////////
// 100e18 -> 193.76e18
// margin ratio < 6.25%
assertEq(vault.getMarginRatio(marketId, attacker, 193.76e18), 6.24e16);
// can be liquidated
@> assertEq(clearingHouse.isLiquidatable(marketId, attacker, 193.76e18), true);
/////////////////////////////////////
/// Attackers trade on their own ////
/////////////////////////////////////
// after transaction attacker closePosition in self and set makerData
bytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: 95 ether }));
// if price band ratio == 0
// for (uint256 i = 0; i < 5; ++i) {
clearingHouse.closePosition(
IClearingHouse.ClosePositionParams({
marketId: marketId,
maker: address(attacker),
oppositeAmountBound: type(uint256).max,
deadline: block.timestamp,
makerData: makerData
})
);
// }
//////////////////////////////////
///Unable to liquidate normally///
//////////////////////////////////
// set price -> 193.76e18
maker.setBaseToQuotePrice(193.76e18);
_mockPythPrice(19376, -2);
// Unable to liquidate
@> assertEq(clearingHouse.isLiquidatable(marketId, attacker, 193.76e18), false);
In the meantime, I'll ask do I understand correctly what the flow of the attack here is? Please correct me if anything is missing or any assumption is wonrg. @oxneon @joicygiore @IllIllI000
We have a user (regular trader) and the attacker. They open longs at the same price, i.e. 100. Instantly after that, the attacker opens a second trade, but with himself as a maker. Because of that, their initial trade is closed and the new one has a differentl average entry price (even if they open the second trade instantly).
Then the price dropped to 50 and now both of them should be liquidated (assuming they opened two identical longs). The user gets liquidated, but the attacker is no liquidatable. The reason for that is because when they opened the second trade their average entry price was lower, therefore, their margin utilisation rate is better and they're not liquidated at this price.
Feel free to correct me.
Simply put, as long as your margin is enough, no one can liquidate you. You just need to wait for it to return to your expected closing price.
But what if the price of both first and second trade is the same? I assume the margin utilization will not change and in the scenario I laid out here, the attacker would be liquidatable at the price of 50?
If the price of the second trade is lower than of the first one (entry price), for example entry of the second trade is 98, then they close the first at 98 as well. Hence, they realize the loss and now they have less margin, lower entry price, so the utilisation ratio is different then to the user. The same for opening the second trade at 102 and realising gains, but vice versa. Correct?
I really don’t quite understand what you mean by 102. This is not my idea, so I don’t know how to explain this issue to you. Below is my original content https://github.com/sherlock-audit/2024-02-perpetual-judging/issues/61 So, sir, that's all
If I've missed anything, please feel free to tell me. Thank you for your patience. I think I need to improve my language skills and expression skills. 🫡 @WangSecurity @IllIllI000
This PoC shows the 98 case (uses 95 instead). Changing
bytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: (amount * 95) / 100 }));
in that PoC tobytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: amount }));
shows that neither is liquidatable for the same price case. The test isn't set up to work for the 102 case (gettingExcessiveInputAmount
error when trying to do the second trade)
I don’t quite understand the implementation logic of 102, so it’s normal that it can’t be used in my POC,thanks for understanding
Thank you for these responses!
I really don’t quite understand what you mean by 102. This is not my idea, so I don’t know how to explain this issue to you
This is just an arbitrary exmple to clarify if there's something I'm missing.
It can be liquidated before the transaction, but cannot be liquidated after the transaction. Please note the state changes of the two transaction assertions(This is just to show the comparison before and after self-trading.),
Totally understand, the thing that is not clear for me, the transaction in this piece of the test is to close the position. Hence, yes it's liquidatable before closing the position, but after you closed the position it's not, which is completely logical. What I don't see is where is the self trade? I see that the maker is the attacker address, but it only closes the position, and it's logical the closed position cannot be closed. Do I miss where you open the second trade?
Also, as I understand, you don't disagree with this assumption. I see your comment:
Simply put, as long as your margin is enough, no one can liquidate you. You just need to wait for it to return to your expected closing price
But it doesn't agree or disagree with my assumption that the attacker cannot be liquidated cause the entry price of the second trade is lower than the first one, not cause they trade with themselves.
And I've got another question to any watson who wish to answer (@joicygiore @IllIllI000) if in that case, the only reason for having better margin utilisation is opening the second trade at a lower price, then it doesn't matter if they trade with themselves? In that case the attacker shouldn't put themselves as the maker, and they can open the second trade using the same maker as for the first trade and achieve the same result. Correct?
For your followup question, the first trade's maker in the thought experiment will be the OM, the SHBM, or another account. The OM uses the Pyth oracle for the price, so they can't do it there because that's a fixed price that they cannot control. The SHBM uses uniswap, so unless they skew the pool, they can't choose a price there either. They can do the same thing by trading with another account (e.g. their own other account), assuming that other account made the original trade (i.e. at price of 100) so that the account has shares to provide as the maker.
He must trade as a maker in order to control the makerData parameters and increase the margin rate. The closePosition
used in my POC is actually just to be lazy and reduce parameters. In order to control makerData, we only need a position opposite to the original position to execute self-transactions. For example, the following method is still valid
// after transaction attacker closePosition in self and set makerData
bytes memory makerData = abi.encode(IClearingHouse.MakerOrder({ amount: 95 ether }));
// if price band ratio == 0
// for (uint256 i = 0; i < 5; ++i) {
clearingHouse.openPosition(
IClearingHouse.OpenPositionParams({
marketId: marketId,
maker: address(attacker),
isBaseToQuote: false,
isExactInput: false,
amount: 1 ether,
oppositeAmountBound: 100 ether,
deadline: block.timestamp,
makerData: makerData
})
);
// clearingHouse.closePosition(
[PASS] testModifyAccountMarginRatio() (gas: 1596049) Logs: attacker Start Collateral Token 100000000 attacker End Collateral Token 150000000
The attacker controls the value of result.quote or result.base through makerData. https://github.com/sherlock-audit/2024-02-perpetual/blob/main/perp-contract-v3/src/clearingHouse/ClearingHouse.sol#L325-L331
} else {
// quote to exactOutput(base), Q2B base+ quote-
result.base = params.amount.toInt256();
@> result.quote = -oppositeAmount.toInt256();
}
}
_checkPriceBand(params.marketId, result.quote.abs().divWad(result.base.abs()));
Thank you for these responses!
I really don’t quite understand what you mean by 102. This is not my idea, so I don’t know how to explain this issue to you
This is just an arbitrary exmple to clarify if there's something I'm missing.
It can be liquidated before the transaction, but cannot be liquidated after the transaction. Please note the state changes of the two transaction assertions(This is just to show the comparison before and after self-trading.),
Totally understand, the thing that is not clear for me, the transaction in this piece of the test is to close the position. Hence, yes it's liquidatable before closing the position, but after you closed the position it's not, which is completely logical. What I don't see is where is the self trade? I see that the maker is the attacker address, but it only closes the position, and it's logical the closed position cannot be closed. Do I miss where you open the second trade?
Also, as I understand, you don't disagree with this assumption. I see your comment:
Simply put, as long as your margin is enough, no one can liquidate you. You just need to wait for it to return to your expected closing price
But it doesn't agree or disagree with my assumption that the attacker cannot be liquidated cause the entry price of the second trade is lower than the first one, not cause they trade with themselves.
And I've got another question to any watson who wish to answer (@joicygiore @IllIllI000) if in that case, the only reason for having better margin utilisation is opening the second trade at a lower price, then it doesn't matter if they trade with themselves? In that case the attacker shouldn't put themselves as the maker, and they can open the second trade using the same maker as for the first trade and achieve the same result. Correct?
Sir, I looked at the code again. In my memory, self-transaction must be used to pass the series of checks in the source code below to complete the parameter control of makeData. So self-dealing is a must
https://github.com/sherlock-audit/2024-02-perpetual/blob/02f17e70a23da5d71364268ccf7ed9ee7cedf428/perp-contract-v3/src/authorization/AuthorizationUpgradeable.sol#L55-L57 https://github.com/sherlock-audit/2024-02-perpetual/blob/02f17e70a23da5d71364268ccf7ed9ee7cedf428/perp-contract-v3/src/clearingHouse/ClearingHouse.sol#L478-L480 https://github.com/sherlock-audit/2024-02-perpetual/blob/02f17e70a23da5d71364268ccf7ed9ee7cedf428/perp-contract-v3/src/clearingHouse/ClearingHouse.sol#L293
Hello, gentlemen. I relearned Neon's code. I think I found the reason for the difference in the POC code. Please see the source code of ClearingHouse::_openPosition()
below. He chose another execution route and also used the self-trade control makerData parameter to increase the margin rate. @WangSecurity @IllIllI000
@> if (params.isExactInput) {
_checkExactInputSlippage(oppositeAmount, params.oppositeAmountBound);
@> if (params.isBaseToQuote) {
// exactInput(base) to quote, B2Q base- quote+
@> result.base = -params.amount.toInt256();
@> result.quote = oppositeAmount.toInt256();
@> } else {
// exactInput(quote) to base, Q2B base+ quote-
@> result.base = oppositeAmount.toInt256();
@> result.quote = -params.amount.toInt256();
}
@> } else {
_checkExactOutputSlippage(oppositeAmount, params.oppositeAmountBound);
@> if (params.isBaseToQuote) {
// base to exactOutput(quote), B2Q base- quote+
@> result.base = -oppositeAmount.toInt256();
@> result.quote = params.amount.toInt256();
} else {
@> result.base = params.amount.toInt256(); // 1e18
@> result.quote = -oppositeAmount.toInt256(); // 1e18
}
}
Thank you for these responses both @joicygiore and @IllIllI000. I believe the assumption I made earlier is correct:
If there are two traders (regular user and the attacker) and both open the identical long trade (for comparison) at price 100. Both should be invalid at 50, but the attacker opens up a second long trade after that. They use themselves as a maker which allows them to bypass important checks and control the open price of the second trade. The problem here is that for second trade is lower, e.g. 95 (as in one of the POCs). Hence, they lose some margin, cause the first trade was also closed at 95. Besides that, cause the open price is lower, their utilisation rate at 50 also better than for the another user, who just held the initial position. It leads to the situation where the attacker is not liquidatable at 50, but the user is. The only reason is that the average entry price of the second trade for the attacker is lower, and the code works exactly as it should. I agree this vulnerability allows for unauthorized actions, but I don't see any impact from it. Hence, I believe low/info severity is indeed appropriate for this issue.
Planning to accept the escalation and invalidate this issue (and it's duplicates).
@WangSecurity First of all, this issue is confirmed and fixed by the sponsor. The sponsor's recommendation is medium. I am not discussing any issues related to this project. But I hope you will change the other 9 issues that the sponsor confirmed will not be fixed to low. Of course, I think as long as you are happy, it will be fine.😃
@joicygiore Sponsor's words, decisions, fixes doesn't effect neither validity nor severity of the issue. If you genuinely disagree with my decision, then please tell me where this assumption is wrong, otherwise, it's low/info:
If there are two traders (regular user and the attacker) and both open the identical long trade (for comparison) at price 100. Both should be invalid at 50, but the attacker opens up a second long trade after that. They use themselves as a maker which allows them to bypass important checks and control the open price of the second trade. The problem here is that for second trade is lower, e.g. 95 (as in one of the POCs). Hence, they lose some margin, cause the first trade was also closed at 95. Besides that, cause the open price is lower, their utilisation rate at 50 also better than for the another user, who just held the initial position. It leads to the situation where the attacker is not liquidatable at 50, but the user is. The only reason is that the average entry price of the second trade for the attacker is lower, and the code works exactly as it should. I agree this vulnerability allows for unauthorized actions, but I don't see any impact from it. Hence, I believe low/info severity is indeed appropriate for this issue.
@joicygiore Sponsor's words, decisions, fixes doesn't effect neither validity nor severity of the issue. If you genuinely disagree with my decision, then please tell me where this assumption is wrong, otherwise, it's low/info:
If there are two traders (regular user and the attacker) and both open the identical long trade (for comparison) at price 100. Both should be invalid at 50, but the attacker opens up a second long trade after that. They use themselves as a maker which allows them to bypass important checks and control the open price of the second trade. The problem here is that for second trade is lower, e.g. 95 (as in one of the POCs). Hence, they lose some margin, cause the first trade was also closed at 95. Besides that, cause the open price is lower, their utilisation rate at 50 also better than for the another user, who just held the initial position. It leads to the situation where the attacker is not liquidatable at 50, but the user is. The only reason is that the average entry price of the second trade for the attacker is lower, and the code works exactly as it should. I agree this vulnerability allows for unauthorized actions, but I don't see any impact from it. Hence, I believe low/info severity is indeed appropriate for this issue.
I can't understand where your second transaction came from. I can only tell you that without the first transaction, it is impossible to generate any orders from your own transaction, and you cannot modify the parameters. You can try it, okay? The most important thing is that you are happy, nothing else matters
Thank you for patiently listening to my nonsense for so many days. I'm sorry for wasting your time. I'm sorry.
If you use the OWASP Top
vulnerability to remotely execute code on a server, but you don't get any useful information, then owasp is wrong and there is something wrong with its rating. It should be low, right? Even if you execute the command remotely to delete everything on the server, but the project still has a backup, the rating will still be low.
Because it doesn't cause any harm at all, right?
Let’s take another simple example to compare self-dealing. You have 1,000 ether, and you use your left hand and your right hand to roll dice to compare the sizes for a gambling game. If you play this game ten million times, you still have 1,000 ether, and there will be no change. Self-trading itself is meaningless, it is just an entrance to modify parameters.
If you have enough time, you can even play until the earth is destroyed. 1000 is still 1000.
thanks @paco0x @IllIllI000 @WangSecurity @joicygiore . Just finished my vacation, please give me some time to review all the comments I missed.
thanks @paco0x @IllIllI000 @WangSecurity @joicygiore . Just finished my vacation, please give me some time to review all the comments I missed.
Finally, you have shown up
Sorry, International Labor Day has been off for five days and I just returned to work today. If I have offended anyone before, I apologize first. Please allow me some time to reread the comments I missed
Result: Invalid Has Duplicates
neon2835
high
Users can avoid the possibility of liquidation
Summary
When the margin utilization rate of the account is lower than
maintenanceMarginRatio
, the user's account will be liquidated. Therefore, in order to avoid liquidation, users must add margin to the account to make its account value higher than themaintenance margin requirement
.However, there is a vulnerability in the system, which can improve the utilization rate of account margin without adding more additional margin, which allows users to avoid the possibility of liquidation.
Vulnerability Detail
There are two main reasons for this vulnerability:
priceBandRatio
can be set in thepriceBandRatioMap
of theconfig
contract.The
priceBandRatio
will also be different according to the risk of different markets. Take the ETH market as an example, itspriceBandRatio
is about 5% (I consulted the project personnel on the discord discussion group).Assuming that users open positions to hold long/short positions in ETH market. At this time, the user trades with himself, he can manipulate the trade price, thus manipulating the margin utilization rate of the account.
Let the code speak for itself, here's my test file:
MyTest.t.sol
, just put it in thetest/clearingHouse
directory:First, run the
test_imporveMarginRatio
function to verify whether the margin utilization rate can be improved, run:Get the results:
We can see that with the
accountValue
unchanged, the margin utilization rate has increased from333333333333333333
to349999999999999999
. Please note that this is only whenpriceBandRatio
is equal to 5%, the greater the value ofpriceBandRatio
, the greater the margin ratio that can be increased!Then let's continue running the
test_avoidLiquidation
function, run:Get the results:
Through the test results, it is found that the account should have met the conditions for liquidation, and by constructing its own transactions with itself, it can not be liquidated any more.
Impact
By trading with themselves, users can improve their margin utilization without adding additional margin, which allows users to avoid the possibility of liquidation
Code Snippet
Although the code snippet that caused the vulnerability is complex, the main reason is in the
_openPosition
function of theClearingHouse
contract:https://github.com/sherlock-audit/2024-02-perpetual/blob/main/perp-contract-v3/src/clearingHouse/ClearingHouse.sol#L267-L356
Tool used
Manual Review Foundry Vscode
Recommendation
Forcibly restrict users from trading with their own accounts. Add judgment conditions to the
_openPosition
function of theClearingHouse
contract: