From 545578bd948982ef45888308de0b87c4126d354e Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Sun, 25 Jun 2023 22:26:10 -0600 Subject: [PATCH 01/22] cl++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0abe865018..a7f7ebb674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +- [2XXX](https://github.com/umee-network/umee/pull/2XXX) Add borrow factor to `x/leverage` - [2192](https://github.com/umee-network/umee/pull/2102) Add `MsgFastLiquidate` 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. From 15668dfcfaad2ad90d5544f4f154dcd884bf9273 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Sun, 25 Jun 2023 22:28:13 -0600 Subject: [PATCH 02/22] cl++ --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f7ebb674..edd2858ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features -- [2XXX](https://github.com/umee-network/umee/pull/2XXX) Add borrow factor to `x/leverage` +- [2114](https://github.com/umee-network/umee/pull/2114) Add borrow factor to `x/leverage` - [2192](https://github.com/umee-network/umee/pull/2102) Add `MsgFastLiquidate` 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. From af9c1fb1ea5a9e6a84ff4a72f31f0a579ea16454 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 00:06:06 -0600 Subject: [PATCH 03/22] rephrase test - sign changes based on cpu architecture so strict equal avoided --- x/leverage/keeper/inspector_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/inspector_test.go b/x/leverage/keeper/inspector_test.go index e9bf57df06..e13d2c5a05 100644 --- a/x/leverage/keeper/inspector_test.go +++ b/x/leverage/keeper/inspector_test.go @@ -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( + 123456789123456789123456789.123456789, + neat(sdk.MustNewDecFromStr("123456789123456789123456789.123456789")), + ) } From 382897312bed35dedcdce7f7b5ad5b8705dfd1ab Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:09:21 -0600 Subject: [PATCH 04/22] implement borrow factor restriction --- x/leverage/keeper/borrows.go | 27 ++++++++++++++++++++++----- x/leverage/keeper/oracle.go | 23 +++++++++++++++++++++++ x/leverage/types/token.go | 8 ++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 14723bd40a..e983ca0fb5 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -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. @@ -18,18 +19,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) if err != nil { return err } - if value.GT(limit) { + if borrowValue.GT(borrowLimit) { return types.ErrUndercollaterized.Wrapf( - "borrowed: %s, limit: %s", value, limit) + "borrowed: %s, limit: %s", borrowValue, borrowLimit) } + + // check health using borrow factor + weightedBorrowValue, err := k.WeightedBorrowValue(ctx, borrowed, types.PriceModeHigh) + if err != nil { + return err + } + collateralValue, err := k.VisibleTokenValue(ctx, collateral, types.PriceModeLow) + if err != nil { + return err + } + if weightedBorrowValue.GT(collateralValue) { + return types.ErrUndercollaterized.Wrapf( + "weighted borrow: %s, collateral value: %s", weightedBorrowValue, collateralValue) + } + return nil } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 3ce0aaf0a1..fbee72f39b 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -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) { + 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 { + 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) { diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index ae27e696d5..70c9859157 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -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")) { + return sdk.MustNewDecFromStr("2.0") + } + return sdk.OneDec().Quo(t.CollateralWeight) +} + func defaultUmeeToken() Token { return Token{ BaseDenom: appparams.BondDenom, From 2cefbdd40295e28943cba929f4af1f2ed12e1fc5 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:18:31 -0600 Subject: [PATCH 05/22] fix new uToken calc --- x/leverage/keeper/borrows.go | 2 +- x/leverage/keeper/oracle.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index e983ca0fb5..d83b708279 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -38,7 +38,7 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres if err != nil { return err } - collateralValue, err := k.VisibleTokenValue(ctx, collateral, types.PriceModeLow) + collateralValue, err := k.VisibleUTokenValue(ctx, collateral, types.PriceModeLow) if err != nil { return err } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index fbee72f39b..ec55bf7b4d 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -162,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) { + 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. From 4264e054d4002e8932114af493cb0ac226e5ddb9 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:48:49 -0600 Subject: [PATCH 06/22] Update x/leverage/keeper/oracle.go --- x/leverage/keeper/oracle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index ec55bf7b4d..9b7cec7add 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -120,7 +120,7 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.Pri } // 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 +// by borrow factor (which is the maximum 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) { From 9f024bfb046ab9a612d65b96f89e35b9ea3a5b79 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:49:11 -0600 Subject: [PATCH 07/22] Update x/leverage/types/token.go --- x/leverage/types/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index 70c9859157..901c57165a 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -167,7 +167,7 @@ func (t Token) AssertNotBlacklisted() error { return nil } -// BorrowFactor returns the minimum of 2.0 or 1 / collateralWeight. +// BorrowFactor returns the maximum of 2.0 or 1 / collateralWeight. func (t Token) BorrowFactor() sdk.Dec { if t.CollateralWeight.LTE(sdk.MustNewDecFromStr("0.5")) { return sdk.MustNewDecFromStr("2.0") From bde1355e58461875102ec2d8bafb08c4a29e0f12 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:49:38 -0600 Subject: [PATCH 08/22] revert --- x/leverage/types/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index 901c57165a..70c9859157 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -167,7 +167,7 @@ func (t Token) AssertNotBlacklisted() error { return nil } -// BorrowFactor returns the maximum of 2.0 or 1 / collateralWeight. +// BorrowFactor returns the minimum of 2.0 or 1 / collateralWeight. func (t Token) BorrowFactor() sdk.Dec { if t.CollateralWeight.LTE(sdk.MustNewDecFromStr("0.5")) { return sdk.MustNewDecFromStr("2.0") From 32affb9d26877a334f222d3a808435d23e96a5f7 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 07:49:58 -0600 Subject: [PATCH 09/22] revert --- x/leverage/keeper/oracle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 9b7cec7add..ec55bf7b4d 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -120,7 +120,7 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.Pri } // WeightedBorrowValue returns the total value of all input tokens, each multiplied -// by borrow factor (which is the maximum of 2.0 and 1/collateral weight). It +// 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) { From 0f37acf185daf5fc8dafdae0faa71780359b001b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:21:28 -0600 Subject: [PATCH 10/22] implement borrow factor in userMaxBorrow --- x/leverage/keeper/collateral.go | 16 ++++---- x/leverage/keeper/grpc_query.go | 2 +- x/leverage/keeper/inspector.go | 2 +- x/leverage/keeper/limits.go | 65 ++++++++++++++++++++++++++++----- x/leverage/keeper/liquidate.go | 2 +- 5 files changed, 66 insertions(+), 21 deletions(-) diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index 018bf7b52f..7d5d12f045 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -73,9 +73,9 @@ func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Coin { } // CalculateCollateralValue uses the price oracle to determine the value (in USD) provided by -// collateral sdk.Coins, using each token's uToken exchange rate. Always uses spot price. +// 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) { +func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins, mode types.PriceMode) (sdk.Dec, error) { total := sdk.ZeroDec() for _, coin := range collateral { @@ -86,7 +86,7 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins) } // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot) + v, err := k.TokenValue(ctx, baseAsset, mode) if err != nil { return sdk.ZeroDec(), err } @@ -99,10 +99,10 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins) } // VisibleCollateralValue uses the price oracle to determine the value (in USD) provided by -// collateral sdk.Coins, using each token's uToken exchange rate. Always uses spot price. +// collateral sdk.Coins, using each token's uToken exchange rate. // Unlike CalculateCollateralValue, this function will not return an error if value calculation // fails on a token - instead, that token will contribute zero value to the total. -func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) { +func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins, mode types.PriceMode) (sdk.Dec, error) { total := sdk.ZeroDec() for _, coin := range collateral { @@ -113,7 +113,7 @@ func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (s } // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot) + v, err := k.TokenValue(ctx, baseAsset, mode) if err == nil { // for coins that did not error, add their value to the total total = total.Add(v) @@ -169,13 +169,13 @@ func (k *Keeper) VisibleCollateralShare(ctx sdk.Context, denom string) (sdk.Dec, thisCollateral := sdk.NewCoins(sdk.NewCoin(denom, systemCollateral.AmountOf(denom))) // get USD collateral value for all uTokens combined, except those experiencing price outages - totalValue, err := k.VisibleCollateralValue(ctx, systemCollateral) + totalValue, err := k.VisibleCollateralValue(ctx, systemCollateral, types.PriceModeSpot) if err != nil { return sdk.ZeroDec(), err } // get USD collateral value for this uToken only - thisValue, err := k.CalculateCollateralValue(ctx, thisCollateral) + thisValue, err := k.CalculateCollateralValue(ctx, thisCollateral, types.PriceModeSpot) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index 3d63a4c639..16ad0d4621 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -228,7 +228,7 @@ func (q Querier) AccountSummary( } // collateral value always uses spot prices, and this line skips assets that are missing prices - collateralValue, err := q.Keeper.VisibleCollateralValue(ctx, collateral) + collateralValue, err := q.Keeper.VisibleCollateralValue(ctx, collateral, types.PriceModeSpot) if err != nil { return nil, err } diff --git a/x/leverage/keeper/inspector.go b/x/leverage/keeper/inspector.go index d3058e6792..5a5f113f5f 100644 --- a/x/leverage/keeper/inspector.go +++ b/x/leverage/keeper/inspector.go @@ -68,7 +68,7 @@ func (q Querier) Inspect( borrowed := k.GetBorrowerBorrows(ctx, addr) borrowedValue, _ := k.TotalTokenValue(ctx, borrowed, types.PriceModeSpot) collateral := k.GetBorrowerCollateral(ctx, addr) - collateralValue, _ := k.CalculateCollateralValue(ctx, collateral) + collateralValue, _ := k.CalculateCollateralValue(ctx, collateral, types.PriceModeSpot) liquidationThreshold, _ := k.CalculateLiquidationThreshold(ctx, collateral) account := types.InspectAccount{ diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 5b0e28185e..295d6fe883 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -7,7 +7,7 @@ import ( ) // userMaxWithdraw calculates the maximum amount of uTokens an account can currently withdraw and the amount of -// these uTokens is non-collateral. Input denom should be a base token. If oracle prices are missing for some of the +// these uTokens which are non-collateral. Input denom should be a base token. If oracle prices are missing for some of the // borrower's collateral (other than the denom being withdrawn), computes the maximum safe withdraw allowed by only // the collateral whose prices are known. func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, sdk.Coin, error) { @@ -37,6 +37,27 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil } + // calculate collateral value for the account, using the lower of spot or historic prices for each token + // will count collateral with missing prices as zero value without returning an error + collateralValue, err := k.VisibleCollateralValue(ctx, totalCollateral, types.PriceModeLow) + if err != nil { + // for errors besides a missing price, the whole transaction fails + return sdk.Coin{}, sdk.Coin{}, err + } + + // calculate weighted borrowed value - used by the borrow factor limit + weightedBorrowValue, err := k.WeightedBorrowValue(ctx, totalBorrowed, types.PriceModeHigh) + if nonOracleError(err) { + // for errors besides a missing price, the whole transaction fails + return sdk.Coin{}, sdk.Coin{}, err + } + if err != nil { + // for missing prices on borrowed assets, we can't withdraw any collateral + // but can withdraw non-collateral uTokens + withdrawAmount := sdk.MinInt(walletUtokens, availableUTokens.Amount) + return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil + } + // if no non-blacklisted tokens are borrowed, withdraw the maximum available amount if borrowedValue.IsZero() { withdrawAmount := walletUtokens.Add(unbondedCollateral.Amount) @@ -46,15 +67,24 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str // compute the borrower's borrow limit using all their collateral // except the denom being withdrawn (also excluding collateral missing oracle prices) - otherBorrowLimit, err := k.VisibleBorrowLimit(ctx, otherCollateral) + otherCollateralBorrowLimit, err := k.VisibleBorrowLimit(ctx, otherCollateral) if err != nil { return sdk.Coin{}, sdk.Coin{}, err } // if their other collateral fully covers all borrows, withdraw the maximum available amount - if borrowedValue.LT(otherBorrowLimit) { - withdrawAmount := walletUtokens.Add(unbondedCollateral.Amount) - withdrawAmount = sdk.MinInt(withdrawAmount, availableUTokens.Amount) - return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil + if borrowedValue.LT(otherCollateralBorrowLimit) { + // also check collateral value vs weighted borrow (borrow factor limit) + otherCollateralValue, err := k.VisibleCollateralValue(ctx, otherCollateral, types.PriceModeLow) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, err + } + // if weighted borrow does not exceed other collateral value, this collateral can be fully withdrawn + if otherCollateralValue.GTE(weightedBorrowValue) { + // in this case, both borrow limits will not be exceeded even if all collateral is withdrawn + withdrawAmount := walletUtokens.Add(unbondedCollateral.Amount) + withdrawAmount = sdk.MinInt(withdrawAmount, availableUTokens.Amount) + return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil + } } // for nonzero borrows, calculations are based on unused borrow limit @@ -64,8 +94,8 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str if err != nil { return sdk.Coin{}, sdk.Coin{}, err } - // borrowers above their borrow limit cannot withdraw collateral, but can withdraw wallet uTokens - if borrowLimit.LTE(borrowedValue) { + // borrowers above either of their borrow limits cannot withdraw collateral, but can withdraw wallet uTokens + if borrowLimit.LTE(borrowedValue) || collateralValue.LTE(weightedBorrowValue) { withdrawAmount := sdk.MinInt(walletUtokens, availableUTokens.Amount) return sdk.NewCoin(uDenom, withdrawAmount), sdk.NewCoin(uDenom, walletUtokens), nil } @@ -81,8 +111,19 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str return sdk.Coin{}, sdk.Coin{}, err } - // if only a portion of collateral is unused, withdraw only that portion + // if only a portion of collateral is unused, withdraw only that portion (regular borrow limit) unusedCollateralFraction := unusedBorrowLimit.Quo(specificBorrowLimit) + + // calculate value of this collateral specifically, which is used in borrow factor's borrow limit + specificCollateralValue, err := k.CalculateCollateralValue(ctx, sdk.NewCoins(thisCollateral), types.PriceModeLow) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, err + } + unusedCollateralValue := collateralValue.Sub(weightedBorrowValue) + // Find the more restrictive of either borrow factor limit or borrow limit + unusedCollateralFraction = sdk.MinDec(unusedCollateralFraction, unusedCollateralValue.Quo(specificCollateralValue)) + + // Both borrow limits are satisfied by this withdrawl amount. The restrictions below relate to neither. unusedCollateral := unusedCollateralFraction.MulInt(thisCollateral.Amount).TruncateInt() // find the minimum of unused collateral (due to borrows) or unbonded collateral (incentive module) @@ -177,7 +218,11 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. thisDenomCollateral := sdk.NewCoin(denom, systemCollateral.AmountOf(denom)) // get USD collateral value for all other denoms, skipping those which are missing oracle prices - otherDenomsValue, err := k.VisibleCollateralValue(ctx, systemCollateral.Sub(thisDenomCollateral)) + otherDenomsValue, err := k.VisibleCollateralValue( + ctx, + systemCollateral.Sub(thisDenomCollateral), + types.PriceModeSpot, + ) if err != nil { return sdk.ZeroInt(), err } diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 54100681e7..c346fec0e5 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -33,7 +33,7 @@ func (k Keeper) getLiquidationAmounts( if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - collateralValue, err := k.CalculateCollateralValue(ctx, borrowerCollateral) + collateralValue, err := k.CalculateCollateralValue(ctx, borrowerCollateral, types.PriceModeSpot) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } From b1874cec704e73176b70b516c4976da0a060ffc1 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:37:11 -0600 Subject: [PATCH 11/22] implement userMaxBorrow + cause failing wasm test --- x/leverage/keeper/limits.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 295d6fe883..cb4282f028 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -142,11 +142,16 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str // userMaxBorrow calculates the maximum amount of a given token an account can currently borrow. // input denom should be a base token. If oracle prices are missing for some of the borrower's -// collateral, computes the maximum safe borrow allowed by only the collateral whose prices are known +// collateral, computes the maximum safe borrow allowed by only the collateral whose prices are known. func (k *Keeper) userMaxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { if types.HasUTokenPrefix(denom) { return sdk.Coin{}, types.ErrUToken } + token, err := k.GetTokenSettings(ctx, denom) + if err != nil { + return sdk.Coin{}, err + } + availableTokens := k.AvailableLiquidity(ctx, denom) totalBorrowed := k.GetBorrowerBorrows(ctx, addr) @@ -163,6 +168,17 @@ func (k *Keeper) userMaxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom strin return sdk.NewCoin(denom, sdk.ZeroInt()), nil } + // calculate weighted borrowed value for the account, using the higher of spot or historic prices + weightedBorrowedValue, err := k.WeightedBorrowValue(ctx, totalBorrowed, types.PriceModeHigh) + if nonOracleError(err) { + // non-oracle errors fail the transaction (or query) + return sdk.Coin{}, err + } + if err != nil { + // oracle errors cause max borrow to be zero + return sdk.NewCoin(denom, sdk.ZeroInt()), nil + } + // calculate borrow limit for the account, using only collateral whose price is known borrowLimit, err := k.VisibleBorrowLimit(ctx, totalCollateral) if err != nil { @@ -173,11 +189,27 @@ func (k *Keeper) userMaxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom strin return sdk.NewCoin(denom, sdk.ZeroInt()), nil } + // calculate collateral value limit for the account, using only collateral whose price is known + collateralValue, err := k.VisibleCollateralValue(ctx, totalCollateral, types.PriceModeLow) + if err != nil { + return sdk.Coin{}, err + } + // borrowers above their borrow factor borrow limit cannot borrow + if collateralValue.LTE(weightedBorrowedValue) { + return sdk.NewCoin(denom, sdk.ZeroInt()), nil + } + // determine the USD amount of borrow limit that is currently unused unusedBorrowLimit := borrowLimit.Sub(borrowedValue) + // determine the USD amount that can be borrowed according to borrow factor limit + maxBorrowValueIncrease := collateralValue.Sub(weightedBorrowedValue).Quo(token.BorrowFactor()) + + // finds the most restrictive of regular borrow limit and borrow factor limit + valueToBorrow := sdk.MinDec(unusedBorrowLimit, maxBorrowValueIncrease) + // determine max borrow, using the higher of spot or historic prices for the token to borrow - maxBorrow, err := k.TokenWithValue(ctx, denom, unusedBorrowLimit, types.PriceModeHigh) + maxBorrow, err := k.TokenWithValue(ctx, denom, valueToBorrow, types.PriceModeHigh) if nonOracleError(err) { // non-oracle errors fail the transaction (or query) return sdk.Coin{}, err From d4b279bb7dfb4ca7a0f8f30fd8fc6d1815620b8c Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:49:10 -0600 Subject: [PATCH 12/22] lint --- x/leverage/keeper/limits.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index cb4282f028..ac9188f612 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -7,9 +7,9 @@ import ( ) // userMaxWithdraw calculates the maximum amount of uTokens an account can currently withdraw and the amount of -// these uTokens which are non-collateral. Input denom should be a base token. If oracle prices are missing for some of the -// borrower's collateral (other than the denom being withdrawn), computes the maximum safe withdraw allowed by only -// the collateral whose prices are known. +// these uTokens which are non-collateral. Input denom should be a base token. If oracle prices are missing for +// some of the borrower's collateral (other than the denom being withdrawn), computes the maximum safe withdraw +// allowed by only the collateral whose prices are known. func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, sdk.Coin, error) { uDenom := types.ToUTokenDenom(denom) availableTokens := sdk.NewCoin(denom, k.AvailableLiquidity(ctx, denom)) @@ -123,7 +123,7 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str // Find the more restrictive of either borrow factor limit or borrow limit unusedCollateralFraction = sdk.MinDec(unusedCollateralFraction, unusedCollateralValue.Quo(specificCollateralValue)) - // Both borrow limits are satisfied by this withdrawl amount. The restrictions below relate to neither. + // Both borrow limits are satisfied by this withdrawal amount. The restrictions below relate to neither. unusedCollateral := unusedCollateralFraction.MulInt(thisCollateral.Amount).TruncateInt() // find the minimum of unused collateral (due to borrows) or unbonded collateral (incentive module) From 2be1218f7faa83681664093f21dcb0d3fcf881ac Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:37:48 -0600 Subject: [PATCH 13/22] add denom to test suite registry --- x/leverage/keeper/grpc_query_test.go | 2 +- x/leverage/keeper/msg_server_test.go | 4 ++-- x/leverage/keeper/suite_test.go | 16 +++++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 28119e7db7..6c984c7cba 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -25,7 +25,7 @@ func (s *IntegrationTestSuite) TestQuerier_RegisteredTokens() { "valid: get the all registered tokens", "", types.QueryRegisteredTokens{}, - 5, + 6, }, { "valid: get the registered token info by base_denom", diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 628a4b343c..e341f85f12 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -98,7 +98,7 @@ func (s *IntegrationTestSuite) TestAddTokensToRegistry() { s.Require().NoError(err) // no tokens should have been deleted tokens := s.app.LeverageKeeper.GetAllRegisteredTokens(s.ctx) - s.Require().Len(tokens, 6) + s.Require().Len(tokens, 7) token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, ntA.BaseDenom) s.Require().NoError(err) @@ -170,7 +170,7 @@ func (s *IntegrationTestSuite) TestUpdateRegistry() { s.Require().NoError(err) // no tokens should have been deleted tokens := s.app.LeverageKeeper.GetAllRegisteredTokens(s.ctx) - s.Require().Len(tokens, 5) + s.Require().Len(tokens, 6) token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uumee") s.Require().NoError(err) diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index 48426aa23c..d35bcba1f8 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -22,11 +22,12 @@ import ( ) const ( - umeeDenom = appparams.BondDenom - atomDenom = fixtures.AtomDenom - daiDenom = fixtures.DaiDenom - pumpDenom = "upump" - dumpDenom = "udump" + umeeDenom = appparams.BondDenom + atomDenom = fixtures.AtomDenom + daiDenom = fixtures.DaiDenom + pumpDenom = "upump" + dumpDenom = "udump" + stableDenom = "stable" ) type IntegrationTestSuite struct { @@ -82,6 +83,11 @@ func (s *IntegrationTestSuite) SetupTest() { // additional tokens for historacle testing require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(dumpDenom, "DUMP", 6))) require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(pumpDenom, "PUMP", 6))) + // additional tokens for borrow factor testing + stable := newToken(stableDenom, "STABLE", 6) + stable.CollateralWeight = sdk.MustNewDecFromStr("0.8") + stable.LiquidationThreshold = sdk.MustNewDecFromStr("0.9") + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, stable)) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) From 62d329797283eefe76e3713e1d3ac166333b6f2b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:47:29 -0600 Subject: [PATCH 14/22] max withdaw test --- x/leverage/keeper/msg_server_test.go | 34 +++++++++++++++++++++++----- x/leverage/keeper/oracle_test.go | 22 ++++++++++-------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index e341f85f12..518484150e 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -555,6 +555,13 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { // borrowed value is $10 (current) or $5 (historic) // collateral weights are always 0.25 in testing + // create an UMEE borrower using STABLE collateral + stableUmeeBorrower := s.newAccount(coin.New(stableDenom, 100_000000)) + s.supply(stableUmeeBorrower, coin.New(stableDenom, 100_000000)) + s.collateralize(stableUmeeBorrower, coin.New("u/"+stableDenom, 100_000000)) + s.borrow(stableUmeeBorrower, coin.New(umeeDenom, 30_000000)) + // UMEE and STABLE have the same price but different collateral weights + zeroUmee := coin.Zero(umeeDenom) zeroUUmee := coin.New("u/"+umeeDenom, 0) tcs := []struct { @@ -574,7 +581,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { sdk.Coin{}, sdk.Coin{}, types.ErrNotRegisteredToken, - }, { + }, + { "can't borrow uToken", supplier, "u/" + umeeDenom, @@ -582,7 +590,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { sdk.Coin{}, sdk.Coin{}, types.ErrUToken, - }, { + }, + { "max withdraw umee", supplier, umeeDenom, @@ -590,7 +599,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { coin.New("u/"+umeeDenom, 75_000000), coin.New(umeeDenom, 100_000000), nil, - }, { + }, + { "duplicate max withdraw umee", supplier, umeeDenom, @@ -598,7 +608,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { zeroUUmee, zeroUmee, nil, - }, { + }, + { "max withdraw with borrow", other, umeeDenom, @@ -606,7 +617,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { coin.New("u/"+umeeDenom, 60_000000), coin.New(umeeDenom, 60_000000), nil, - }, { + }, + { "max withdrawal (dump borrower)", dumpborrower, pumpDenom, @@ -614,7 +626,8 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { coin.New("u/"+pumpDenom, 20_000000), coin.New(pumpDenom, 20_000000), nil, - }, { + }, + { "max withdrawal (pump borrower)", pumpborrower, dumpDenom, @@ -623,6 +636,15 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { coin.New(dumpDenom, 20_000000), nil, }, + { + "max withdrawal (borrow factor 2 with stablecoin collateral)", + stableUmeeBorrower, + stableDenom, + coin.New("u/"+stableDenom, 40_000000), + coin.New("u/"+stableDenom, 40_000000), + coin.New(stableDenom, 40_000000), + nil, + }, } for _, tc := range tcs { diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index d594617c25..14f0281552 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -62,18 +62,20 @@ func (m *mockOracleKeeper) Clear(denom string) { // Reset restores the mock oracle's prices to its default values. func (m *mockOracleKeeper) Reset() { m.symbolExchangeRates = map[string]sdk.Dec{ - "UMEE": sdk.MustNewDecFromStr("4.21"), - "ATOM": sdk.MustNewDecFromStr("39.38"), - "DAI": sdk.MustNewDecFromStr("1.00"), - "DUMP": sdk.MustNewDecFromStr("0.50"), // A token which has recently halved in price - "PUMP": sdk.MustNewDecFromStr("2.00"), // A token which has recently doubled in price + "UMEE": sdk.MustNewDecFromStr("4.21"), + "ATOM": sdk.MustNewDecFromStr("39.38"), + "DAI": sdk.MustNewDecFromStr("1.00"), + "DUMP": sdk.MustNewDecFromStr("0.50"), // A token which has recently halved in price + "PUMP": sdk.MustNewDecFromStr("2.00"), // A token which has recently doubled in price + "STABLE": sdk.MustNewDecFromStr("4.21"), // Same price as umee } m.historicExchangeRates = map[string]sdk.Dec{ - "UMEE": sdk.MustNewDecFromStr("4.21"), - "ATOM": sdk.MustNewDecFromStr("39.38"), - "DAI": sdk.MustNewDecFromStr("1.00"), - "DUMP": sdk.MustNewDecFromStr("1.00"), - "PUMP": sdk.MustNewDecFromStr("1.00"), + "UMEE": sdk.MustNewDecFromStr("4.21"), + "ATOM": sdk.MustNewDecFromStr("39.38"), + "DAI": sdk.MustNewDecFromStr("1.00"), + "DUMP": sdk.MustNewDecFromStr("1.00"), + "PUMP": sdk.MustNewDecFromStr("1.00"), + "STABLE": sdk.MustNewDecFromStr("4.21"), } } From 9d0421fd4b5ec94a8c1ab70f6152afb6b0ebfeb7 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:54:50 -0600 Subject: [PATCH 15/22] withdraw and max withdraw tests --- x/leverage/keeper/msg_server_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 518484150e..64a8463832 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -350,6 +350,13 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { // borrowed value is $10 (current) or $5 (historic) // collateral weights are always 0.25 in testing + // create an UMEE borrower using STABLE collateral + stableUmeeBorrower := s.newAccount(coin.New(stableDenom, 100_000000)) + s.supply(stableUmeeBorrower, coin.New(stableDenom, 100_000000)) + s.collateralize(stableUmeeBorrower, coin.New("u/"+stableDenom, 100_000000)) + s.borrow(stableUmeeBorrower, coin.New(umeeDenom, 30_000000)) + // UMEE and STABLE have the same price but different collateral weights + tcs := []struct { msg string addr sdk.AccAddress @@ -455,6 +462,14 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { nil, sdk.Coin{}, types.ErrUndercollaterized, + }, { + "borrow limit (undercollateralized due to borrow factor but not collateral weight)", + stableUmeeBorrower, + coin.New("u/"+stableDenom, 50_000000), + nil, + nil, + sdk.Coin{}, + types.ErrUndercollaterized, }, } From 4104e6f04eaa08c82e8d118b6df4a2b2318618d2 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 04:09:05 -0600 Subject: [PATCH 16/22] borrow and maxborrow tests --- x/leverage/keeper/msg_server_test.go | 32 ++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 64a8463832..859e73995a 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -1310,6 +1310,13 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { // collateral value is $50 (current) or $100 (historic) // collateral weights are always 0.25 in testing + // create an UMEE borrower using STABLE collateral + stableUmeeBorrower := s.newAccount(coin.New(stableDenom, 40_000000)) + s.supply(stableUmeeBorrower, coin.New(stableDenom, 40_000000)) + s.collateralize(stableUmeeBorrower, coin.New("u/"+stableDenom, 40_000000)) + s.borrow(stableUmeeBorrower, coin.New(umeeDenom, 3_000000)) + // UMEE and STABLE have the same price but different collateral weights + tcs := []testCase{ { "uToken", @@ -1332,14 +1339,19 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { coin.New(umeeDenom, 70_000000), nil, }, { - "additional borrow", - borrower, - coin.New(umeeDenom, 20_000000), + "stable umee borrower (acceptable)", + stableUmeeBorrower, + coin.New(umeeDenom, 17_000000), nil, + }, { + "stable umee borrower (borrow factor limit)", + stableUmeeBorrower, + coin.New(umeeDenom, 1_000000), + types.ErrUndercollaterized, }, { "max supply utilization", borrower, - coin.New(umeeDenom, 10_000000), + coin.New(umeeDenom, 9_000000), types.ErrMaxSupplyUtilization, }, { "atom borrow", @@ -1457,6 +1469,13 @@ func (s *IntegrationTestSuite) TestMsgMaxBorrow() { // collateral value is $50 (current) or $100 (historic) // collateral weights are always 0.25 in testing + // create an UMEE borrower using STABLE collateral + stableUmeeBorrower := s.newAccount(coin.New(stableDenom, 100_000000)) + s.supply(stableUmeeBorrower, coin.New(stableDenom, 100_000000)) + s.collateralize(stableUmeeBorrower, coin.New("u/"+stableDenom, 100_000000)) + s.borrow(stableUmeeBorrower, coin.New(umeeDenom, 30_000000)) + // UMEE and STABLE have the same price but different collateral weights + tcs := []struct { msg string addr sdk.AccAddress @@ -1498,6 +1517,11 @@ func (s *IntegrationTestSuite) TestMsgMaxBorrow() { pumpborrower, coin.New(pumpDenom, 6_250000), nil, + }, { + "stable umee borrower", + stableUmeeBorrower, + coin.New(umeeDenom, 20_000000), + nil, }, } From 9c6881b2d099f1563bcad36f1b52e0d2457bfc92 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:27:01 +0100 Subject: [PATCH 17/22] renaming suggestion --- x/leverage/keeper/borrows.go | 2 +- x/leverage/keeper/oracle.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 2081cd5889..de6307016e 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -41,7 +41,7 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres if err != nil { return err } - collateralValue, err := k.VisibleUTokenValue(ctx, collateral, types.PriceModeLow) + collateralValue, err := k.VisibleUTokensValue(ctx, collateral, types.PriceModeLow) if err != nil { return err } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index ec55bf7b4d..051ed76874 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -162,8 +162,8 @@ 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) { +// VisibleUTokensValue converts uTokens to tokens and calls VisibleTokenValue. Errors on non-uTokens. +func (k Keeper) VisibleUTokensValue(ctx sdk.Context, uTokens sdk.Coins, mode types.PriceMode) (sdk.Dec, error) { tokens := sdk.NewCoins() for _, u := range uTokens { From 43bb4208287fab966c3aae2ac0f52debe9cddda7 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:31:43 +0100 Subject: [PATCH 18/22] halfDec --- x/leverage/types/token.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index 70c9859157..8020ab7f61 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -15,6 +15,8 @@ const ( UTokenPrefix = "u/" ) +var halfDec = sdk.MustNewDecFromStr("0.5") + // HasUTokenPrefix detects the uToken prefix on a denom. func HasUTokenPrefix(denom string) bool { return strings.HasPrefix(denom, UTokenPrefix) @@ -169,7 +171,7 @@ func (t Token) AssertNotBlacklisted() error { // BorrowFactor returns the minimum of 2.0 or 1 / collateralWeight. func (t Token) BorrowFactor() sdk.Dec { - if t.CollateralWeight.LTE(sdk.MustNewDecFromStr("0.5")) { + if t.CollateralWeight.LTE(halfDec) { return sdk.MustNewDecFromStr("2.0") } return sdk.OneDec().Quo(t.CollateralWeight) From ed33f8bcf6b7a51ad332d55439e07a2ab0f9b8ff Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:40:02 +0100 Subject: [PATCH 19/22] rename --- x/leverage/keeper/borrows.go | 2 +- x/leverage/keeper/collateral.go | 2 +- x/leverage/keeper/limits.go | 4 ++-- x/leverage/keeper/oracle.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index de6307016e..dcf3419654 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -37,7 +37,7 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres } // check health using borrow factor - weightedBorrowValue, err := k.WeightedBorrowValue(ctx, borrowed, types.PriceModeHigh) + weightedBorrowValue, err := k.ValueWithBorrowFactor(ctx, borrowed, types.PriceModeHigh) if err != nil { return err } diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index 7d5d12f045..200fc78494 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -91,7 +91,7 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins, return sdk.ZeroDec(), err } - // add each collateral coin's weighted value to borrow limit + // add each collateral coin's value to borrow limit total = total.Add(v) } diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index ac9188f612..16cb9030ba 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -46,7 +46,7 @@ func (k *Keeper) userMaxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom str } // calculate weighted borrowed value - used by the borrow factor limit - weightedBorrowValue, err := k.WeightedBorrowValue(ctx, totalBorrowed, types.PriceModeHigh) + weightedBorrowValue, err := k.ValueWithBorrowFactor(ctx, totalBorrowed, types.PriceModeHigh) if nonOracleError(err) { // for errors besides a missing price, the whole transaction fails return sdk.Coin{}, sdk.Coin{}, err @@ -169,7 +169,7 @@ func (k *Keeper) userMaxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom strin } // calculate weighted borrowed value for the account, using the higher of spot or historic prices - weightedBorrowedValue, err := k.WeightedBorrowValue(ctx, totalBorrowed, types.PriceModeHigh) + weightedBorrowedValue, err := k.ValueWithBorrowFactor(ctx, totalBorrowed, types.PriceModeHigh) if nonOracleError(err) { // non-oracle errors fail the transaction (or query) return sdk.Coin{}, err diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 051ed76874..7b27072b2e 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -119,11 +119,11 @@ 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 +// ValueWithBorrowFactor 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) { +func (k Keeper) ValueWithBorrowFactor(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) (sdk.Dec, error) { total := sdk.ZeroDec() for _, c := range coins { From 043375f0cb1216b9e28df3b81d558b9a6d1074b9 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:47:47 +0100 Subject: [PATCH 20/22] README --- x/leverage/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/x/leverage/README.md b/x/leverage/README.md index 7b59cc6dc4..ba990fe2d9 100644 --- a/x/leverage/README.md +++ b/x/leverage/README.md @@ -20,6 +20,7 @@ The leverage module depends directly on `x/oracle` for asset prices, and interac - [uToken Exchange Rate](#utoken-exchange-rate) - [Supply Utilization](#supply-utilization) - [Borrow Limit](#borrow-limit) + - [Borrow Factor](#borrow-factor) - [Liquidation Threshold](#liquidation-threshold) - [Borrow APY](#borrow-apy) - [Supplying APY](#supplying-apy) @@ -168,6 +169,20 @@ A user's borrow limit is the sum of the contributions from each denomination of For tokens with hith historic prices enabled (indicated by a `HistoricMedians` parameter greater than zero), each collateral `TokenValue` is computed with `PriceModeLow`, i.e. the lower of either spot price or historic price is used. +#### Borrow Factor + +Each token in the `Token Registry` has a parameter called `CollateralWeight`, always less than 1, which determines the portion of the token's value that goes towards a user's borrow limit, when the token is used as collateral. + +An implied parameter `BorrowFactor` is derived from `CollateralWeight` - specifically, it is the minimum of `2.0` and `1/CollateralWeight`. + +When a user is borrowing, their borrow limit is whichever is more restrictive of these two rules: + +- Borrowed value must be less than collateral value times the weighted average of collateral assets' `CollateralWeight` +- Borrowed value times the weighted average of borrowed assets' `BorrowFactor` must be less than collateral value. + +This means that when the original borrow limit based on collateral weight would allow a higher quality collateral to borrow a risky asset with a small margin of safety, the user's effective collateral weight is reduced to that of the riskier asset. +(Or `0.5` at the minimum.) + #### Historic Borrow Limit, Value The leverage module also makes use of the oracle's historic prices to enforce an additional restriction on borrowing. From f9be585a92dd2f46bdcb76f4117fa61e8b150d2e Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Jun 2023 02:07:45 +0100 Subject: [PATCH 21/22] Update x/leverage/README.md Co-authored-by: Robert Zaremba --- x/leverage/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/README.md b/x/leverage/README.md index ba990fe2d9..f391b92afb 100644 --- a/x/leverage/README.md +++ b/x/leverage/README.md @@ -175,7 +175,7 @@ Each token in the `Token Registry` has a parameter called `CollateralWeight`, al An implied parameter `BorrowFactor` is derived from `CollateralWeight` - specifically, it is the minimum of `2.0` and `1/CollateralWeight`. -When a user is borrowing, their borrow limit is whichever is more restrictive of these two rules: +When a user is borrowing, their borrow limit is whichever is more restrictive of the following two rules: - Borrowed value must be less than collateral value times the weighted average of collateral assets' `CollateralWeight` - Borrowed value times the weighted average of borrowed assets' `BorrowFactor` must be less than collateral value. From 51ddd39ee94e7fe7d02a0d7777c19b49bf4198e0 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Jun 2023 02:15:13 +0100 Subject: [PATCH 22/22] readme suggestions --- x/leverage/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x/leverage/README.md b/x/leverage/README.md index f391b92afb..dfc97a437f 100644 --- a/x/leverage/README.md +++ b/x/leverage/README.md @@ -174,11 +174,12 @@ For tokens with hith historic prices enabled (indicated by a `HistoricMedians` p Each token in the `Token Registry` has a parameter called `CollateralWeight`, always less than 1, which determines the portion of the token's value that goes towards a user's borrow limit, when the token is used as collateral. An implied parameter `BorrowFactor` is derived from `CollateralWeight` - specifically, it is the minimum of `2.0` and `1/CollateralWeight`. +The maximum borrow factor of `2.0` allows risky or non-collateral assets (`0 <= CollateralWeight < 0.5`) to be borrowed to a certain minimum degree. When a user is borrowing, their borrow limit is whichever is more restrictive of the following two rules: -- Borrowed value must be less than collateral value times the weighted average of collateral assets' `CollateralWeight` -- Borrowed value times the weighted average of borrowed assets' `BorrowFactor` must be less than collateral value. +- Borrowed value must be less than collateral value times `CollateralWeight` (sum over each collateral asset) +- Borrowed value times `BorrowFactor` (sum over each borrowed asset) must be less than collateral value. This means that when the original borrow limit based on collateral weight would allow a higher quality collateral to borrow a risky asset with a small margin of safety, the user's effective collateral weight is reduced to that of the riskier asset. (Or `0.5` at the minimum.)