Users can avoid liquidation while being under the primary liquidation ratio if on the last short record
The protocol permits users to maintain up to 254 concurrent short records. When this limit is reached, any additional orders are appended to the final position, rather than creating a new one. A short record is subject to flagging if it breaches the primary liquidation ratio set by the protocol, leading to potential liquidation if it remains below the threshold for a predefined period.\\nThe vulnerability emerges from the dependency of liquidation times on the `updatedAt` value of shorts. For the last short record, the appending of any new orders provides an alternative pathway for updating the `updatedAt` value of shorts, enabling users to circumvent liquidation by submitting minimal shorts to block liquidation by adjusting the time difference, thus avoiding liquidation even when they do not meet the collateral requirements for a healthy state.\\nlets take a look at the code to see how this works.\\nFlagging of Short Record:\\nThe `flagShort` function allows a short to be flagged if it's under `primaryLiquidationCR`, subsequently invoking `setFlagger` which updates the short's `updatedAt` timestamp to the current time.\\n```\\nfunction flagShort(address asset, address shorter, uint8 id, uint16 flaggerHint)\\n        external\\n        isNotFrozen(asset)\\n        nonReentrant\\n        onlyValidShortRecord(asset, shorter, id)\\n    {\\n        // initial code\\n\\n        short.setFlagger(cusd, flaggerHint);\\n        emit Events.FlagShort(asset, shorter, id, msg.sender, adjustedTimestamp);\\n    }\\n```\\n\\nLiquidation Eligibility Check:\\nThe `_canLiquidate` function assesses whether the flagged short is still under `primaryLiquidationCR` after a certain period and if it's eligible for liquidation, depending on the `updatedAt` timestamp and various liquidation time frames.\\n```\\nfunction _canLiquidate(MTypes.MarginCallPrimary memory m)\\n        private\\n        view\\n        returns (bool)\\n    {\\n       // Initial code\\n\\n        uint256 timeDiff = LibOrders.getOffsetTimeHours() - m.short.updatedAt;\\n        uint256 resetLiquidationTime = LibAsset.resetLiquidationTime(m.asset);\\n\\n        if (timeDiff >= resetLiquidationTime) {\\n            return false;\\n        } else {\\n            uint256 secondLiquidationTime = LibAsset.secondLiquidationTime(m.asset);\\n            bool isBetweenFirstAndSecondLiquidationTime = timeDiff\\n                > LibAsset.firstLiquidationTime(m.asset) && timeDiff <= secondLiquidationTime\\n                && s.flagMapping[m.short.flaggerId] == msg.sender;\\n            bool isBetweenSecondAndResetLiquidationTime =\\n                timeDiff > secondLiquidationTime && timeDiff <= resetLiquidationTime;\\n            if (\\n                !(\\n                    (isBetweenFirstAndSecondLiquidationTime)\\n                        || (isBetweenSecondAndResetLiquidationTime)\\n                )\\n            ) {\\n                revert Errors.MarginCallIneligibleWindow();\\n            }\\n\\n            return true;\\n        }\\n    }\\n}\\n```\\n\\nShort Record Merging:\\nFor the last short record, the `fillShortRecord` function combines new matched shorts with the existing one, invoking the `merge` function, which updates the `updatedAt` value to the current time.\\n```\\nfunction fillShortRecord(\\n        address asset,\\n        address shorter,\\n        uint8 shortId,\\n        SR status,\\n        uint88 collateral,\\n        uint88 ercAmount,\\n        uint256 ercDebtRate,\\n        uint256 zethYieldRate\\n    ) internal {\\n        AppStorage storage s = appStorage();\\n\\n        uint256 ercDebtSocialized = ercAmount.mul(ercDebtRate);\\n        uint256 yield = collateral.mul(zethYieldRate);\\n\\n        STypes.ShortRecord storage short = s.shortRecords[asset][shorter][shortId];\\n        if (short.status == SR.Cancelled) {\\n            short.ercDebt = short.collateral = 0;\\n        }\\n\\n        short.status = status;\\n        LibShortRecord.merge(\\n            short,\\n            ercAmount,\\n            ercDebtSocialized,\\n            collateral,\\n            yield,\\n            LibOrders.getOffsetTimeHours()\\n        );\\n    }\\n```\\n\\nIn the merge function we see that we update the updatedAt value to creationTime which is LibOrders.getOffsetTimeHours().\\n```\\nfunction merge(\\n        STypes.ShortRecord storage short,\\n        uint88 ercDebt,\\n        uint256 ercDebtSocialized,\\n        uint88 collateral,\\n        uint256 yield,\\n        uint24 creationTime\\n    ) internal {\\n        // Resolve ercDebt\\n        ercDebtSocialized += short.ercDebt.mul(short.ercDebtRate);\\n        short.ercDebt += ercDebt;\\n        short.ercDebtRate = ercDebtSocialized.divU64(short.ercDebt);\\n        // Resolve zethCollateral\\n        yield += short.collateral.mul(short.zethYieldRate);\\n        short.collateral += collateral;\\n        short.zethYieldRate = yield.divU80(short.collateral);\\n        // Assign updatedAt\\n        short.updatedAt = creationTime;\\n    }\\n```\\n\\nThis means that even if the position was flagged and is still under the `primaryLiquidationCR`, it cannot be liquidated as the `updatedAt` timestamp has been updated, making the time difference not big enough.\\n
Impose stricter conditions for updating the last short record when the position is flagged and remains under the `primaryLiquidationCR` post-merge, similar to how the `combineShorts` function works.\\n```\\nfunction createShortRecord(\\n        address asset,\\n        address shorter,\\n        SR status,\\n        uint88 collateral,\\n        uint88 ercAmount,\\n        uint64 ercDebtRate,\\n        uint80 zethYieldRate,\\n        uint40 tokenId\\n    ) internal returns (uint8 id) {\\n        AppStorage storage s = appStorage();\\n\\n        // Initial code\\n\\n        } else {\\n            // All shortRecordIds used, combine into max shortRecordId\\n            id = Constants.SHORT_MAX_ID;\\n            fillShortRecord(\\n                asset,\\n                shorter,\\n                id,\\n                status,\\n                collateral,\\n                ercAmount,\\n                ercDebtRate,\\n                zethYieldRate\\n            );\\n\\n    // If the short was flagged, ensure resulting c-ratio > primaryLiquidationCR\\n          if (Constants.SHORT_MAX_ID.shortFlagExists) {\\n                 if (\\n                     Constants.SHORT_MAX_ID.getCollateralRatioSpotPrice(\\n                         LibOracle.getSavedOrSpotOraclePrice(_asset)\\n                     ) < LibAsset.primaryLiquidationCR(_asset)\\n                 ) revert Errors.InsufficientCollateral();\\n                 // Resulting combined short has sufficient c-ratio to remove flag\\n                 Constants.SHORT_MAX_ID.resetFlag();\\n          }\\n            }\\n    }\\n```\\n
This allows a user with a position under the primaryLiquidationCR to avoid primary liquidation even if the short is in the valid time ranges for liquidation.
```\\nfunction flagShort(address asset, address shorter, uint8 id, uint16 flaggerHint)\\n        external\\n        isNotFrozen(asset)\\n        nonReentrant\\n        onlyValidShortRecord(asset, shorter, id)\\n    {\\n        // initial code\\n\\n        short.setFlagger(cusd, flaggerHint);\\n        emit Events.FlagShort(asset, shorter, id, msg.sender, adjustedTimestamp);\\n    }\\n```\\n