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: add borrow factor #2114

Merged
merged 26 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

- [2114](https://github.com/umee-network/umee/pull/2114) Add borrow factor to `x/leverage`
- [2102](https://github.com/umee-network/umee/pull/2102) and [2106](https://github.com/umee-network/umee/pull/2106) Add `MsgLeveragedLiquidate` to `x/leverage`
- [2085](https://github.com/umee-network/umee/pull/2085) Add `inspect` query to leverage module, which msut be enabled on a node by running with `-l` liquidator query flag.
- [1952](https://github.com/umee-network/umee/pull/1952) Add `x/incentive` module.
Expand Down
27 changes: 22 additions & 5 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
)

// assertBorrowerHealth returns an error if a borrower is currently above their borrow limit,
// under either recent (historic median) or current prices. It returns an error if
// under either recent (historic median) or current prices. Checks using borrow limit based
// on collateral weight, then check separately for borrow limit using borrow factor. Error if
// borrowed asset prices cannot be calculated, but will try to treat collateral whose prices are
// unavailable as having zero value. This can still result in a borrow limit being too low,
// unless the remaining collateral is enough to cover all borrows.
Expand All @@ -21,18 +22,34 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)
collateral := k.GetBorrowerCollateral(ctx, borrowerAddr)

value, err := k.TotalTokenValue(ctx, borrowed, types.PriceModeHigh)
// check health using collateral weight
borrowValue, err := k.TotalTokenValue(ctx, borrowed, types.PriceModeHigh)
if err != nil {
return err
}
limit, err := k.VisibleBorrowLimit(ctx, collateral)
borrowLimit, err := k.VisibleBorrowLimit(ctx, collateral)
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
if value.GT(limit.Mul(maxUsage)) {
if borrowValue.GT(borrowLimit.Mul(maxUsage)) {
return types.ErrUndercollaterized.Wrapf(
"borrowed: %s, limit: %s, max usage %s", value, limit, maxUsage)
"borrowed: %s, limit: %s, max usage %s", borrowValue, borrowLimit, maxUsage)
}

// check health using borrow factor
weightedBorrowValue, err := k.WeightedBorrowValue(ctx, borrowed, types.PriceModeHigh)
toteki marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
collateralValue, err := k.VisibleUTokenValue(ctx, collateral, types.PriceModeLow)
if err != nil {
return err
}
if weightedBorrowValue.GT(collateralValue.Mul(maxUsage)) {
return types.ErrUndercollaterized.Wrapf(
"weighted borrow: %s, collateral value: %s, max usage %s", weightedBorrowValue, collateralValue, maxUsage)
}

return nil
}

Expand Down
12 changes: 8 additions & 4 deletions x/leverage/keeper/inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ func TestNeat(t *testing.T) {
"-12.555": -12.55, // truncates default to cent
"-0.00123456789": -0.001234, // truncates <0.01 to millionth
"-0.000000987654321": -0.000000987654321, // <0.000001 gets maximum precision
// edge case: >2^64 displays incorrectly
// this should be fine, since this is a display-only function (not used in transactions)
// which is used on dollar (not token) amounts
"123456789123456789123456789.123456789": -9.223372036854776e+21,
}

for s, f := range cases {
assert.Equal(f, neat(sdk.MustNewDecFromStr(s)))
}

// edge case: >2^64 displays incorrectly
// this should be fine, since this is a display-only function (not used in transactions)
// which is used on dollar (not token) amounts
assert.NotEqual(
toteki marked this conversation as resolved.
Show resolved Hide resolved
123456789123456789123456789.123456789,
neat(sdk.MustNewDecFromStr("123456789123456789123456789.123456789")),
)
}
38 changes: 38 additions & 0 deletions x/leverage/keeper/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.Pri
return total, nil
}

// WeightedBorrowValue returns the total value of all input tokens, each multiplied
// by borrow factor (which is the minimum of 2.0 and 1/collateral weight). It
// ignores unregistered and blacklisted tokens instead of returning an error, but
// will error on unavailable prices.
func (k Keeper) WeightedBorrowValue(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) (sdk.Dec, error) {
toteki marked this conversation as resolved.
Show resolved Hide resolved
total := sdk.ZeroDec()

for _, c := range coins {
token, err := k.GetTokenSettings(ctx, c.Denom)
if err != nil {
continue
}
v, err := k.TokenValue(ctx, c, mode)
if err != nil {
toteki marked this conversation as resolved.
Show resolved Hide resolved
return sdk.ZeroDec(), err
}

total = total.Add(v.Mul(token.BorrowFactor()))
}

return total, nil
}

// VisibleTokenValue functions like TotalTokenValue, but interprets missing oracle prices
// as zero value instead of returning an error.
func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) (sdk.Dec, error) {
Expand All @@ -139,6 +162,21 @@ func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.P
return total, nil
}

// VisibleUTokenValue converts uTokens to tokens and calls VisibleTokenValue. Errors on non-uTokens.
func (k Keeper) VisibleUTokenValue(ctx sdk.Context, uTokens sdk.Coins, mode types.PriceMode) (sdk.Dec, error) {
toteki marked this conversation as resolved.
Show resolved Hide resolved
tokens := sdk.NewCoins()

for _, u := range uTokens {
t, err := k.ExchangeUToken(ctx, u)
if err != nil {
return sdk.ZeroDec(), err
}
tokens = tokens.Add(t)
}

return k.VisibleTokenValue(ctx, tokens, mode)
}

// TokenWithValue creates a token of a given denom with an given USD value.
// Returns an error on invalid price or denom. Rounds down, i.e. the
// value of the token returned may be slightly less than the requested value.
Expand Down
8 changes: 8 additions & 0 deletions x/leverage/types/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ func (t Token) AssertNotBlacklisted() error {
return nil
}

// BorrowFactor returns the minimum of 2.0 or 1 / collateralWeight.
func (t Token) BorrowFactor() sdk.Dec {
if t.CollateralWeight.LTE(sdk.MustNewDecFromStr("0.5")) {
toteki marked this conversation as resolved.
Show resolved Hide resolved
return sdk.MustNewDecFromStr("2.0")
}
return sdk.OneDec().Quo(t.CollateralWeight)
toteki marked this conversation as resolved.
Show resolved Hide resolved
}

func defaultUmeeToken() Token {
return Token{
BaseDenom: appparams.BondDenom,
Expand Down
Loading