[WIP] feat: Partial Liquidation#1099
[WIP] feat: Partial Liquidation#1099vlad-woof-software wants to merge 124 commits intocompound-finance:mainfrom
Conversation
…ars, add NatSpec and inline comments
…ebtRemainingValue decrement Track remaining debt directly instead of accumulating seized value, moving the baseBorrowMin check post-seizure to avoid a two-step rounding cascade. Simplify the main seizure branch and add an early break when debt is fully repaid.
31a9201 to
f202f17
Compare
…ationState struct Move per-asset seizure logic into a dedicated `_absorbCollateral` internal function. Replace ad-hoc loop variables with a `LiquidationState` struct (debtRemainingValue, isHealthy, isFullDebtClosure) threaded across iterations. Add isFullDebtClosure mode to handle dust debt that spans multiple collateral assets. Fix repayAndSupplyAmount call to use the correct pre-update oldPrincipal.
…ng ones Added new test scenarios for partial liquidation logic. Updated legacy tests to use realistic collateral factor defaults. Temporarily skipped tests that are currently failing — will be fixed in follow-up commits.
…am and stale NatSpec
| emit SetTargetReserves(cometProxy, oldTargetReserves, newTargetReserves); | ||
| } | ||
|
|
||
| function setTargetHealthFactor(address cometProxy, uint64 newTargetHealthFactor) external governorOrMarketAdmin { |
There was a problem hiding this comment.
setTargetHealthFactor skips the MIN_TARGET_HEALTH_FACTOR check that the constructor enforces.
CometWithExtendedAssetList.sol:130 guards deployment with:
if (config.targetHealthFactor < MIN_TARGET_HEALTH_FACTOR) revert BadHealthFactor();
where MIN_TARGET_HEALTH_FACTOR = 105e16. But this setter writes any uint64 — including 0 — directly into configuratorParams[cometProxy].targetHealthFactor. Because governorOrMarketAdmin is broader than onlyGovernor, any market admin can quietly stage an out-of-range value.
Impact path:
Setter succeeds, event emits, getter reads back the invalid value — no revert.
The next deploy() call reverts in the Comet constructor with BadHealthFactor(), potentially after a governance proposal has already queued.
The phase-1 denominator mulFactor(LF, targetHF) - BCF becomes -BCF if targetHF = 0, which underflows uint256. Enforcing the bound at write time makes that state unreachable from storage.
Suggested:
function setTargetHealthFactor(address cometProxy, uint64 newTargetHealthFactor) external governorOrMarketAdmin {
if (newTargetHealthFactor < MIN_TARGET_HEALTH_FACTOR) revert BadHealthFactor();
uint64 oldTargetHealthFactor = configuratorParams[cometProxy].targetHealthFactor;
configuratorParams[cometProxy].targetHealthFactor = newTargetHealthFactor;
emit SetTargetHealthFactor(cometProxy, oldTargetHealthFactor, newTargetHealthFactor);
}
One wrinkle: MIN_TARGET_HEALTH_FACTOR currently lives as an internal constant inside CometWithExtendedAssetList.sol. To reference it from Configurator.sol without duplicating the literal, it probably wants to move to CometConfiguration.sol (or a shared constants file).
| uint public override immutable targetReserves; | ||
|
|
||
| /// @notice The target health factor for partial liquidation | ||
| uint public override immutable targetHealthFactor; |
There was a problem hiding this comment.
Is targetHealthFactor intentionally a no-op in Comet.sol / ScrollComet.sol? I see it gets stored in the constructors but isn't referenced in their absorbInternal. Asking because the constant MIN_TARGET_HEALTH_FACTOR and the corresponding revert check only exist in CometWithExtendedAssetList.sol — wanted to confirm that's by design (interface uniformity) rather than an oversight before reading further.
| // numAssets lives in storage and would be re-read on every iteration - cache it once. | ||
| uint8 _numAssets = numAssets; | ||
|
|
||
| for (uint8 i; i < _numAssets;) { |
There was a problem hiding this comment.
Since it breaks on the first asset that satisfies phase-1, the asset at the lowest assetsIn index is the one whose balance gets reduced. Under full liquidation the order didn't matter; under partial it does.
Example: index 0 is Asset A (LF 0.85), index 1 is Asset B (LF 1.05), both $1k. wantedCollateralValue is smaller for B (higher LF in the denominator), but A gets hit first because of index order. So governance's addAsset order effectively decides which collateral the borrower loses.
Is list order intended to be part of the spec here? If yes, worth a line in SPEC.md + a comment above absorbInternal so integrators know. If not, sorting assetsIn by LF desc before the loop would make it deterministic on economics instead of history.
|
Balance tranfer account https://revolut.me/r/eZ37KPjLkY |
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
targetHealthFactorthreshold - 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
targetHealthFactormoved intoCometConfiguration.Configurationstruct - no longer a separate contract/interface, but afirst-class config field
targetHealthFactorinitialized inCometandScrollCometconstructors from configtargetHealthFactor()added toCometMainInterfaceConfiguratorupdated to exposesetTargetHealthFactormanagementCore logic
CometWithExtendedAssetList.absorbInternal: iterates collateral assets and eitherpartially seizes one asset (stopping at
targetHF) or fully seizes it and moves to the next∆ = (targetHF × debt − totalCV) × FACTOR_SCALE / (LP × targetHF − CF)targetHF == 0Tests
test/partial-liquidation-test.tscovering basic scenarios: single-collateral partial seizure,multi-collateral mixed seizure, full-liquidation fallback
targetHealthFactorconfig field and updatedmakeProtocolsetupWhat is still pending (reason for WIP)
baseBorrowMinboundary, dust positions, rounding behaviour)baseBorrowMincheck: if remaining debt after partial seizure falls belowbaseBorrowMin, the protocol should fall through tofull liquidation instead
LP × targetHF > CFfor all assets (prevents invalid configuration that would cause revert ordivision by zero at runtime)
console.logdebug calls from contracts