[RFC] Partial Liquidations

Intro

Liquidations are bad for both protocol and users:

  • Liquidated positions don’t accrue interest (fees).
  • Borrowers lose collateral.

Liquidations are inevitable, but damage can be decreased by liquidating a part of the borrowing position, instead of absorbing the full collateral value.

Partial liquidations are a key component in increasing the Collateral Factor (Loan-to-Value).

This post describes how partial liquidations can be implemented within the Compound v3 infrastructure.

Motivation

The current Compound Comet model is based on the evaluation of the health factor of the user’s account based on the collateralization factors for each collateral asset.

Note: To lay the basis, let’s start from the loan-to-value (LTV) definition, which characterizes the user’s position in the protocol. Based on the agreed notion (in both traditional sources, web3 resources, and Compound itself), LTV is always defined as the ratio of debt / collateral value. And, transitionally, the same applies to collateral factors and liquidation factors, represented in numbers below 100% (below 1). Therefore, we will keep the same notion and represent Health Factor (HF) as a ratio of debt / collateral value (scaled by collateral factors). So, if HF is less than 1, then the account’s collateral (scaled) covers the debt; if the collateral factor is more than 1, then the debt exceeds the collateral value. And it is in congruence with the loan-to-value ratio, as the health factor is its direct descendant.

HF (Health Factor) is a ratio between debt and collateral value, factored by the collateral factor:

  1. HF = \frac{ (\text{principal} + \text{interest}) \times \text{base asset price} }{ \sum_{i}^{n} \left( \text{collateral balance}_i \times \text{collateral price}_i \times \text{collateral factor}_i \right) }

Once HF increases above 1, the user is not able to increase the loan. However, a user’s liquidation occurs only after the user falls under the liquidation threshold.

LF (liquidation factor) is a ratio between the user’s debt and the collateral value, factored by the collateral liquidation factor:

  1. LF = \frac{ (\text{principal} + \text{interest}) \times \text{base asset price} }{ \sum_{i}^{n} \left( \text{collateral balance}_i \times \text{collateral price}_i \times \text{collateral liquidation factor}_i \right) }

The protocol keeps HF >= LF (as collateral factor < collateral liquidation factor)

Once LF grows above 1, the user will be liquidated. And in this case, the protocol seizes the full amount of collateral, though it leaves a difference after the debt coverage with the discounted value of the collateral.

Such an approach leads to 2 issues:

  1. Users (in general) lose all of their collateral.

While it is consistent with the protocol’s mechanics, which protects it from bad debts, it creates a downside for a user willing to pay the interest but short on time to collect additional collateral. Thus, such a user (with the health factor pushed to be right above the liquidation threshold) who could bring more profit in perspective by paying interest, will lose the position. Another factor to take into consideration - if the user has more collateral value (scaled down by penalty) than the debt, then the difference between collateral value and debt is returned to the user as principal balance in the base asset. Thus, the user gets a “virtual swap” of collaterals to the base asset, which (after the withdrawal) will decrease the overall supply - and so it acts as a factor that contaminates the growth of the reserves.

  1. Liquidators should cover the whole debt position.

By default, it is assumed that the protocol absorbs the position below the liquidation threshold. In that case, the protocol closes the user’s debt and seizes collateral with a penalty into the reserves. So there exists a “virtual swap” where the protocol “swaps” debt denominated in base asset to collateral denominated in other assets. So, the base asset supply is increased by a value of closed debt, but it is actually presented as a sum of collaterals instead of the appropriate amount in the underlying. And since the protocol should protect lenders who provided the liquidity, it should bring the same base asset amount back to the pool. Thus, the protocol sells collaterals back to liquidators at a discounted price, but this is still higher than the liquidation factor. So, after 2 actions (debt absorption and collateral sell), the protocol gets back the necessary base asset amount. However, this operation assumes that liquidators will have enough base asset amount in the first place, and that assumption limits the number of liquidators willing to participate in the liquidation process and limits the retention of the base asset liquidity back to the protocol.

