DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: medium
Valid

Rounding issue leads to malfunctioning of flagShort()

Summary

The MarginCallPrimaryFacet::flagShort() faces a rounding-down issue when verifying the margin call end time (resetLiquidationTime).

Subsequently, a short flagger cannot re-flag the target Short suddenly after 16 hours (margin call end time -- resetLiquidationTime) as expected since the margin call end time will be shifted forward by 1 hour.

Vulnerability Details

Once a flagger invokes the flagShort() on the target Short, the function will verify that if the target Short was already flagged, the flagger will be able to flag the target Short again if and only if the previous flag has reached the margin call end time (resetLiquidationTime).

For example, if the resetLiquidationTime is 16 hours, the flagger can suddenly flag the target Short again after the timeDiff variable has passed 16 hours.

However, I discovered that the flagShort() does not function according to the above spec. Since the flagShort() 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 end time (resetLiquidationTime) will be shifted forward by 1 hour.

As a result, the flagger can re-flag the target Short when the timeDiff variable has passed 17 hours (not 16 hours as designed).

// FILE: https://github.com/Cyfrin/2023-09-ditto/blob/main/contracts/facets/MarginCallPrimaryFacet.sol
function flagShort(address asset, address shorter, uint8 id, uint16 flaggerHint)
external
isNotFrozen(asset)
nonReentrant
onlyValidShortRecord(asset, shorter, id)
{
if (msg.sender == shorter) revert Errors.CannotFlagSelf();
STypes.ShortRecord storage short = s.shortRecords[asset][shorter][id];
short.updateErcDebt(asset);
if (
short.getCollateralRatioSpotPrice(LibOracle.getSavedOrSpotOraclePrice(asset))
>= LibAsset.primaryLiquidationCR(asset)
) {
revert Errors.SufficientCollateral();
}
@> uint256 adjustedTimestamp = LibOrders.getOffsetTimeHours();
// check if already flagged
if (short.flaggerId != 0) {
@> uint256 timeDiff = adjustedTimestamp - short.updatedAt;
uint256 resetLiquidationTime = LibAsset.resetLiquidationTime(asset);
@> if (timeDiff <= resetLiquidationTime) {
revert Errors.MarginCallAlreadyFlagged();
}
}
short.setFlagger(cusd, flaggerHint);
emit Events.FlagShort(asset, shorter, id, msg.sender, adjustedTimestamp);
}
// FILE: https://github.com/Cyfrin/2023-09-ditto/blob/main/contracts/libraries/LibOrders.sol
function getOffsetTimeHours() internal view returns (uint24 timeInHours) {
@> return uint24(getOffsetTime() / 1 hours);
}

Proof of Concept

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

timeInSeconds = 3600
timeInHours = timeInSeconds / 3600
= 3600 / 3600
= 1
  • But if the timeInSeconds = 5400 (1.5 hours)

  • The timeInHours = 1 (not 1.5)

timeInSeconds = 5400
timeInHours = timeInSeconds / 3600
= 5400 / 3600
= 1 (rounding down; since Solidity has no fixed-point numbers)

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.

Impact

Subsequently, a short flagger can re-flag the target Short when the timeDiff variable has passed 17 hours (not 16 hours as designed).

This will break the functionality of the flagShort(), 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).

Tools Used

Manual Review

Recommendations

To fix the vulnerability, remove the edge case of timeDiff == resetLiquidationTime like the below snippet.

function flagShort(address asset, address shorter, uint8 id, uint16 flaggerHint)
external
isNotFrozen(asset)
nonReentrant
onlyValidShortRecord(asset, shorter, id)
{
if (msg.sender == shorter) revert Errors.CannotFlagSelf();
STypes.ShortRecord storage short = s.shortRecords[asset][shorter][id];
short.updateErcDebt(asset);
if (
short.getCollateralRatioSpotPrice(LibOracle.getSavedOrSpotOraclePrice(asset))
>= LibAsset.primaryLiquidationCR(asset)
) {
revert Errors.SufficientCollateral();
}
uint256 adjustedTimestamp = LibOrders.getOffsetTimeHours();
// check if already flagged
if (short.flaggerId != 0) {
uint256 timeDiff = adjustedTimestamp - short.updatedAt;
uint256 resetLiquidationTime = LibAsset.resetLiquidationTime(asset);
- if (timeDiff <= resetLiquidationTime) {
+ if (timeDiff < resetLiquidationTime) {
revert Errors.MarginCallAlreadyFlagged();
}
}
short.setFlagger(cusd, flaggerHint);
emit Events.FlagShort(asset, shorter, id, msg.sender, adjustedTimestamp);
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-569

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.