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:
-
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:
-
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:
- 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.
- 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:
- The solution should keep a fair competition between liquidators.
- The solution should not disrupt an existing mechanism for liquidators.
- The solution should not increase the amount of bad debt in the system - vice versa, it should encourage ecosystem participants to avoid bad debt.
- 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.
- The solution should not discourage liquidators from full liquidation if they want such; however, partial liquidation should become a preferable option.
- 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.):
-
\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:
-
\text{target } HF < LHF
Where LHF is a health factor when LF = 1.
Now we can reformulate the solution as:
-
\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):
-
LF = \frac{ \text{debt} }{ \sum_{i}^{n} \left( \text{collateral value}_i \times \text{collateral liquidation factor}_i \right) }
-
\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:
-
\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.
-
\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:
-
\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):
-
\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.
-
\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:
-
\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:
-
\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:
-
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
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