Skip to content

[WIP] feat: Partial Liquidation#374

Open
vlad-woof-software wants to merge 84 commits into
feat/service-patchfrom
wip-partial
Open

[WIP] feat: Partial Liquidation#374
vlad-woof-software wants to merge 84 commits into
feat/service-patchfrom
wip-partial

Conversation

@vlad-woof-software
Copy link
Copy Markdown
Collaborator

@vlad-woof-software vlad-woof-software commented Apr 3, 2026

Overview

This PR introduces partial liquidation support to the Comet protocol.
Instead of always seizing all collateral from an underwater account,
the liquidator now brings the account's health factor up to a configured
targetHealthFactor threshold - seizing only as much collateral as needed.
This reduces unnecessary value extraction from borrowers and better aligns
with the protocol RFC.

What has been done

Configuration

  • targetHealthFactor moved into CometConfiguration.Configuration struct - no longer a separate contract/interface, but a
    first-class config field
  • targetHealthFactor initialized in Comet and ScrollComet constructors from config
  • targetHealthFactor() added to CometMainInterface
  • Configurator updated to expose setTargetHealthFactor management

Core logic

  • Partial liquidation algorithm implemented in CometWithExtendedAssetList.absorbInternal: iterates collateral assets and either
    partially seizes one asset (stopping at targetHF) or fully seizes it and moves to the next
  • Formula: ∆ = (targetHF × debt − totalCV) × FACTOR_SCALE / (LP × targetHF − CF)
  • Falls back to original full-seizure behaviour when targetHF == 0

Tests

  • Initial test suite added in test/partial-liquidation-test.ts covering basic scenarios: single-collateral partial seizure,
    multi-collateral mixed seizure, full-liquidation fallback
  • Existing test suite fixed to account for new targetHealthFactor config field and updated makeProtocol setup

What is still pending (reason for WIP)

  • Test coverage: edge cases and full flow not yet covered (e.g. baseBorrowMin boundary, dust positions, rounding behaviour)
  • baseBorrowMin check: if remaining debt after partial seizure falls below baseBorrowMin, the protocol should fall through to
    full liquidation instead
  • Configurator-level validation that LP × targetHF > CF for all assets (prevents invalid configuration that would cause revert or
    division by zero at runtime)
  • Removal of remaining console.log debug calls from contracts
  • Internal review
  • Scenario / integration tests
  • Audit-readiness review

MishaShWoof and others added 30 commits August 13, 2025 13:38
…sible to reach targetHF to avoid underflow error
- Fix 1: correct numerator to (targetHF × debt − totalCV_CF) / (LP × targetHF − CF)
  instead of wrong LF-weighted totalCollaterizedValue2
- Fix 2: seizeAmount = rawCollateralUSD / price; seizedValue = rawCollateralUSD × LP
  (consistent with full-seizure path semantics)
- Fix 3: replace guard condition with denominator-positivity check + available balance
  check; remove pre-loop that computed totalCollaterizedValue2; simplify
  LiquidationData struct from 10 fields to 4
- Fix 4 incorrect finalIsLiquidatable assertions (true→false): after absorb
  account always exits liquidation zone regardless of path taken
- Add 3 new tests:
  * formula accuracy: verifies rawCollateralUSD, remaining balance > 0, HF ≈ targetHF
  * multi-collateral mixed: full COMP seizure then partial WETH seizure
  * full liquidation fallback: all collateral exhausted, debt zeroed by reserves
Comment thread contracts/CometWithExtendedAssetList.sol Fixed
Comment thread contracts/CometWithExtendedAssetList.sol Fixed
Comment thread contracts/CometWithExtendedAssetList.sol Fixed
uint256 seizedAmount;
uint256 seizedValue;