The main motivation under the partial liquidation is:

  • To decrease the average amount of base assets, the liquidators should have to participate in the liquidation process;
  • Decrease the number of cases when the user loses the whole position and increase the lifetime of the position, maximizing the amount of potential interest.

And vice versa, the partial liquidation itself brings additional motivation for the ecosystem participants:

  • It gives a mechanism that will help users keep the account’s health for a longer period.
  • It encourages liquidators with low- and medium-sized portfolios to participate in liquidations;
  • Together, both mechanisms work on keeping healthy accounts and avoiding bad debts, as they operate with smaller amounts.
  • Additionally, partial liquidation works on the prevention of cascade liquidations of monolithic liquidity, working on the health of the whole system

Basis

Before the solution development, we define the basis for it - as the partial liquidation should not disrupt existing protocol mechanics. Therefore, we base the solution on the following principles:

  1. The solution should keep a fair competition between liquidators.
  2. The solution should not disrupt an existing mechanism for liquidators.
  3. The solution should not increase the amount of bad debt in the system - vice versa, it should encourage ecosystem participants to avoid bad debt.
  4. The solution should, at maximum scale, re-use the existing liquidation mechanics. We need to avoid building a new infrastructure or implementing breaking changes into the existing liquidation process.
  5. The solution should not discourage liquidators from full liquidation if they want such; however, partial liquidation should become a preferable option.
  6. The solution should keep minimal changes in the protocol’s codebase, ideally, with no modifications in existing functions, but only with the implementation of new ones.

Solution

The solution can be expressed in a single sentence: give liquidators a debt amount they should cover for a user, to bring the user’s HF back above the liquidation threshold.

If expressed in a formula (using HF and LF formulas from 1. and 2.):

  1. \text{If } (LF > 1), \text{ then find such } \Delta \text{debt so that } LF < 1

However, having LF as a target is inconvenient, as once we bring it back to 1, the accrued interest will exceed the LF again. Instead, we’ll use the fact that when LF = 1 it corresponds to a certain value of the health factor, which we can name LHF (liquidation health factor).

Let LHF (liquidation health factor) correspond to HF when LF = 1.

Example:

  • collateral factor = 0.825
  • collateral liquidation factor = 0.85
  • Collateral price = 2000$
  • Collateral balance = 1

It gives:

  • HF = 1 with maximum debt of 1650$ (collateral balance * price * collateral factor)
  • LF = 1 with debt exceeding 1700$ (collatearal balance * price * liquidation factor)
  • When LF = 1, the appropriate health factor equals:
    LHF = debt / (collateral balance * price * collateral factor) = 1700$ / 1650$ = 1.03

So, now we can define the target HF (or min HF) to achieve, to keep account above the liquidation threshold:

  1. \text{target } HF < LHF

Where LHF is a health factor when LF = 1.

Now we can reformulate the solution as:

  1. \text{If user's } LF \geq 1, \text{ then find such } \Delta \text{debt so that } HF = \text{target } HF

Note: In Comet, all debt balance calculations are performed in terms of principal scaled by the borrow index, to represent (principal + interest). So we can safely refer to debt as (principal + interest) to simplify the equation. Also, let’s define debt as (debt * base asset price) to avoid extra multiplier in notation. Additionally let’s substitute collateral value calculations with collateral value = (collateral balance * collateral price) - again to avoid extra price multiplier (since it is never used without connection to the balance)

Expressed in formula (based on formulas (1) and (2):

  1. LF = \frac{ \text{debt} }{ \sum_{i}^{n} \left( \text{collateral value}_i \times \text{collateral liquidation factor}_i \right) }
  2. \text{target } HF = \frac{ \text{debt} - \Delta \text{debt} }{ \sum_{i}^{n} \left( (\text{collateral value}_i - \Delta \text{collateral value}_i) \times \text{collateral liquidation factor}_i \right) }

Compound siezes the collateral from the user with a penalty. So:

  1. \text{debt} \leq \sum_i \left( \text{collateral value}_i \times \text{liquidation penalty}_i \right)

We can safely use the border case of equality - when the penalized collateral fully covers the debt and equals it. In case the debt exceeds the available collateral, the difference will just be added to the user’s balance, in the same way it works during the absorption.

  1. \text{debt} = \sum_i \left( \text{collateral value}_i \times \text{liquidation penalty}_i \right)

It allows us to express the difference the liquidator should pay as:

  1. \Delta \text{debt} = \sum_i \left( \Delta \text{collateral value}_i \times \text{liquidation penalty}_i \right)

And now the task is to find the 𝚫collateral value to substitute it and get 𝚫debt the liquidator should pay.

Using equation (7):

  1. \text{target } HF = \frac{ \text{debt} - \sum_{i}^{i} \left( \Delta \text{collateral value}_i \times \text{liquidation penalty}_i \right) }{ \sum_{i}^{n} \left( \left( \text{collateral value}_i - \Delta \text{collateral value}_i \right) \times \text{collateral factor}_i \right) }

This is the place to apply a rule: the liquidator should set the collateral to seize and the order of collaterals to get and use one collateral at a time. We need to apply such a rule to simplify the equation:

  • we cannot assume that the liquidation penalty, liquidation factor and collateral factor are the same for all collaterals;
  • We cannot limit partial liquidations to single-colateral loans.
  • We cannot work with equations with multiple unknowns
  • So we need to split it into a system of consecutive equations.
  1. \text{target } HF = \frac{ \text{debt} - \Delta \text{collateral value} \times \text{liquidation penalty} }{ \left( \text{collateral value} - \Delta \text{collateral value} \right) \times \text{collateral factor} }

Now, by simplifying expression (12) we can express delta collateral:

  1. \Delta \text{collateral value} = \frac{ \text{debt} - \text{collateral value} \times \text{collateral factor} \times \text{target } HF }{ \text{liquidation penalty} - \text{collateral factor} \times \text{target } HF }

Returning to the 𝚫debt equation (10), we can give to the liquidator the amount of base asset the liquidator should pay to seize a certain collateral to bring the user back above the liquidation threshold.

Edge-cases

The solution also covers the row of exceptions and edge cases:

Q: What if the user does not have enough collateral of this type (equation (12))?
A: The rule states to use one collateral at a time and to define an order of collaterals to seize. So, if the calculation shows 𝚫collateral value that exceeds the collateral balance of the user - just use the full balance, and go to the next collateral to calculate its 𝚫 - up until there are enough collaterals:

  1. \text{target } HF = \frac{ \text{debt} - \left( \text{collateral value}_1 \cdot \text{liquidation penalty}_1 + \Delta \text{collateral value}_2 \cdot \text{liquidation penalty}_2 \right) }{ \left( \text{collateral value} - \text{collateral value}_1 \cdot \text{collateral factor}_1 - \Delta \text{collateral value}_2 \cdot \text{collateral factor}_2 \right) }
  • And calculation repeats with the updated debt value and new collateral
  • The cycle for calculation will be similar to the ones used in isBorrowCollateralized() and isLiquidatable() functions

Q: What if the debt from the start falls under the condition of the bad debt (equation (8) is disrupted) ?
A: Have a check on bad debt before any partial liquidation, similar to the collateral or liquidation checks in withdraw() and absorb() functions. And then follow the existing liquidation flow.

Q: Is this partial liquidation consistent with the full liquidation mechanics, which are already implemented?
A: In short - yes.
The maximum value that can be liquidated is debt itself.
The maximum value the user can lose is:

  • The whole collateral amount in case of bad debt;
  • Exactly all collateral if 𝚺(collateral value_i * liquidation penalty_i) = debt (if penalized collateral covers debt exactly);
  • 𝚺(collateral value_i) - (𝚺(collateral value_i * liquidation penalty_i) - debt) otherwise (in case if penalized collateral covers more than current debt)

Debt to repay in partial liquidation is calculated exactly based on penalized collateral, thus neither user can lose more than that during full liquidation, nor can the liquidator acquire more assets than that during full liquidation.

Q: The equations are based on the equality of debt calculation and penalized collateral value. What if the user falls under the liquidation threshold, but still has enough collateral to cover the debt with a penalty?
A: Assumption is based on the fact that penalized collateral will be seized anyway in case of full liquidation, thus it’s safe to calculate it this way from the start. During partial liquidation, the user cannot lose more collateral than necessary to cover the debt. Thus, the other difference will remain.

Q: What is the minimal amount of debt to be repaid via the partial liquidation flow?
A: The 𝚫debt calculated is actually the smallest amount of debt to repay to bring the account back to the target HF. So, it depends on the target HF set for a pool. And, to avoid micro payments, the functionality will be enforced to have the user’s LF <= 1 at the end of the function.

However, additional restrictions can be implemented to bound the partial liquidation to a % of a user’s debt to cover (e.g., not less than 20% of the debt with fulfillment of the equations above).

Q: How to calculate the target HF (target health factor) ?
A: Based on the definition, the next should be applicable:

HF <= target HF <= LHF

So, it can have a respective storefront coefficient, as
Target HF = x% of LHF

For example:

  • With the example used in the LHF we have LHF = 1,03
  • The storefront coefficient can be set as: x = 98%
  • Target HF = x% of LHF = 98% of LHF = 1,0094

So it can be easily computed for each user respectively as:

  1. LHF = \frac{ \sum_{i}^{n} \left( \text{collateral value}_i \cdot \text{collateral liquidation factor}_i \right) }{ \sum_{i}^{n} \left( \text{collateral value}_i \cdot \text{collateral factor}_i \right) }

Target HF = x% * LHF

Q: How to encourage liquidators to perform partial liquidations instead of full liquidations?
A: There are several ideas that require feedback:

  • (requires new parameter for assets): Allow partial liquidation to start earlier than full liquidation - implement a sub-liquidation factor (lower than the liquidation factor), which will be used to check if a user is eligible for the partial liquidation (partial liquidations will start on a lower HF than full liquidation).
  • (requires new parameter for assets): Increase the discount on sold collateral during the partial liquidation operation compared with the full liquidation (liquidators get more collateral cumulatively);
  • (requires new parameter for assets): Increase the penalty on seized collateral compared with the full liquidation (so more collateral is seized in general compared with full liquidation);
  • Add more liquidation points during the partial liquidation (to influence further rewards distribution).
  • Boost partial liquidation with separate rewards.
  • Add a bonding curve or a linear decrease in the discount for the liquidation, from minimal to the full (so that immediate partial liquidation will bring more cumulative profit than the full liquidation of the same amount).

For now, we prefer to have the combination of “early start” for partial liquidations together with the linear growth of liquidation discount:

  • Introduce a new threshold for assets - PLF, partial liquidation factor (collateral factor <= PLF <= liquidation factor). It will define a respective PLHF (partial liquidation health factor). Thus, HF <= PLHF <= LHF <= bad debt health factor
  • That gives an interval for the debt to fall (from PLHF to LHF) and be eligible for partial liquidation
  • Bound the liquidator’s discount to the Target HF / actual HF value, to have an increased discount for early partial liquidations
  • Once LHF is achieved, the liquidator’s discount is the same for full liquidation and partial liquidation, and it is up to a liquidator to decide.
  • Note: idea is equivalent to having a liquidation penalty linearly dependent on the Target HF / actual HF ratio. Thus, liquidation penalty is another parameter to work with - with the currently set penalty (in assetInfo.liquidationFactor) staying as the final lowest penalty to achieve.

Modeling results

Partial liquidation modeling

Implementation

Implement new storage:

  • Global parameter to represent the storefront coefficient for the decrease of LHF to HF (for Target HF calculation).

Implement new functions:
1. collateralValues()

  • The utility function that gets collateral values and caches them (to avoid frequent oracle calls)
  • Structure is similar to isLiqidatable() and coisBorrowCollateralized()
  • Returns array of collateral values

2. isBadDebt()

  • Structure is similar to isLiqidatable() and coisBorrowCollateralized()
  • Uses cached collateral values from collateralValues()
  • Checks the factorized sum based on assetInfo.liquidationFactor

3. Additional viewers:

  • targetHF() - function to calculate the minimal health factor account needed to set account above the liquidation threshold
  • minimalDebt() - function to return the minimal debt value to repay to set up over the liquidation threshold (based on equations (10) and (13))
  • collateralForDebt() - helper viewer based on the equation (13) to show how much collateral can be seized for a partially closed debt.

4. absorbPartial()

  • Gets an array of collaterals to seize and account to liquidate
  • Gets amount of debt the liquidator is willing to repay
  • Checks if the user has bad debt
  • Uses cached collateral values from collateralValues()
  • Calculates LHF for the user
  • Calculates Target HF for the user
  • Calculates delta collateral based on equations (13) and (14) - cycle is similar to isLiqidatable() and coisBorrowCollateralized() (note - all prices are cached at the beginning of the function, so no additional storage reads are present)
  • Calculates the minimal delta debt to cover
  • Expect not less debt to be covered than delta debt.
  • Updates the user’s principal, total supply and total borrow values
  • enforces the rule that the health factor after the absorption should be <= target HF

In general, the function’s structure is similar to the absorb() function

And so the same is the next step for the liquidator - call buyCollateral() function to return the base asset into the pool. This call can be enforced in the same function (the result depends on the final gas testing).

5. Changes on an incentivisation of partial liquidations depend on the final decision on the form of liquidation. Though the preferred approach includes:

  • Adding a new factor for assets (recorded in assetInfo) - PLHF;
  • Adding new viewers similar to isLiquidatable() : isPartiallyLiquidatable()
  • Viewer to calculate starting discount and final discount based on linear model and with existing values (assetInfo,liquidationFactor and storefrontPriceFactor) as input values
  • Integration with the new partialAbsorb() function and buyColalteral() function.

Next steps

  • Community Discussion
  • Technical Scoping
  • Snapshot Vote
  • Implementation
7 Likes

I’m not sure I agree with this statement. In the past we’ve seen liquidations actually contribute to massive growth of reserves of a market because of the corresponding sale of the collateral back to users who wish to purchase it at a discount. The sale of the collateral absorbed from the liquidation is what contributes to reserve growth as well.

This section is not quite true either. The protocol actually is what assumes the whole debt position (in the form of reducing reserves). The protocol then recovers its reserves by selling the seized collateral at a discount to anyone who wants to buy it. The selling of the seized collateral is what then contributes to reserves once more to fill in the gap made by taking ownership of the absorbed debt. No liquidator needs to cover the whole of anything, rather buyers of the seized collateral only need to worry about how much of whatever seized collateral they want to purchase from the protocol collateral reserves. It is for this case that reserves are even allowed to go negative during an absorb while the seized collateral is waiting to be bought.


What if we instead start with adjusting the absorb() function to some percentage of a user’s position? (This was tough to identify in v2 what this percentage level should be depending on various states of prices in the market). We could come up with a model that says at minimum x% of a user’s total debt (vs 100% currently) is able to be absorbed by the protocol at all times which is a bit safer for the user to defend their position while also ensuring a position is also attractive for users who wish to trigger the absorption. I do not think that only allowing the current underwater debt amount to be absorbed is the correct approach, especially in severe conditions but I would defer risk on this to Guantlet. Adjusting the absorb() is probably the quickest change to implement as well with minimal changes to the whole liquidation system.

If the protocol is responsible for absorbing a position, the protocol can also attempt to seize all collateral fairly vs a case where some liquidators may only want specific collateral assets to seize if the protocol leaves the decision up to a liquidator. This is a far better experience for the borrower. I do still think allowing anyone to purchase seized assets from the protocol itself is a great experience for anyone who wants to purchase assets at a discount, though traditional liquidators are the ones here who are probably the quickest. Allowing liquidators to choose which collateral they want is likely pretty bad for borrowers as only the highest collateral factor assets are likely to be taken first meaning the borrower will lose those assets first in a falling market environment.

There should be discussion however on Liquidator Points, as that is what is accrued to users who liquidate by calling absorb() which absorbs bad debt positions into the protocol. Currently Liquidator Points are tracked but nothing is currently done with those points.

To give a little context, the v2 Compound Protocol does support partial liquidations, however we designed the separate Liquidation Factor (LF) in v3 in order to better protect user’s positions by giving them some extra buffer to defend their positions beyond just the Collateral Factor. Additionally, for v3 we made the decision to absorb an entire debt position during a liquidation vs reading a Close Factor that existed in v2 because we noticed that there were lots of positions in v2 that could never be closed resulting in just stale assets that will likely just sit there forever. I think we can find a better middle ground here.

3 Likes

Hey, thank you for the detailed feedback! I’m Pavlo, a Solutions Architect at WOOF! who worked on the solution and its reasoning. Let me answer the raised statements.

  1. The core question is - what exactly should be that percentage x% of a user’s debt that the protocol will allow to repay? The point is - it should be enough to bring the user above the liquidation threshold and it should be enough to not trigger immediate liquidation again. The post above already provides an approach to resolving minimal debt to achieve a target health factor. So, keeping all calculations, it may serve as a minimal target, AND now we can add a percentage of the repaid debt upon it (thus increasing the repaid amount but still keeping it partial).
  2. The next logical query: why should we still have the target HF and minimal debt calculation instead of just having a fixed % to repay? First of all, it is an orienteer for liquidators on the amount of debt enough to bring the user back into the healthy account, and secondly - it works for the flexibility, as seizing fixed % of debt in any situation is still in some sense analogous to a full liquidation but with less collateral seized, and it still limits small- and min-sized liquidators.
  3. However, in general - yes, %x of the debt can be implemented, but it still will require most of the checks described in the document. So I’d still advise to have all underground calculations - but they can be covered with a nice % parameter for convenience. Though, in some way, it will not be a partial liquidation in its pure definition, but a fixed % liquidation. It is a better condition for a user than a full liquidation, but still harsh enough.
  4. As for keeping collateral purchase and absorption separate. That is fair, though an approach to unite these operations for partial liquidations was offered to allow liquidators to choose the collateral to seize and purchase it immediately + to have adjustments for the liquidator’s discount based on partial liquidations. Without it, the incentive to push the partial liquidation over the full liquidation will be only in a specific collateral choice and maybe the implementation of a higher penalty (as the liquidator should be able to have some incentive). So, it is still an option to consider, as dividing these functions is convenient for the protocol itself (in case the protocol triggers the liquidation), but as for the liquidator, it will be convenient to have the atomic seizing of the collateral. So, the functionality can be left intact for protocol-based liquidations (with a registry of protocol’s liquidators) and can be united for a partial liquidation flow. On the implementation level it can be just a flag in the absorb() function indicating if the liquidator is willing to purchase collateral immediately.
  5. As for the liquidator points as incentives (instead of playing around penalty or discount) - it is a point of discussion, as indeed it is a whole functionality unused but which can be utilized for incentives - implementation can contain bonus points for the partial liquidations.
  6. As for LF implementation discussion - indeed, and LF is the exact border we are playing around with a partial liquidation as well.
  7. As for positions that are never closed - that is a completely different question, probably for working with position maturity. E.g., mature positions cannot be eligible for partial liquidations, only for full liquidations. That will solve the problem of stale liquidity, still allowing the partial liquidation. However, with current absorption flow it seems not to be a problem - as even with partial liquidations, there is a threshold after which the position (decreased during several partial liquidations) will be closed completely with no stale liquidity in the end.

So, in short:

  • %x over debt can still be implemented with the target HF and minimal debt under it - though we still think that higher flexibility will work better for liquidators as it will engage new audience
  • split between absorption / collateral purchase - it can be split for the protocol-triggered liquidations, though it is still advised to be united for partial liquidations (implemented as a simple flag in a absorb() function)
  • as for liquidator points - that is a good incentive mechanism which is currently unused. We can offer e.g. a reversed bonding curve to connect liquidator points to a percentage of debt closed. Though in that case, an additional rewards mechanism should be developed - e.g., a higher discount during a collateral purchase based on liquidator’s points accumulated (thus it will trigger slow burning of liquidator points without the need to unite operations of absorption and purchase)
  • to avoid stale liquidity, an additional maturity factor can be introduced, after which only full liquidations are allowed, though again - it seems not to be a problem, as position will be closed anyway after a certain threshold/number of partial liquidations
1 Like