The MarginCallPrimaryFacet::liquidate()
faces a rounding-down issue when verifying the margin call time window.
Subsequently, a flagged Short will not be liquidable by a short flagger suddenly after 10 hours have passed (firstLiquidationTime
), and it will not be liquidable by other margin callers immediately after 12 hours have passed (secondLiquidationTime
) as expected.
The liquidate()
is one of the most essential functions of the Ditto
protocol. The malfunctioning of the liquidate()
can lead to the disruption of the Ditto
protocol and their minted stable assets (e.g., cUSD).
The liquidate()
executes the _canLiquidate()
to verify that the function must be called within the designed margin call time window according to the following timeline.
By default, the firstLiquidationTime
is 10 hours, the secondLiquidationTime
is 12 hours, and the resetLiquidationTime
is 16 hours.
Between the updatedAt
and firstLiquidationTime
, the liquidate()
is not callable.
Between the firstLiquidationTime
and secondLiquidationTime
, the liquidate()
is only callable by the short flagger.
Between the secondLiquidationTime
and resetLiquidationTime
, the liquidate()
is callable by anyone.
After the resetLiquidationTime
, the liquidate()
is not callable, and the Short's flag can be reset.
However, I discovered that the _canLiquidate()
does not function according to the above spec. Since the _canLiquidate()
computes the timeDiff
variable using an hour unit, the resulting timeDiff
will face a rounding-down issue because Solidity has no fixed-point numbers (please refer to the Proof of Concept
section below for more details).
Consequently, the margin call time window will be shifted forward by 1 hour.
To elaborate, the above timeline will become the below.
By default, the firstLiquidationTime
is 10 hours, the secondLiquidationTime
is 12 hours, and the resetLiquidationTime
is 16 hours. (Using the same settings)
Between the updatedAt
and firstLiquidationTime
(+ 1 hour), the liquidate()
is not callable.
Between the firstLiquidationTime
(+ 1 hour) and secondLiquidationTime
(+ 1 hour), the liquidate()
is only callable by the short flagger.
Between the secondLiquidationTime
(+ 1 hour) and resetLiquidationTime
(+ 1 hour), the liquidate()
is callable by anyone.
After the resetLiquidationTime
(+ 1 hour), the liquidate()
is not callable, and the Short's flag can be reset.
The liquidate() executes the _canLiquidate() to check if the function call is within the expected margin call time window
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallPrimaryFacet.sol#L118
The _canLiquidate() computes the timeDiff (in an hour unit)
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallPrimaryFacet.sol#L384
Since the timeDiff is in an hour unit, the margin call time window is shifted forward by 1 hour (not the expected time window)
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallPrimaryFacet.sol#L391-L395
The getOffsetTimeHours()'s resulting timeInHours faces a rounding-down issue (coarse-grained timestamp unit)
: https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/libraries/LibOrders.sol#L31
This section will demonstrate how the timeDiff
variable will face the rounding-down issue.
Assuming that the variable timeInSeconds
= 3600 (1 hour)
When we compute the variable timeInHours
= 1
But if the timeInSeconds
= 5400 (1.5 hours)
The timeInHours
= 1 (not 1.5)
As you can see, the resulting timeInHours
will become 1 (not 1.5) due to the rounding down since Solidity has no fixed-point numbers.
Subsequently, the flagged Short will not be liquidable by the short flagger suddenly after 10 hours have passed (firstLiquidationTime
), and it will not be liquidable by other margin callers immediately after 12 hours have passed (secondLiquidationTime
) as expected.
This will break the functionality of the liquidate()
, affecting the user experience. Moreover, the shift of 1 hour can cause the debts of all Short positions in the protocol to be larger than expected, affecting the stability of the Ditto
protocol and its minted stable assets (e.g., cUSD).
Manual Review
To fix the vulnerability, change the conditional logic for the isBetweenFirstAndSecondLiquidationTime
and isBetweenSecondAndResetLiquidationTime
variables like the below snippet.
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.