for (uint8 i; i < numAssets; ++i) {
Comment thread contracts/AssetList.sol
Comment on lines +145 to +151
if (assetConfig.borrowCollateralFactor != 0 && assetConfig.liquidateCollateralFactor == 0) {
revert CometMainInterface.BorrowCFTooLarge();
} else if (assetConfig.borrowCollateralFactor != 0 && assetConfig.liquidateCollateralFactor != 0) {
// Ensure collateral factors are within range
if (assetConfig.borrowCollateralFactor > assetConfig.liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge();
if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert CometMainInterface.LiquidateCFTooLarge();
}
Comment thread contracts/AssetList.sol
Comment on lines +147 to +151
} else if (assetConfig.borrowCollateralFactor != 0 && assetConfig.liquidateCollateralFactor != 0) {
// Ensure collateral factors are within range
if (assetConfig.borrowCollateralFactor > assetConfig.liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge();
if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert CometMainInterface.LiquidateCFTooLarge();
}
Comment thread contracts/AssetList.sol
Comment on lines +160 to +165
if (borrowCollateralFactor != 0 && liquidateCollateralFactor == 0) {
revert CometMainInterface.BorrowCFTooLarge();
} else if (borrowCollateralFactor != 0 && liquidateCollateralFactor != 0) {
// Be nice and check descaled values are still within range
if (borrowCollateralFactor >= liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge();
}
Comment thread contracts/AssetList.sol
Comment on lines +162 to +165
} else if (borrowCollateralFactor != 0 && liquidateCollateralFactor != 0) {
// Be nice and check descaled values are still within range
if (borrowCollateralFactor >= liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge();
}
Comment thread contracts/CometExt.sol
*/
function pauseCollateralAssetSupply(uint24 assetIndex, bool paused) override external onlyGovernorOrPauseGuardian isValidAssetIndex(assetIndex) {
if ((collateralsSupplyPauseFlags & (uint24(1) << assetIndex) != 0) == paused) revert CollateralAssetOffsetStatusAlreadySet(collateralsSupplyPauseFlags, assetIndex, paused);
if (!paused && isCollateralDeactivated(assetIndex)) revert CollateralIsDeactivated(assetIndex);
Comment thread contracts/CometExt.sol
*/
function pauseCollateralAssetTransfer(uint24 assetIndex, bool paused) override external onlyGovernorOrPauseGuardian isValidAssetIndex(assetIndex) {
if ((collateralsTransferPauseFlags & (uint24(1) << assetIndex) != 0) == paused) revert CollateralAssetOffsetStatusAlreadySet(collateralsTransferPauseFlags, assetIndex, paused);
if (!paused && isCollateralDeactivated(assetIndex)) revert CollateralIsDeactivated(assetIndex);
for (uint8 i; i < numAssets; ) {
if (isInAsset(assetsIn, i, _reserved)) {
asset = getAssetInfo(i);
if (fetchedCollateralPrices.length == 0) collateralPrices[i] = getPrice(asset.priceFeed);
Comment on lines +1269 to +1295
if (liquidation && asset.liquidateCollateralFactor == 0) {
unchecked { ++i; }
continue;
} else {
// Block ALL borrow-side actions when the borrower still holds deactivated collateral.
// This revert is intentionally broad: it prevents borrowing, withdrawing other
// collateral, and transferring — even if the remaining active collateral would
// pass the collateralization check on its own. The purpose is to force the
// borrower to withdraw the deactivated collateral FIRST before doing anything
// else (see the deactivation lifecycle comment on isCollateralDeactivated).
//
// If the borrower cannot withdraw the deactivated collateral without becoming
// under-collateralized, they are stuck and must wait for liquidation.
if (isCollateralDeactivated(asset.offset)) revert TokenIsDeactivated(asset.asset);

// Skip assets with borrowCollateralFactor == 0 — they provide no
// borrowing power, so mulFactor(value, 0) would add nothing to liquidity.
// More critically, this avoids calling getPrice() for their price feed:
// if a non-contributing asset's oracle reverts (stale, broken, decommissioned),
// it would otherwise block the entire collateralization check, paralyzing
// borrows and transfers for every account that holds that asset — even though
// the asset has zero influence on their borrow capacity.
if (asset.borrowCollateralFactor == 0) {
unchecked { ++i; }
continue;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Tests Tests refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants