Hollow Slate Gazelle
Medium
partyB
can make losses to partyA
by not completing the partyA
request to close quote. Also making partyA
, not to able to call the forceClose
function
Look at the function ForceActionsFacetImpl::forceClosePosition()
. It allows Party A to force close their position by referencing data before the block.timestamp
with some checks. Here are those checks:
require(sig.endTime + maLayout.forceCloseSecondCooldown <= quote.deadline, "PartyBFacet: Close request is expired");
require(sig.startTime >= quote.statusModifyTimestamp + maLayout.forceCloseFirstCooldown, "PartyAFacet: Cooldown not reached");
require(sig.endTime <= block.timestamp - maLayout.forceCloseSecondCooldown, "PartyAFacet: Cooldown not reached");
Let’s take a simple example:
- Party A wants to close their position and calls
PartyAFacet::requestToClosePosition()
, which will make some changes.
Let's assume the following data:maLayout.forceCloseFirstCooldown = 60
maLayout.forceCloseSecondCooldown = 60
quote.statusModifyTimestamp = block.timestamp = 314
quote.deadline = deadline = 500
quote.statusModifyTimestamp = block.timestamp;
quote.quoteStatus = QuoteStatus.CLOSE_PENDING;
quote.requestedClosePrice = closePrice;
quote.quantityToClose = quantityToClose;
quote.orderType = orderType;
quote.deadline = deadline;
-
Now, at
block.timestamp = 502
, the user selects a timeframe from380 (sig.startTime)
to430 (sig.endTime)
, selects a price, verifies it with Binance, and gets a signature from Muon. They then callForceActionsFacet::forceClosePosition()
with the signature. -
At
block.timestamp = 502
, although this bypasses thequote.deadline (500)
, the transaction will still be processed. -
If we look at the first check:
sig.endTime(430) + maLayout.forceCloseSecondCooldown(60) <= quote.deadline(500) -> (490 <= 500)
✅ -
If we look at the second check:
sig.startTime(380) >= quote.statusModifyTimestamp(314) + maLayout.forceCloseFirstCooldown(60) -> (380 >= 374)
✅ -
The third check:
sig.endTime(430) <= block.timestamp(502) - maLayout.forceCloseSecondCooldown(60) -> 430 <= 442
✅
As a result, the transaction will execute and be processed. The purpose of showing this is to illustrate that it doesn't matter whether the quote's deadline has passed relative to the current block.timestamp
. This is an intended design of the protocol to ensure that Party A doesn’t face any losses due to Party B's inaction. If there were a check between the block.timestamp
and the deadline, it could lead to losses for Party A. Party A will try to close the position when they are in profit, and if Party B does not accept the request, Party A's only option would be to force close. If that fails due to the deadline, Party A would have to expire their position, open it again, and lose the opportunity to close the position at a time when they were in high profit. This could cause Party A to face a loss due to Party B's actions, which is why the protocol has not implemented such checks.
PartyAFacet::expireQuote()
can be called by anyone , so when thepartyA
status isclose_pending
, alsoquote.deadline < block.timestamp
sopartyB
can callPartyAFacet::expireQuote()
to stoppartyA
in force closing of the position as thepartyB
is in high loss.
partyB
should act maliciously.(partyB can act malicious as it is a semi-trusted role. Also partyB is considered malicious in previous findings
No such condition's required
As given in Initialize.fixtures.ts
, forceCloseFirstCooldown = 300
and forceCloseSecondCooldown = 120
:
-
Party A has a quote with Party B, and Party A is in high profit while Party B is in high loss.
-
Party A calls
PartyAFacet::requestToClosePosition()
to close the request with a deadline 7 minutes (420 secs) from the currentblock.timestamp
. Party A can give any deadline as there is no check for that.quote.statusModifyTimestamp = block.timestamp = 300
quote.deadline = deadline = 720
(300 + 420)
-
Party B, in high loss, acts maliciously by not accepting the request, waiting for the prices to move in their favor before closing the position(partyB can act malicious as it is a semi-trusted role. Also partyB is considered malicious in previous findings).
-
At
t = 500
, Party A is still waiting for Party B to fill the close quote request. To callForceActionsFacet::forceClosePosition()
,sig.startTime()
should be greater thanquote.statusModifyTimestamp(300) + 300
, and withcurrentTimestamp = 500
, Party A is unable to callforceClosePosition()
yet. -
At
t = 721
or later, Party A will be eligible to pass all the checks and will select a timeframe when the desired price was attained, verify it with Binance, get a signature from Muon, and callForceActionsFacet::forceClosePosition()
with the signature. -
Before Party A can act, Party B front-runs the transaction and calls
PartyAFacet::expireQuote()
with thequoteId
. Since the currentblock.timestamp(721)
exceedsquote.deadline(720)
, the function executes and modifies thequote
at the storage level. Party B could have called this function as soon as the conditionquote.deadline < block.timestamp
was met other then frontrunning it. -
Party A's
ForceActionsFacet::forceClosePosition()
call will now fail sincequote.quoteStatus = QuoteStatus.OPENED
andquote.statusModifyTimestamp = block.timestamp
. -
Hence partyA will not be able to close his position when he wanted, and now he can't call
ForceActionsFacet::forceClosePosition()
with again that timeframe asquote.statusModifyTimestamp
has changed with current block.timestamp. As a result, Party A cannot close the position as planned. Due to the volatile crypto market, Party A could now face losses while Party B profits from the market shift.
This vulnerability can also be exploited in other ways by Party B if Party A sets a shorter deadline. Party B could exploit the situation more easily since there are no checks on the deadline. Party A can set any value, and given that people often prefer quicker results, it's very likely that Party A might set a deadline of less than 5 minutes.
- Losses for Party A:
- Party A will not be able to close their position at their intended time.
No response
- Implement some checks while setting
quote.deadline
. - Also add some modifier in
expireQuote()