Vulnerability Details
After a flag, firstLiquidationTime
amount of time (initially set to 10hrs) has to pass before a liquidation can be made.
function _canLiquidate(MTypes.MarginCallPrimary memory m)
private
view
returns (bool)
{
uint256 timeDiff = LibOrders.getOffsetTimeHours() - m.short.updatedAt;
uint256 resetLiquidationTime = LibAsset.resetLiquidationTime(m.asset);
if (timeDiff >= resetLiquidationTime) {
return false;
} else {
uint256 secondLiquidationTime = LibAsset.secondLiquidationTime(m.asset);
bool isBetweenFirstAndSecondLiquidationTime = timeDiff
> LibAsset.firstLiquidationTime(m.asset) && timeDiff <= secondLiquidationTime
&& s.flagMapping[m.short.flaggerId] == msg.sender;
bool isBetweenSecondAndResetLiquidationTime =
timeDiff > secondLiquidationTime && timeDiff <= resetLiquidationTime;
if (
!(
(isBetweenFirstAndSecondLiquidationTime)
|| (isBetweenSecondAndResetLiquidationTime)
)
) {
revert Errors.MarginCallIneligibleWindow();
}
return true;
}
}
}
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/MarginCallPrimaryFacet.sol#L351-L408
Within the 10hr period, there are multiple ways the collateralRatio could increase above primaryLiquidationCR
. The shorter can add more collateral, combine with other shorts having high collateralRatio or the price of collateral could increase. The ability of shorter's to reliably increase the collateral ratio within 10hrs could deter most of the flaggers as they would have no rewards for the gas spent on calling the flagShort
function.
function increaseCollateral(address asset, uint8 id, uint88 amount)
external
isNotFrozen(asset)
nonReentrant
onlyValidShortRecord(asset, msg.sender, id)
{
STypes.ShortRecord storage short = s.shortRecords[asset][msg.sender][id];
short.updateErcDebt(asset);
uint256 yield = short.collateral.mul(short.zethYieldRate);
short.collateral += amount;
uint256 cRatio = short.getCollateralRatio(asset);
if (cRatio >= Constants.CRATIO_MAX) revert Errors.CollateralHigherThanMax();
if (cRatio >= LibAsset.primaryLiquidationCR(asset)) {
short.resetFlag();
}
}
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortRecordFacet.sol#L38-L60
function combineShorts(address asset, uint8[] memory ids)
external
isNotFrozen(asset)
nonReentrant
onlyValidShortRecord(asset, msg.sender, ids[0])
{
if (c.shortFlagExists) {
if (
firstShort.getCollateralRatioSpotPrice(
LibOracle.getSavedOrSpotOraclePrice(_asset)
) < LibAsset.primaryLiquidationCR(_asset)
) revert Errors.InsufficientCollateral();
firstShort.resetFlag();
}
emit Events.CombineShorts(asset, msg.sender, ids);
}
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortRecordFacet.sol#L117-L189
The above method of flagging before liquidation has to be followed until the collateral ratio reaches secondaryLiquidationCR
(initially set to 150%). But since secondary liquidation offers neither any fees nor any gas refunds to the margin caller, the liquidation is again not incentivized.
Impact
The system could end up with debt having not enough underlying collateral, leading to loss of value of the pegged asset.
Recommendations
Offer gas reimbursements to a flagger on flagging.