Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve close factor #1322

Merged
merged 17 commits into from
Sep 7, 2022
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1236](https://github.com/umee-network/umee/pull/1236) Improve leverage event fields.
- [1294](https://github.com/umee-network/umee/pull/1294) Simplify window progress query math.
- [1300](https://github.com/umee-network/umee/pull/1300) Improve leverage test suite and error specificity.
- [1322](https://github.com/umee-network/umee/pull/1322) Improve complete liquidation threshold and close factor.
- [1332](https://github.com/umee-network/umee/pull/1332) Improve reserve exhaustion event and log message.

### Bug Fixes
Expand Down
6 changes: 3 additions & 3 deletions x/leverage/keeper/collateral.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Coin {
// collateral sdk.Coins, using each token's uToken exchange rate.
// An error is returned if any input coins are not uTokens or if value calculation fails.
func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
limit := sdk.ZeroDec()
total := sdk.ZeroDec()

for _, coin := range collateral {
// convert uToken collateral to base assets
Expand All @@ -118,10 +118,10 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins)
}

// add each collateral coin's weighted value to borrow limit
limit = limit.Add(v)
total = total.Add(v)
}

return limit, nil
return total, nil
}

// GetAllTotalCollateral returns total collateral across all uTokens.
Expand Down
6 changes: 3 additions & 3 deletions x/leverage/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -848,9 +848,9 @@ func (s *IntegrationTestSuite) TestLiquidate() {
closeBorrower,
coin(umeeDenom, 200_000000),
"u/" + umeeDenom,
coin(umeeDenom, 21_216000),
coin("u/"+umeeDenom, 23_337600),
coin("u/"+umeeDenom, 23_337600),
coin(umeeDenom, 7_752000),
coin("u/"+umeeDenom, 8_527200),
coin("u/"+umeeDenom, 8_527200),
nil,
},
}
Expand Down
50 changes: 38 additions & 12 deletions x/leverage/keeper/liquidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ func (k Keeper) getLiquidationAmounts(
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}
collateralValue, err := k.CalculateCollateralValue(ctx, borrowerCollateral)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}
liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, borrowerCollateral)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}
if liquidationThreshold.GTE(borrowedValue) {
if borrowedValue.LT(liquidationThreshold) {
// borrower is healthy and cannot be liquidated
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationIneligible
}
Expand All @@ -50,6 +54,7 @@ func (k Keeper) getLiquidationAmounts(
params := k.GetParams(ctx)
closeFactor := ComputeCloseFactor(
borrowedValue,
collateralValue,
liquidationThreshold,
params.SmallLiquidationSize,
params.MinimumCloseFactor,
Expand Down Expand Up @@ -186,16 +191,35 @@ func ComputeLiquidation(
return tokenRepay, collateralBurn, tokenReward
}

// ComputeCloseFactor derives the maximum portion of a borrower's current
// borrowed value can currently be repaid in a single liquidate transaction.
// ComputeCloseFactor derives the maximum portion of a borrower's current borrowedValue
// that can currently be repaid in a single liquidate transaction.
//
// closeFactor scales linearly between minimumCloseFactor and 1.0,
// reaching its maximum when borrowedValue has reached a critical value
// between liquidationThreshold to collateralValue.
// This critical value is defined as:
//
// B = critical borrowedValue
// C = collateralValue
// L = liquidationThreshold
// CLT = completeLiquidationThreshold
//
// B = L + (C-L) * CLT
//
// closeFactor is zero for borrowers that are not eligible for liquidation,
// i.e. borrowedValue < liquidationThreshold
//
// Finally, if borrowedValue is less than smallLiquidationSize,
// closeFactor will always be 1 as long as the borrower is eligible for liquidation.
func ComputeCloseFactor(
borrowedValue sdk.Dec,
collateralValue sdk.Dec,
liquidationThreshold sdk.Dec,
smallLiquidationSize sdk.Dec,
minimumCloseFactor sdk.Dec,
completeLiquidationThreshold sdk.Dec,
) (closeFactor sdk.Dec) {
if !liquidationThreshold.IsPositive() || borrowedValue.LTE(liquidationThreshold) {
if borrowedValue.LT(liquidationThreshold) {
// Not eligible for liquidation
return sdk.ZeroDec()
}
Expand All @@ -206,19 +230,21 @@ func ComputeCloseFactor(
}

if completeLiquidationThreshold.IsZero() {
// If close factor is set to unlimited by global params
// If close factor is set to unlimited
return sdk.OneDec()
}

// outside of special cases, close factor scales linearly between MinimumCloseFactor and 1.0,
// reaching max value when (borrowed / threshold) = 1 + CompleteLiquidationThreshold
// Calculate the borrowed value at which close factor reaches 1.0
criticalValue := liquidationThreshold.Add(completeLiquidationThreshold.Mul(collateralValue.Sub(liquidationThreshold)))

closeFactor = Interpolate(
borrowedValue.Quo(liquidationThreshold).Sub(sdk.OneDec()), // x
sdk.ZeroDec(), // xMin
minimumCloseFactor, // yMin
completeLiquidationThreshold, // xMax
sdk.OneDec(), // yMax
borrowedValue, // x
liquidationThreshold, // xMin
minimumCloseFactor, // yMin
criticalValue, // xMax
sdk.OneDec(), // yMax
)

if closeFactor.GTE(sdk.OneDec()) {
closeFactor = sdk.OneDec()
}
Expand Down
80 changes: 80 additions & 0 deletions x/leverage/keeper/liquidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,83 @@ func TestComputeLiquidation(t *testing.T) {
uDust.liquidationIncentive = sdk.MustNewDecFromStr("0")
runTestCase(uDust, 2, 1, 29, "high exchange rate collateral dust")
}

func TestCloseFactor(t *testing.T) {
require := require.New(t)

type testCase struct {
borrowedValue sdk.Dec
collateralValue sdk.Dec
liquidationThreshold sdk.Dec
smallLiquidationSize sdk.Dec
minimumCloseFactor sdk.Dec
completeLiquidationThreshold sdk.Dec
}

baseCase := func() testCase {
// returns a liquidation scenario where close factor will reach 1 at a borrowed value of 58
return testCase{
sdk.MustNewDecFromStr("50"), // borrowed value 50
sdk.MustNewDecFromStr("100"), // collateral value 100
sdk.MustNewDecFromStr("40"), // liquidation threshold 40
sdk.MustNewDecFromStr("20"), // small liquidation size 20
sdk.MustNewDecFromStr("0.1"), // minimum close factor 10%
sdk.MustNewDecFromStr("0.3"), // complete liquidation threshold 30%
}
}

runTestCase := func(tc testCase, expectedCloseFactor string, msg string) {
closeFactor := keeper.ComputeCloseFactor(
tc.borrowedValue,
tc.collateralValue,
tc.liquidationThreshold,
tc.smallLiquidationSize,
tc.minimumCloseFactor,
tc.completeLiquidationThreshold,
)

require.Equal(sdk.MustNewDecFromStr(expectedCloseFactor), closeFactor, msg)
}

// In the base case, close factor scales from 10% to 100% as borrowed value
// goes from liquidation threshold ($40) to a critical value, which is defined
// to be 30% of the way between liquidation threshold and collateral value ($100).
// Since the borrowed value of $50 is 5/9 the way from liquidation threshold to
// the base case's critical value of $58, the computed close factor will be
// 5/9 of the way from 10% to 100% - thus, 60%.
runTestCase(baseCase(), "0.6", "base case")

// If borrowed value has passed the critical point, close factor is 1
completeLiquidation := baseCase()
completeLiquidation.borrowedValue = sdk.MustNewDecFromStr("60")
runTestCase(completeLiquidation, "1", "complete liquidation")

// If borrowed value is less than small liquidation size, close factor is 1.
smallLiquidation := baseCase()
smallLiquidation.smallLiquidationSize = sdk.MustNewDecFromStr("60")
runTestCase(smallLiquidation, "1", "small liquidation")

// A liquidation-ineligible target would not have its close factor calculated in
// practice, but the function should return zero if it were.
notEligible := baseCase()
notEligible.borrowedValue = sdk.MustNewDecFromStr("30")
runTestCase(notEligible, "0", "liquidation ineligible")

// A liquidation-ineligible target which is below the small liquidation size
// should still return a close factor of zero.
smallNotEligible := baseCase()
smallNotEligible.borrowedValue = sdk.MustNewDecFromStr("10")
runTestCase(smallNotEligible, "0", "liquidation ineligible (small)")

// A borrower which is exactly on their liquidation threshold will have a close factor
// equal to minimumCloseFactor.
exactThreshold := baseCase()
exactThreshold.borrowedValue = sdk.MustNewDecFromStr("40")
runTestCase(exactThreshold, "0.1", "exact threshold")

// If collateral weights are all 1 (CV = LT), close factor will be MinimumCloseFactor.
// This situation will not occur in practice as collateral weights are less than one.
highCollateralWeight := baseCase()
highCollateralWeight.collateralValue = sdk.MustNewDecFromStr("40")
runTestCase(highCollateralWeight, "0.1", "high collateral weights")
}
13 changes: 6 additions & 7 deletions x/leverage/types/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,16 @@ func (t Token) Validate() error {
return ErrUToken.Wrap(t.SymbolDenom)
}

// Reserve factor and collateral weight range between 0 and 1, inclusive.
if t.ReserveFactor.IsNegative() || t.ReserveFactor.GT(sdk.OneDec()) {
// Reserve factor is non-negative and less than 1.
if t.ReserveFactor.IsNegative() || t.ReserveFactor.GTE(sdk.OneDec()) {
return fmt.Errorf("invalid reserve factor: %s", t.ReserveFactor)
}

if t.CollateralWeight.IsNegative() || t.CollateralWeight.GT(sdk.OneDec()) {
// Collateral weight is non-negative and less than 1.
if t.CollateralWeight.IsNegative() || t.CollateralWeight.GTE(sdk.OneDec()) {
return fmt.Errorf("invalid collateral rate: %s", t.CollateralWeight)
}

// Liquidation threshold ranges between collateral weight and 1, inclusive.
if t.LiquidationThreshold.LT(t.CollateralWeight) || t.LiquidationThreshold.GT(sdk.OneDec()) {
// Liquidation threshold is at least collateral weight, but less than 1.
if t.LiquidationThreshold.LT(t.CollateralWeight) || t.LiquidationThreshold.GTE(sdk.OneDec()) {
return fmt.Errorf("invalid liquidation threshold: %s", t.LiquidationThreshold)
}

Expand Down