From 5dc0efe05d8f6910edebc4e571d9e3cd4ad7f530 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Sun, 11 Dec 2022 23:04:34 -0700 Subject: [PATCH 01/36] add pump&dump tokens to mock oracle --- x/leverage/keeper/grpc_query_test.go | 2 +- x/leverage/keeper/msg_server_test.go | 4 ++-- x/leverage/keeper/oracle_test.go | 24 ++++++++++++++++++++++++ x/leverage/keeper/suite_test.go | 3 +++ x/leverage/types/expected_types.go | 1 + 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 51831fbe1e..a9a8e08f0e 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -14,7 +14,7 @@ func (s *IntegrationTestSuite) TestQuerier_RegisteredTokens() { resp, err := s.queryClient.RegisteredTokens(ctx.Context(), &types.QueryRegisteredTokens{}) require.NoError(err) - require.Len(resp.Registry, 3, "token registry length") + require.Len(resp.Registry, 5, "token registry length") } func (s *IntegrationTestSuite) TestQuerier_Params() { diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 629ca924b4..0964dedf5c 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -83,7 +83,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, 4) + s.Require().Len(tokens, 6) token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uabcd") s.Require().NoError(err) @@ -155,7 +155,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, 3) + s.Require().Len(tokens, 5) token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uumee") s.Require().NoError(err) diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index ea273daea8..f1b4229e21 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -12,18 +12,31 @@ import ( type mockOracleKeeper struct { baseExchangeRates map[string]sdk.Dec symbolExchangeRates map[string]sdk.Dec + medianExchangeRates map[string]sdk.Dec } func newMockOracleKeeper() *mockOracleKeeper { m := &mockOracleKeeper{ baseExchangeRates: make(map[string]sdk.Dec), symbolExchangeRates: make(map[string]sdk.Dec), + medianExchangeRates: make(map[string]sdk.Dec), } m.Reset() return m } +// TODO: Does this function take base denom or symbol denom? +func (m *mockOracleKeeper) MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64, +) (sdk.Dec, error) { + p, ok := m.medianExchangeRates[denom] + if !ok { + return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) + } + + return p, nil +} + func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec, error) { p, ok := m.symbolExchangeRates[denom] if !ok { @@ -47,11 +60,22 @@ func (m *mockOracleKeeper) Reset() { "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 } m.baseExchangeRates = map[string]sdk.Dec{ appparams.BondDenom: sdk.MustNewDecFromStr("0.00000421"), atomDenom: sdk.MustNewDecFromStr("0.00003938"), daiDenom: sdk.MustNewDecFromStr("0.000000000000000001"), + "udump": sdk.MustNewDecFromStr("0.0000005"), + "upump": sdk.MustNewDecFromStr("0.0000020"), + } + m.medianExchangeRates = 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"), } } diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index fe5f58dfc4..57bbcbbcb1 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -75,6 +75,9 @@ func (s *IntegrationTestSuite) SetupTest() { require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(appparams.BondDenom, "UMEE", 6))) require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(atomDenom, "ATOM", 6))) require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(daiDenom, "DAI", 18))) + // additional tokens for historacle testing + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken("udump", "DUMP", 6))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken("upump", "PUMP", 6))) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) diff --git a/x/leverage/types/expected_types.go b/x/leverage/types/expected_types.go index b925cca10a..d614a9b83f 100644 --- a/x/leverage/types/expected_types.go +++ b/x/leverage/types/expected_types.go @@ -33,4 +33,5 @@ type BankKeeper interface { type OracleKeeper interface { GetExchangeRate(ctx sdk.Context, denom string) (sdk.Dec, error) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error) + MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64) (sdk.Dec, error) } From 84065b5a36d3da08fa8e117ce8f6faa0f1a0d0f2 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Sun, 11 Dec 2022 23:17:20 -0700 Subject: [PATCH 02/36] value functions --- x/leverage/keeper/oracle.go | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index e5ad56906e..601d021b54 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -11,6 +11,9 @@ import ( var ten = sdk.MustNewDecFromStr("10") +// TODO: Parameterize and move this +const numHistoracleStamps = uint64(10) + // TokenBasePrice returns the USD value of a base token. Note, the token's denomination // must be the base denomination, e.g. uumee. The x/oracle module must know of // the base and display/symbol denominations for each exchange pair. E.g. it must @@ -63,6 +66,32 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.D return price, t.Exponent, nil } +// TokenHistoraclePrice returns the USD value of a token's symbol denom, e.g. UMEE, considered +// cautiously over a recent time period using medians. Input denom is base denomination, e.g. uumee. +// When error is nil, price is guaranteed to be positive. Also returns the token's exponent to +// reduce redundant registry reads. +func (k Keeper) TokenHistoraclePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { + t, err := k.GetTokenSettings(ctx, baseDenom) + if err != nil { + return sdk.ZeroDec(), 0, err + } + + if t.Blacklist { + return sdk.ZeroDec(), t.Exponent, types.ErrBlacklisted + } + + median, err := k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) + if err != nil { + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(err, "oracle") + } + + if median.IsNil() || !median.IsPositive() { + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) + } + + return median, t.Exponent, nil +} + // exponent multiplies an sdk.Dec by 10^n. n can be negative. func exponent(input sdk.Dec, n int32) sdk.Dec { if n == 0 { @@ -87,6 +116,18 @@ func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { return exponent(p.Mul(toDec(coin.Amount)), int32(exp)*-1), nil } +// TokenRecentValue returns the total token value given a Coin using a median price. +// Error if we cannot get the token's price or if it's not an accepted token. +// Computation uses price of token's default denom to avoid rounding errors +// for exponent >= 18 tokens. +func (k Keeper) TokenRecentValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { + p, exp, err := k.TokenHistoraclePrice(ctx, coin.Denom) + if err != nil { + return sdk.ZeroDec(), err + } + return exponent(p.Mul(toDec(coin.Amount)), int32(exp)*-1), nil +} + // TotalTokenValue returns the total value of all supplied tokens. It is // equivalent to the sum of TokenValue on each coin individually, except it // ignores unregistered and blacklisted tokens instead of returning an error. @@ -107,6 +148,26 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, erro return total, nil } +// TotalTokenRecentValue returns the total value of all supplied tokens using median +// prices. It is equivalent to the sum of TokenRecentValue on each coin individually, +// except it ignores unregistered and blacklisted tokens instead of returning an error. +func (k Keeper) TotalTokenRecentValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { + total := sdk.ZeroDec() + + accepted := k.filterAcceptedCoins(ctx, coins) + + for _, c := range accepted { + v, err := k.TokenRecentValue(ctx, c) + if err != nil { + return sdk.ZeroDec(), err + } + + total = total.Add(v) + } + + return total, nil +} + // PriceRatio computed the ratio of the USD prices of two base tokens, as sdk.Dec(fromPrice/toPrice). // Will return an error if either token price is not positive, and guarantees a positive output. // Computation uses price of token's default denom to avoid rounding errors for exponent >= 18 tokens, From 109a2ba6066b04778b6139b40461a78f8ae3788c Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 07:46:01 -0700 Subject: [PATCH 03/36] comments, renames --- x/leverage/keeper/oracle.go | 39 ++++++++++---- x/leverage/keeper/oracle_test.go | 89 ++++++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 18 deletions(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 601d021b54..119396affc 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -66,11 +66,11 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.D return price, t.Exponent, nil } -// TokenHistoraclePrice returns the USD value of a token's symbol denom, e.g. UMEE, considered +// HistoricTokenPrice returns the USD value of a token's symbol denom, e.g. UMEE, considered // cautiously over a recent time period using medians. Input denom is base denomination, e.g. uumee. // When error is nil, price is guaranteed to be positive. Also returns the token's exponent to // reduce redundant registry reads. -func (k Keeper) TokenHistoraclePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { +func (k Keeper) HistoricTokenPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { t, err := k.GetTokenSettings(ctx, baseDenom) if err != nil { return sdk.ZeroDec(), 0, err @@ -116,12 +116,12 @@ func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { return exponent(p.Mul(toDec(coin.Amount)), int32(exp)*-1), nil } -// TokenRecentValue returns the total token value given a Coin using a median price. +// HistoricTokenValue returns the total token value given a Coin using a median price. // Error if we cannot get the token's price or if it's not an accepted token. // Computation uses price of token's default denom to avoid rounding errors // for exponent >= 18 tokens. -func (k Keeper) TokenRecentValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { - p, exp, err := k.TokenHistoraclePrice(ctx, coin.Denom) +func (k Keeper) HistoricTokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { + p, exp, err := k.HistoricTokenPrice(ctx, coin.Denom) if err != nil { return sdk.ZeroDec(), err } @@ -148,16 +148,16 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, erro return total, nil } -// TotalTokenRecentValue returns the total value of all supplied tokens using median +// HistoricTotalTokenValue returns the total value of all supplied tokens using median // prices. It is equivalent to the sum of TokenRecentValue on each coin individually, // except it ignores unregistered and blacklisted tokens instead of returning an error. -func (k Keeper) TotalTokenRecentValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { +func (k Keeper) HistoricTotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { total := sdk.ZeroDec() accepted := k.filterAcceptedCoins(ctx, coins) for _, c := range accepted { - v, err := k.TokenRecentValue(ctx, c) + v, err := k.HistoricTokenValue(ctx, c) if err != nil { return sdk.ZeroDec(), err } @@ -168,7 +168,7 @@ func (k Keeper) TotalTokenRecentValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec return total, nil } -// PriceRatio computed the ratio of the USD prices of two base tokens, as sdk.Dec(fromPrice/toPrice). +// PriceRatio computes the ratio of the USD prices of two base tokens, as sdk.Dec(fromPrice/toPrice). // Will return an error if either token price is not positive, and guarantees a positive output. // Computation uses price of token's default denom to avoid rounding errors for exponent >= 18 tokens, // but returns in terms of base tokens. @@ -189,6 +189,27 @@ func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, return exponent(p1, powerDifference).Quo(p2), nil } +// HistoricPriceRatio computes the ratio of the recent USD prices of two base tokens, as sdk.Dec(fromPrice/toPrice). +// Will return an error if either token price is not positive, and guarantees a positive output. +// Computation uses price of token's default denom to avoid rounding errors for exponent >= 18 tokens, +// but returns in terms of base tokens. +func (k Keeper) HistoricPriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, error) { + p1, e1, err := k.HistoricTokenPrice(ctx, fromDenom) + if err != nil { + return sdk.ZeroDec(), err + } + p2, e2, err := k.HistoricTokenPrice(ctx, toDenom) + if err != nil { + return sdk.ZeroDec(), err + } + // If tokens have different exponents, the symbol price ratio must be adjusted + // to obtain the base token price ratio. If fromDenom has a higher exponent, then + // the ratio p1/p2 must be adjusted lower. + powerDifference := int32(e2) - int32(e1) + // Price ratio > 1 if fromDenom is worth more than toDenom. + return exponent(p1, powerDifference).Quo(p2), nil +} + // FundOracle transfers requested coins to the oracle module account, as // long as the leverage module account has sufficient unreserved assets. func (k Keeper) FundOracle(ctx sdk.Context, requested sdk.Coins) error { diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index f1b4229e21..7f2b0d3761 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -10,26 +10,25 @@ import ( ) type mockOracleKeeper struct { - baseExchangeRates map[string]sdk.Dec - symbolExchangeRates map[string]sdk.Dec - medianExchangeRates map[string]sdk.Dec + baseExchangeRates map[string]sdk.Dec + symbolExchangeRates map[string]sdk.Dec + historicExchangeRates map[string]sdk.Dec } func newMockOracleKeeper() *mockOracleKeeper { m := &mockOracleKeeper{ - baseExchangeRates: make(map[string]sdk.Dec), - symbolExchangeRates: make(map[string]sdk.Dec), - medianExchangeRates: make(map[string]sdk.Dec), + baseExchangeRates: make(map[string]sdk.Dec), + symbolExchangeRates: make(map[string]sdk.Dec), + historicExchangeRates: make(map[string]sdk.Dec), } m.Reset() return m } -// TODO: Does this function take base denom or symbol denom? func (m *mockOracleKeeper) MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64, ) (sdk.Dec, error) { - p, ok := m.medianExchangeRates[denom] + p, ok := m.historicExchangeRates[denom] if !ok { return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) } @@ -70,7 +69,7 @@ func (m *mockOracleKeeper) Reset() { "udump": sdk.MustNewDecFromStr("0.0000005"), "upump": sdk.MustNewDecFromStr("0.0000020"), } - m.medianExchangeRates = map[string]sdk.Dec{ + m.historicExchangeRates = map[string]sdk.Dec{ "UMEE": sdk.MustNewDecFromStr("4.21"), "ATOM": sdk.MustNewDecFromStr("39.38"), "DAI": sdk.MustNewDecFromStr("1.00"), @@ -112,6 +111,45 @@ func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) + + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump") + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("2.0"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump") + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("0.5"), p) + require.Equal(uint32(6), e) +} + +func (s *IntegrationTestSuite) TestOracle_HistoricTokenPrice() { + app, ctx, require := s.app, s.ctx, s.Require() + + p, e, err := app.LeverageKeeper.HistoricTokenPrice(ctx, appparams.BondDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("4.21"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, atomDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("39.38"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "foo") + require.ErrorIs(err, types.ErrNotRegisteredToken) + require.Equal(sdk.ZeroDec(), p) + require.Equal(uint32(0), e) + + p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "upump") + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("1.00"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "udump") + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("1.00"), p) + require.Equal(uint32(6), e) } func (s *IntegrationTestSuite) TestOracle_TokenValue() { @@ -125,6 +163,39 @@ func (s *IntegrationTestSuite) TestOracle_TokenValue() { v, err = app.LeverageKeeper.TokenValue(ctx, coin("foo", 2_400000)) require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), v) + + // 2.4 DUMP * $0.5 + v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("1.2"), v) + + // 2.4 PUMP * $2.00 + v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("4.8"), v) +} + +func (s *IntegrationTestSuite) TestOracle_HistoricTokenValue() { + app, ctx, require := s.app, s.ctx, s.Require() + + // 2.4 UMEE * $4.21 + v, err := app.LeverageKeeper.HistoricTokenValue(ctx, coin(appparams.BondDenom, 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("10.104"), v) + + v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("foo", 2_400000)) + require.ErrorIs(err, types.ErrNotRegisteredToken) + require.Equal(sdk.ZeroDec(), v) + + // 2.4 DUMP * $1.00 + v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("udump", 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("2.4"), v) + + // 2.4 PUMP * $1.00 + v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("upump", 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("2.4"), v) } func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { From d09eea6d932cd6b3c19c4a4c81c5bdd2444b8ee5 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 08:53:21 -0700 Subject: [PATCH 04/36] function signature --- x/leverage/keeper/borrows.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 5778516d0c..e5718aaf24 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -7,6 +7,20 @@ import ( "github.com/umee-network/umee/v3/x/leverage/types" ) +// AssertBorrowerHealth returns an error if a borrower is currently above their borrow limit, +// under either recent (historic median) or current prices. It also returns an error if +// relevant prices cannot be calculated. +// This should be checked at the end of any transaction which is restricted by borrow limits, +// i.e. Borrow, Decollateralize, Withdraw. +func (k Keeper) AssertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { + // borrowed := k.GetBorrowerBorrows(ctx,borrowerAddr) + // collateral := k.GetBorrowerCollateral(ctx,borrowerAddr) + + // Check using current prices + // Check using historic prices + return nil +} + // GetBorrow returns an sdk.Coin representing how much of a given denom a // borrower currently owes. func (k Keeper) GetBorrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, denom string) sdk.Coin { From 45eb3722ea3e521e52cd264bfd3d61a74e041e98 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:11:04 -0700 Subject: [PATCH 05/36] no one outpizzas the hut --- x/leverage/keeper/borrows.go | 4 +- x/leverage/keeper/collateral.go | 2 +- x/leverage/keeper/grpc_query.go | 6 +- x/leverage/keeper/iter.go | 2 +- x/leverage/keeper/keeper.go | 6 +- x/leverage/keeper/limits.go | 2 +- x/leverage/keeper/liquidate.go | 6 +- x/leverage/keeper/oracle.go | 106 +++++----------------------- x/leverage/keeper/oracle_test.go | 81 +++++++++++++-------- x/leverage/simulation/operations.go | 2 +- 10 files changed, 85 insertions(+), 132 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index e5718aaf24..cc4633cf3f 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -104,7 +104,7 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk // ignore blacklisted tokens if !ts.Blacklist { // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset) + v, err := k.TokenValue(ctx, baseAsset, false) if err != nil { return sdk.ZeroDec(), err } @@ -139,7 +139,7 @@ func (k Keeper) CalculateLiquidationThreshold(ctx sdk.Context, collateral sdk.Co // ignore blacklisted tokens if !ts.Blacklist { // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset) + v, err := k.TokenValue(ctx, baseAsset, false) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index 589ea3a207..e5d2cbc644 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -66,7 +66,7 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins) } // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset) + v, err := k.TokenValue(ctx, baseAsset, false) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index f067e48b00..7139150cab 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -135,7 +135,7 @@ func (q Querier) MarketSummary( } // Oracle price in response will be nil if it is unavailable - if oraclePrice, _, oracleErr := q.Keeper.TokenDefaultDenomPrice(ctx, req.Denom); oracleErr == nil { + if oraclePrice, _, oracleErr := q.Keeper.TokenDefaultDenomPrice(ctx, req.Denom, false); oracleErr == nil { resp.OraclePrice = &oraclePrice } @@ -199,11 +199,11 @@ func (q Querier) AccountSummary( collateral := q.Keeper.GetBorrowerCollateral(ctx, addr) borrowed := q.Keeper.GetBorrowerBorrows(ctx, addr) - suppliedValue, err := q.Keeper.TotalTokenValue(ctx, supplied) + suppliedValue, err := q.Keeper.TotalTokenValue(ctx, supplied, false) if err != nil { return nil, err } - borrowedValue, err := q.Keeper.TotalTokenValue(ctx, borrowed) + borrowedValue, err := q.Keeper.TotalTokenValue(ctx, borrowed, false) if err != nil { return nil, err } diff --git a/x/leverage/keeper/iter.go b/x/leverage/keeper/iter.go index 408428abfd..0837cd94d7 100644 --- a/x/leverage/keeper/iter.go +++ b/x/leverage/keeper/iter.go @@ -182,7 +182,7 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress collateral := k.GetBorrowerCollateral(ctx, addr) // use oracle helper functions to find total borrowed value in USD - borrowValue, err := k.TotalTokenValue(ctx, borrowed) + borrowValue, err := k.TotalTokenValue(ctx, borrowed, false) if err != nil { return err } diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 65bc965902..3143d60527 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -153,7 +153,7 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd if amountFromCollateral.IsPositive() { // Calculate current borrowed value borrowed := k.GetBorrowerBorrows(ctx, supplierAddr) - borrowedValue, err := k.TotalTokenValue(ctx, borrowed) + borrowedValue, err := k.TotalTokenValue(ctx, borrowed, false) if err != nil { return sdk.Coin{}, err } @@ -240,7 +240,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. } // Calculate borrowed value will be AFTER this borrow - newBorrowedValue, err := k.TotalTokenValue(ctx, borrowed.Add(borrow)) + newBorrowedValue, err := k.TotalTokenValue(ctx, borrowed.Add(borrow), false) if err != nil { return err } @@ -345,7 +345,7 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uT // Determine currently borrowed value borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) - borrowedValue, err := k.TotalTokenValue(ctx, borrowed) + borrowedValue, err := k.TotalTokenValue(ctx, borrowed, false) if err != nil { return err } diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index db28756321..3281f889a9 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -22,7 +22,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) specificCollateral := sdk.NewCoin(uDenom, totalCollateral.AmountOf(uDenom)) // calculate borrowed value for the account - borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed) + borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, false) if err != nil { return sdk.Coin{}, err } diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index b4071c7e43..d02061c2f8 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -28,7 +28,7 @@ func (k Keeper) getLiquidationAmounts( repayDenomBorrowed := sdk.NewCoin(repayDenom, totalBorrowed.AmountOf(repayDenom)) // calculate borrower health in USD values - borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed) + borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, false) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } @@ -44,7 +44,7 @@ func (k Keeper) getLiquidationAmounts( // borrower is healthy and cannot be liquidated return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationIneligible } - repayDenomBorrowedValue, err := k.TokenValue(ctx, repayDenomBorrowed) + repayDenomBorrowedValue, err := k.TokenValue(ctx, repayDenomBorrowed, false) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } @@ -75,7 +75,7 @@ func (k Keeper) getLiquidationAmounts( } // get precise (less rounding at high exponent) price ratio - priceRatio, err := k.PriceRatio(ctx, repayDenom, rewardDenom) + priceRatio, err := k.PriceRatio(ctx, repayDenom, rewardDenom, false) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 119396affc..c791dc6519 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -44,7 +44,8 @@ func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, erro // TokenDefaultDenomPrice returns the USD value of a token's symbol denom, e.g. UMEE. Note, the input // denom must still be the base denomination, e.g. uumee. When error is nil, price is guaranteed // to be positive. Also returns the token's exponent to reduce redundant registry reads. -func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { +// If the historic parameter is true, uses a median of recent prices instead of current price. +func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string, historic bool) (sdk.Dec, uint32, error) { t, err := k.GetTokenSettings(ctx, baseDenom) if err != nil { return sdk.ZeroDec(), 0, err @@ -54,7 +55,12 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.D return sdk.ZeroDec(), t.Exponent, types.ErrBlacklisted } - price, err := k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + var price sdk.Dec + if historic { + price, err = k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) + } else { + price, err = k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + } if err != nil { return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(err, "oracle") } @@ -66,32 +72,6 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.D return price, t.Exponent, nil } -// HistoricTokenPrice returns the USD value of a token's symbol denom, e.g. UMEE, considered -// cautiously over a recent time period using medians. Input denom is base denomination, e.g. uumee. -// When error is nil, price is guaranteed to be positive. Also returns the token's exponent to -// reduce redundant registry reads. -func (k Keeper) HistoricTokenPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { - t, err := k.GetTokenSettings(ctx, baseDenom) - if err != nil { - return sdk.ZeroDec(), 0, err - } - - if t.Blacklist { - return sdk.ZeroDec(), t.Exponent, types.ErrBlacklisted - } - - median, err := k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) - if err != nil { - return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(err, "oracle") - } - - if median.IsNil() || !median.IsPositive() { - return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) - } - - return median, t.Exponent, nil -} - // exponent multiplies an sdk.Dec by 10^n. n can be negative. func exponent(input sdk.Dec, n int32) sdk.Dec { if n == 0 { @@ -108,20 +88,9 @@ func exponent(input sdk.Dec, n int32) sdk.Dec { // returned if we cannot get the token's price or if it's not an accepted token. // Computation uses price of token's default denom to avoid rounding errors // for exponent >= 18 tokens. -func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { - p, exp, err := k.TokenDefaultDenomPrice(ctx, coin.Denom) - if err != nil { - return sdk.ZeroDec(), err - } - return exponent(p.Mul(toDec(coin.Amount)), int32(exp)*-1), nil -} - -// HistoricTokenValue returns the total token value given a Coin using a median price. -// Error if we cannot get the token's price or if it's not an accepted token. -// Computation uses price of token's default denom to avoid rounding errors -// for exponent >= 18 tokens. -func (k Keeper) HistoricTokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { - p, exp, err := k.HistoricTokenPrice(ctx, coin.Denom) +// If the historic parameter is true, uses medians of recent prices instead of current prices. +func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin, historic bool) (sdk.Dec, error) { + p, exp, err := k.TokenDefaultDenomPrice(ctx, coin.Denom, historic) if err != nil { return sdk.ZeroDec(), err } @@ -131,33 +100,14 @@ func (k Keeper) HistoricTokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, err // TotalTokenValue returns the total value of all supplied tokens. It is // equivalent to the sum of TokenValue on each coin individually, except it // ignores unregistered and blacklisted tokens instead of returning an error. -func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { +// If the historic parameter is true, uses medians of recent prices instead of current prices. +func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, historic bool) (sdk.Dec, error) { total := sdk.ZeroDec() accepted := k.filterAcceptedCoins(ctx, coins) for _, c := range accepted { - v, err := k.TokenValue(ctx, c) - if err != nil { - return sdk.ZeroDec(), err - } - - total = total.Add(v) - } - - return total, nil -} - -// HistoricTotalTokenValue returns the total value of all supplied tokens using median -// prices. It is equivalent to the sum of TokenRecentValue on each coin individually, -// except it ignores unregistered and blacklisted tokens instead of returning an error. -func (k Keeper) HistoricTotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { - total := sdk.ZeroDec() - - accepted := k.filterAcceptedCoins(ctx, coins) - - for _, c := range accepted { - v, err := k.HistoricTokenValue(ctx, c) + v, err := k.TokenValue(ctx, c, historic) if err != nil { return sdk.ZeroDec(), err } @@ -172,33 +122,13 @@ func (k Keeper) HistoricTotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.D // Will return an error if either token price is not positive, and guarantees a positive output. // Computation uses price of token's default denom to avoid rounding errors for exponent >= 18 tokens, // but returns in terms of base tokens. -func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, error) { - p1, e1, err := k.TokenDefaultDenomPrice(ctx, fromDenom) - if err != nil { - return sdk.ZeroDec(), err - } - p2, e2, err := k.TokenDefaultDenomPrice(ctx, toDenom) - if err != nil { - return sdk.ZeroDec(), err - } - // If tokens have different exponents, the symbol price ratio must be adjusted - // to obtain the base token price ratio. If fromDenom has a higher exponent, then - // the ratio p1/p2 must be adjusted lower. - powerDifference := int32(e2) - int32(e1) - // Price ratio > 1 if fromDenom is worth more than toDenom. - return exponent(p1, powerDifference).Quo(p2), nil -} - -// HistoricPriceRatio computes the ratio of the recent USD prices of two base tokens, as sdk.Dec(fromPrice/toPrice). -// Will return an error if either token price is not positive, and guarantees a positive output. -// Computation uses price of token's default denom to avoid rounding errors for exponent >= 18 tokens, -// but returns in terms of base tokens. -func (k Keeper) HistoricPriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, error) { - p1, e1, err := k.HistoricTokenPrice(ctx, fromDenom) +// If the historic parameter is true, uses medians of recent prices instead of current prices. +func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string, historic bool) (sdk.Dec, error) { + p1, e1, err := k.TokenDefaultDenomPrice(ctx, fromDenom, historic) if err != nil { return sdk.ZeroDec(), err } - p2, e2, err := k.HistoricTokenPrice(ctx, toDenom) + p2, e2, err := k.TokenDefaultDenomPrice(ctx, toDenom, historic) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 7f2b0d3761..05c4f163da 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -97,56 +97,54 @@ func (s *IntegrationTestSuite) TestOracle_TokenBasePrice() { func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { app, ctx, require := s.app, s.ctx, s.Require() - p, e, err := app.LeverageKeeper.TokenDefaultDenomPrice(ctx, appparams.BondDenom) + p, e, err := app.LeverageKeeper.TokenDefaultDenomPrice(ctx, appparams.BondDenom, false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("4.21"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, atomDenom) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, atomDenom, false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("39.38"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "foo") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "foo", false) require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump", false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.0"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump", false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("0.5"), p) require.Equal(uint32(6), e) -} -func (s *IntegrationTestSuite) TestOracle_HistoricTokenPrice() { - app, ctx, require := s.app, s.ctx, s.Require() + // Now with historic = true - p, e, err := app.LeverageKeeper.HistoricTokenPrice(ctx, appparams.BondDenom) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, appparams.BondDenom, true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("4.21"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, atomDenom) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, atomDenom, true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("39.38"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "foo") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "foo", true) require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) - p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "upump") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump", true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.00"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.HistoricTokenPrice(ctx, "udump") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump", true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.00"), p) require.Equal(uint32(6), e) @@ -156,44 +154,42 @@ func (s *IntegrationTestSuite) TestOracle_TokenValue() { app, ctx, require := s.app, s.ctx, s.Require() // 2.4 UMEE * $4.21 - v, err := app.LeverageKeeper.TokenValue(ctx, coin(appparams.BondDenom, 2_400000)) + v, err := app.LeverageKeeper.TokenValue(ctx, coin(appparams.BondDenom, 2_400000), false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("10.104"), v) - v, err = app.LeverageKeeper.TokenValue(ctx, coin("foo", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("foo", 2_400000), false) require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), v) // 2.4 DUMP * $0.5 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000), false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.2"), v) // 2.4 PUMP * $2.00 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000), false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("4.8"), v) -} -func (s *IntegrationTestSuite) TestOracle_HistoricTokenValue() { - app, ctx, require := s.app, s.ctx, s.Require() + // Now with historic = true // 2.4 UMEE * $4.21 - v, err := app.LeverageKeeper.HistoricTokenValue(ctx, coin(appparams.BondDenom, 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin(appparams.BondDenom, 2_400000), true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("10.104"), v) - v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("foo", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("foo", 2_400000), true) require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), v) // 2.4 DUMP * $1.00 - v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("udump", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000), true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.4"), v) // 2.4 PUMP * $1.00 - v, err = app.LeverageKeeper.HistoricTokenValue(ctx, coin("upump", 2_400000)) + v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000), true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.4"), v) } @@ -208,6 +204,7 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { coin(appparams.BondDenom, 2_400000), coin(atomDenom, 4_700000), ), + false, ) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("195.19"), v) @@ -220,32 +217,58 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { coin(atomDenom, 4_700000), coin("foo", 4_700000), ), + false, ) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("195.19"), v) + + // complex historic case + v, err = app.LeverageKeeper.TotalTokenValue( + ctx, + sdk.NewCoins( + coin(appparams.BondDenom, 2_400000), + coin(atomDenom, 4_700000), + coin("foo", 4_700000), + coin("udump", 2_000000), + ), + true, + ) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("197.19"), v) } func (s *IntegrationTestSuite) TestOracle_PriceRatio() { app, ctx, require := s.app, s.ctx, s.Require() - r, err := app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, atomDenom) + r, err := app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, atomDenom, false) require.NoError(err) // $4.21 / $39.38 at same exponent require.Equal(sdk.MustNewDecFromStr("0.106907059421025901"), r) - r, err = app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, daiDenom) + r, err = app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, daiDenom, false) require.NoError(err) // $4.21 / $1.00 at a difference of 12 exponent require.Equal(sdk.MustNewDecFromStr("4210000000000"), r) - r, err = app.LeverageKeeper.PriceRatio(ctx, daiDenom, appparams.BondDenom) + r, err = app.LeverageKeeper.PriceRatio(ctx, daiDenom, appparams.BondDenom, false) require.NoError(err) // $1.00 / $4.21 at a difference of -12 exponent require.Equal(sdk.MustNewDecFromStr("0.000000000000237530"), r) - _, err = app.LeverageKeeper.PriceRatio(ctx, "foo", atomDenom) + _, err = app.LeverageKeeper.PriceRatio(ctx, "foo", atomDenom, false) require.ErrorIs(err, types.ErrNotRegisteredToken) - _, err = app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, "foo") + _, err = app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, "foo", false) require.ErrorIs(err, types.ErrNotRegisteredToken) + + // current price of volatile assets + r, err = app.LeverageKeeper.PriceRatio(ctx, "upump", "udump", false) + require.NoError(err) + // $2.00 / $0.50 + require.Equal(sdk.MustNewDecFromStr("4"), r) + // historic price of volatile assets + r, err = app.LeverageKeeper.PriceRatio(ctx, "upump", "udump", true) + require.NoError(err) + // $1.00 / $1.00 + require.Equal(sdk.MustNewDecFromStr("1"), r) } diff --git a/x/leverage/simulation/operations.go b/x/leverage/simulation/operations.go index 885d6dc13f..43d2a039f0 100644 --- a/x/leverage/simulation/operations.go +++ b/x/leverage/simulation/operations.go @@ -409,7 +409,7 @@ func randomLiquidateFields( if err != nil { return liquidator, borrower, sdk.Coin{}, "", true } - borrowedValue, err := lk.TotalTokenValue(ctx, borrowed) + borrowedValue, err := lk.TotalTokenValue(ctx, borrowed, false) if err != nil { return liquidator, borrower, sdk.Coin{}, "", true } From db15b24f8317fe70984cd056933f8be022d203de Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:13:19 -0700 Subject: [PATCH 06/36] var --- x/leverage/keeper/oracle_test.go | 26 +++++++++++++------------- x/leverage/keeper/suite_test.go | 6 ++++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 05c4f163da..d5ab91cdfa 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -66,8 +66,8 @@ func (m *mockOracleKeeper) Reset() { appparams.BondDenom: sdk.MustNewDecFromStr("0.00000421"), atomDenom: sdk.MustNewDecFromStr("0.00003938"), daiDenom: sdk.MustNewDecFromStr("0.000000000000000001"), - "udump": sdk.MustNewDecFromStr("0.0000005"), - "upump": sdk.MustNewDecFromStr("0.0000020"), + dumpDenom: sdk.MustNewDecFromStr("0.0000005"), + pumpDenom: sdk.MustNewDecFromStr("0.0000020"), } m.historicExchangeRates = map[string]sdk.Dec{ "UMEE": sdk.MustNewDecFromStr("4.21"), @@ -112,12 +112,12 @@ func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump", false) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, pumpDenom, false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.0"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump", false) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, dumpDenom, false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("0.5"), p) require.Equal(uint32(6), e) @@ -139,12 +139,12 @@ func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "upump", true) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, pumpDenom, true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.00"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "udump", true) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, dumpDenom, true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.00"), p) require.Equal(uint32(6), e) @@ -163,12 +163,12 @@ func (s *IntegrationTestSuite) TestOracle_TokenValue() { require.Equal(sdk.ZeroDec(), v) // 2.4 DUMP * $0.5 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000), false) + v, err = app.LeverageKeeper.TokenValue(ctx, coin(dumpDenom, 2_400000), false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("1.2"), v) // 2.4 PUMP * $2.00 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000), false) + v, err = app.LeverageKeeper.TokenValue(ctx, coin(pumpDenom, 2_400000), false) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("4.8"), v) @@ -184,12 +184,12 @@ func (s *IntegrationTestSuite) TestOracle_TokenValue() { require.Equal(sdk.ZeroDec(), v) // 2.4 DUMP * $1.00 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("udump", 2_400000), true) + v, err = app.LeverageKeeper.TokenValue(ctx, coin(dumpDenom, 2_400000), true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.4"), v) // 2.4 PUMP * $1.00 - v, err = app.LeverageKeeper.TokenValue(ctx, coin("upump", 2_400000), true) + v, err = app.LeverageKeeper.TokenValue(ctx, coin(pumpDenom, 2_400000), true) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("2.4"), v) } @@ -229,7 +229,7 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { coin(appparams.BondDenom, 2_400000), coin(atomDenom, 4_700000), coin("foo", 4_700000), - coin("udump", 2_000000), + coin(dumpDenom, 2_000000), ), true, ) @@ -262,12 +262,12 @@ func (s *IntegrationTestSuite) TestOracle_PriceRatio() { require.ErrorIs(err, types.ErrNotRegisteredToken) // current price of volatile assets - r, err = app.LeverageKeeper.PriceRatio(ctx, "upump", "udump", false) + r, err = app.LeverageKeeper.PriceRatio(ctx, pumpDenom, dumpDenom, false) require.NoError(err) // $2.00 / $0.50 require.Equal(sdk.MustNewDecFromStr("4"), r) // historic price of volatile assets - r, err = app.LeverageKeeper.PriceRatio(ctx, "upump", "udump", true) + r, err = app.LeverageKeeper.PriceRatio(ctx, pumpDenom, dumpDenom, true) require.NoError(err) // $1.00 / $1.00 require.Equal(sdk.MustNewDecFromStr("1"), r) diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index 57bbcbbcb1..231aa9cecc 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -27,6 +27,8 @@ const ( umeeDenom = appparams.BondDenom atomDenom = fixtures.AtomDenom daiDenom = fixtures.DaiDenom + pumpDenom = "upump" + dumpDenom = "udump" ) type IntegrationTestSuite struct { @@ -76,8 +78,8 @@ func (s *IntegrationTestSuite) SetupTest() { require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(atomDenom, "ATOM", 6))) require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(daiDenom, "DAI", 18))) // additional tokens for historacle testing - require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken("udump", "DUMP", 6))) - require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken("upump", "PUMP", 6))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(dumpDenom, "DUMP", 6))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(pumpDenom, "PUMP", 6))) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) From 5ebcae6b7afbf8196c159053d2384d589fb08538 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 11:39:54 -0700 Subject: [PATCH 07/36] implement AssertBorrowerHealth --- x/leverage/keeper/borrows.go | 34 +++++++++++++++++++++++++++---- x/leverage/keeper/borrows_test.go | 10 ++++----- x/leverage/keeper/grpc_query.go | 2 +- x/leverage/keeper/keeper.go | 6 +++--- x/leverage/keeper/limits.go | 4 ++-- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index cc4633cf3f..c97587f576 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -13,11 +13,36 @@ import ( // This should be checked at the end of any transaction which is restricted by borrow limits, // i.e. Borrow, Decollateralize, Withdraw. func (k Keeper) AssertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { - // borrowed := k.GetBorrowerBorrows(ctx,borrowerAddr) - // collateral := k.GetBorrowerCollateral(ctx,borrowerAddr) + borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) + collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) // Check using current prices + currentValue, err := k.TotalTokenValue(ctx, borrowed, false) + if err != nil { + return err + } + currentLimit, err := k.CalculateBorrowLimit(ctx, collateral, false) + if err != nil { + return err + } + if currentValue.GT(currentLimit) { + return types.ErrUndercollaterized.Wrapf( + "borrowed: %s, limit: %s (current prices)", currentValue, currentLimit) + } + // Check using historic prices + historicValue, err := k.TotalTokenValue(ctx, borrowed, true) + if err != nil { + return err + } + historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) + if err != nil { + return err + } + if historicValue.GT(historicLimit) { + return types.ErrUndercollaterized.Wrapf( + "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) + } return nil } @@ -86,7 +111,8 @@ func (k Keeper) SupplyUtilization(ctx sdk.Context, denom string) sdk.Dec { // CalculateBorrowLimit uses the price oracle to determine the borrow limit (in USD) provided by // collateral sdk.Coins, using each token's uToken exchange rate and collateral weight. // An error is returned if any input coins are not uTokens or if value calculation fails. -func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) { +// If the historic parameter is true, uses medians of recent prices instead of current prices. +func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins, historic bool) (sdk.Dec, error) { limit := sdk.ZeroDec() for _, coin := range collateral { @@ -104,7 +130,7 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk // ignore blacklisted tokens if !ts.Blacklist { // get USD value of base assets - v, err := k.TokenValue(ctx, baseAsset, false) + v, err := k.TokenValue(ctx, baseAsset, historic) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/borrows_test.go b/x/leverage/keeper/borrows_test.go index 8936fe4da1..8049c55f7a 100644 --- a/x/leverage/keeper/borrows_test.go +++ b/x/leverage/keeper/borrows_test.go @@ -202,13 +202,13 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { app, ctx, require := s.app, s.ctx, s.Require() // Empty coins - borrowLimit, err := app.LeverageKeeper.CalculateBorrowLimit(ctx, sdk.NewCoins()) + borrowLimit, err := app.LeverageKeeper.CalculateBorrowLimit(ctx, sdk.NewCoins(), false) require.NoError(err) require.Equal(sdk.ZeroDec(), borrowLimit) // Unregistered asset invalidCoins := sdk.NewCoins(coin("abcd", 1000)) - _, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, invalidCoins) + _, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, invalidCoins, false) require.ErrorIs(err, types.ErrNotUToken) // Create collateral uTokens (1k u/umee) @@ -222,7 +222,7 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { Mul(sdk.MustNewDecFromStr("0.25")) // Check borrow limit vs. manually computed value - borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, umeeCollateral) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, umeeCollateral, false) require.NoError(err) require.Equal(expectedUmeeLimit, borrowLimit) @@ -237,7 +237,7 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { Mul(sdk.MustNewDecFromStr("0.25")) // Check borrow limit vs. manually computed value - borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, atomCollateral) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, atomCollateral, false) require.NoError(err) require.Equal(expectedAtomLimit, borrowLimit) @@ -246,7 +246,7 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { combinedCollateral := umeeCollateral.Add(atomCollateral...) // Check borrow limit vs. manually computed value - borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, combinedCollateral) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, combinedCollateral, false) require.NoError(err) require.Equal(expectedCombinedLimit, borrowLimit) } diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index 7139150cab..a7b1b74176 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -211,7 +211,7 @@ func (q Querier) AccountSummary( if err != nil { return nil, err } - borrowLimit, err := q.Keeper.CalculateBorrowLimit(ctx, collateral) + borrowLimit, err := q.Keeper.CalculateBorrowLimit(ctx, collateral, false) if err != nil { return nil, err } diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 3143d60527..14d003b25d 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -169,7 +169,7 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd // Calculate what borrow limit will be AFTER this withdrawal collateralToWithdraw := sdk.NewCoin(uToken.Denom, amountFromCollateral) - newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(collateralToWithdraw)) + newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(collateralToWithdraw), false) if err != nil { return sdk.Coin{}, err } @@ -234,7 +234,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. // Calculate current borrow limit collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) - borrowLimit, err := k.CalculateBorrowLimit(ctx, collateral) + borrowLimit, err := k.CalculateBorrowLimit(ctx, collateral, false) if err != nil { return err } @@ -338,7 +338,7 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uT } // Determine what borrow limit would be AFTER disabling this denom as collateral - newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(uToken)) + newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(uToken), false) if err != nil { return err } diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 3281f889a9..f3a073dedb 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -35,7 +35,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) } // for nonzero borrows, calculations are based on unused borrow limit - borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral) + borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral, false) if err != nil { return sdk.Coin{}, err } @@ -49,7 +49,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) unusedBorrowLimit := borrowLimit.Sub(borrowedValue) // calculate the contribution to borrow limit made by only the type of collateral being withdrawn - specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(specificCollateral)) + specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(specificCollateral), false) if err != nil { return sdk.Coin{}, err } From c4a61db5f1585bf50dbd4e229bcd6862cd2aaadb Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 12:00:41 -0700 Subject: [PATCH 08/36] implement restrictions - broken tests --- x/leverage/keeper/borrows.go | 51 ++++++++++++------ x/leverage/keeper/keeper.go | 94 +++++---------------------------- x/leverage/keeper/msg_server.go | 44 +++++++++++++++ 3 files changed, 93 insertions(+), 96 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index c97587f576..7914bfebbb 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -7,12 +7,12 @@ import ( "github.com/umee-network/umee/v3/x/leverage/types" ) -// AssertBorrowerHealth returns an error if a borrower is currently above their borrow limit, +// checkBorrowerHealth returns an error if a borrower is currently above their borrow limit, // under either recent (historic median) or current prices. It also returns an error if // relevant prices cannot be calculated. // This should be checked at the end of any transaction which is restricted by borrow limits, // i.e. Borrow, Decollateralize, Withdraw. -func (k Keeper) AssertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { +func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) @@ -30,19 +30,25 @@ func (k Keeper) AssertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres "borrowed: %s, limit: %s (current prices)", currentValue, currentLimit) } - // Check using historic prices - historicValue, err := k.TotalTokenValue(ctx, borrowed, true) - if err != nil { - return err - } - historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) - if err != nil { - return err - } - if historicValue.GT(historicLimit) { - return types.ErrUndercollaterized.Wrapf( - "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) - } + /* + + // TODO: Comment this back in once all tests have a mock oracle which supports historic prices + + // Check using historic prices + historicValue, err := k.TotalTokenValue(ctx, borrowed, true) + if err != nil { + return err + } + historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) + if err != nil { + return err + } + if historicValue.GT(historicLimit) { + return types.ErrUndercollaterized.Wrapf( + "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) + } + + */ return nil } @@ -177,3 +183,18 @@ func (k Keeper) CalculateLiquidationThreshold(ctx sdk.Context, collateral sdk.Co return totalThreshold, nil } + +// checkSupplyUtilization returns the appropriate error if a token denom's +// supply utilization has exceeded MaxSupplyUtilization +func (k Keeper) checkSupplyUtilization(ctx sdk.Context, denom string) error { + token, err := k.GetTokenSettings(ctx, denom) + if err != nil { + return err + } + + utilization := k.SupplyUtilization(ctx, denom) + if utilization.GT(token.MaxSupplyUtilization) { + return types.ErrMaxSupplyUtilization.Wrap(utilization.String()) + } + return nil +} diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 14d003b25d..cc8a08af80 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -125,9 +125,10 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co // Withdraw attempts to redeem uTokens from the leverage module in exchange for base tokens. // If there are not enough uTokens in balance, Withdraw will attempt to withdraw uToken collateral -// to make up the difference (as long as borrow limit allows). If the uToken denom is invalid or -// balances are insufficient to withdraw the full amount requested, returns an error. -// Returns the amount of base tokens received. +// to make up the difference. If the uToken denom is invalid or balances are insufficient to withdraw +// the amount requested, returns an error. Returns the amount of base tokens received. +// This function does NOT check that a borrower remains under their borrow limit or that +// collateral liquidity remains healthy - those assertions have been moved to MsgServer. func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, error) { if err := validateUToken(uToken); err != nil { return sdk.Coin{}, err @@ -151,13 +152,6 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd amountFromCollateral := uToken.Amount.Sub(amountFromWallet) if amountFromCollateral.IsPositive() { - // Calculate current borrowed value - borrowed := k.GetBorrowerBorrows(ctx, supplierAddr) - borrowedValue, err := k.TotalTokenValue(ctx, borrowed, false) - if err != nil { - return sdk.Coin{}, err - } - // Check for sufficient collateral collateral := k.GetBorrowerCollateral(ctx, supplierAddr) collateralAmount := collateral.AmountOf(uToken.Denom) @@ -167,19 +161,6 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd amountFromWallet, collateralAmount, uToken) } - // Calculate what borrow limit will be AFTER this withdrawal - collateralToWithdraw := sdk.NewCoin(uToken.Denom, amountFromCollateral) - newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(collateralToWithdraw), false) - if err != nil { - return sdk.Coin{}, err - } - - // Return error if borrow limit would drop below borrowed value - if borrowedValue.GT(newBorrowLimit) { - return sdk.Coin{}, types.ErrUndercollaterized.Wrapf( - "withdraw would decrease borrow limit to %s, below the current borrowed value %s", newBorrowLimit, borrowedValue) - } - // reduce the supplier's collateral by amountFromCollateral newCollateral := sdk.NewCoin(uToken.Denom, collateralAmount.Sub(amountFromCollateral)) if err = k.setCollateral(ctx, supplierAddr, newCollateral); err != nil { @@ -207,17 +188,14 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd return sdk.Coin{}, err } - // check MinCollateralLiquidity is still satisfied after the transaction - if err = k.checkCollateralLiquidity(ctx, token.Denom); err != nil { - return sdk.Coin{}, err - } - return token, nil } // Borrow attempts to borrow tokens from the leverage module account using -// collateral uTokens. If asset type is invalid, collateral is insufficient, -// or module balance is insufficient, we return an error. +// collateral uTokens. If asset type is invalid, or module balance is insufficient, +// we return an error. +// This function does NOT check that a borrower remains under their borrow limit or that +// collateral liquidity remains healthy - those assertions have been moved to MsgServer. func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk.Coin) error { if err := k.validateBorrow(ctx, borrow); err != nil { return err @@ -232,27 +210,8 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. // Determine amount of all tokens currently borrowed borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) - // Calculate current borrow limit - collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) - borrowLimit, err := k.CalculateBorrowLimit(ctx, collateral, false) - if err != nil { - return err - } - - // Calculate borrowed value will be AFTER this borrow - newBorrowedValue, err := k.TotalTokenValue(ctx, borrowed.Add(borrow), false) - if err != nil { - return err - } - - // Return error if borrowed value would exceed borrow limit - if newBorrowedValue.GT(borrowLimit) { - return types.ErrUndercollaterized.Wrapf("new borrowed value would be %s with borrow limit %s", - newBorrowedValue, borrowLimit) - } - - loanTokens := sdk.NewCoins(borrow) - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, borrowerAddr, loanTokens); err != nil { + if err := k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, types.ModuleName, borrowerAddr, sdk.NewCoins(borrow)); err != nil { return err } @@ -262,18 +221,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. return err } - // Check MaxSupplyUtilization after transaction - token, err := k.GetTokenSettings(ctx, borrow.Denom) - if err != nil { - return err - } - utilization := k.SupplyUtilization(ctx, borrow.Denom) - if utilization.GT(token.MaxSupplyUtilization) { - return types.ErrMaxSupplyUtilization.Wrap(utilization.String()) - } - - // check MinCollateralLiquidity is still satisfied after the transaction - return k.checkCollateralLiquidity(ctx, borrow.Denom) + return nil } // Repay attempts to repay a borrow position. If asset type is invalid, account balance @@ -326,6 +274,8 @@ func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uTok } // Decollateralize disables selected uTokens for use as collateral by a single borrower. +// This function does NOT check that a borrower remains under their borrow limit. +// That assertion has been moved to MsgServer. func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uToken sdk.Coin) error { if err := validateUToken(uToken); err != nil { return err @@ -337,24 +287,6 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uT return types.ErrInsufficientCollateral } - // Determine what borrow limit would be AFTER disabling this denom as collateral - newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(uToken), false) - if err != nil { - return err - } - - // Determine currently borrowed value - borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) - borrowedValue, err := k.TotalTokenValue(ctx, borrowed, false) - if err != nil { - return err - } - - // Return error if borrow limit would drop below borrowed value - if newBorrowLimit.LT(borrowedValue) { - return types.ErrUndercollaterized.Wrap("new borrow limit: " + newBorrowLimit.String()) - } - // Disabling uTokens as collateral withdraws any stored collateral of the denom in question // from the module account and returns it to the user newCollateralAmount := collateral.AmountOf(uToken.Denom).Sub(uToken.Amount) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index e52ae6120c..58c9d79ca8 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -67,6 +67,17 @@ func (s msgServer) Withdraw( return nil, err } + // Fail here if supplier ends up over their borrow limit under current or historic prices + err = s.keeper.checkBorrowerHealth(ctx, supplierAddr) + if err != nil { + return nil, err + } + + // Ensure MinCollateralLiquidity is still satisfied after the transaction + if err = s.keeper.checkCollateralLiquidity(ctx, received.Denom); err != nil { + return nil, err + } + err = s.logWithdrawal(ctx, msg.Supplier, msg.Asset, received, "supplied assets withdrawn") return &types.MsgWithdrawResponse{ Received: received, @@ -94,6 +105,17 @@ func (s msgServer) MaxWithdraw( return nil, err } + // Fail here if supplier ends up over their borrow limit under current or historic prices + err = s.keeper.checkBorrowerHealth(ctx, supplierAddr) + if err != nil { + return nil, err + } + + // Ensure MinCollateralLiquidity is still satisfied after the transaction + if err = s.keeper.checkCollateralLiquidity(ctx, received.Denom); err != nil { + return nil, err + } + err = s.logWithdrawal(ctx, msg.Supplier, uToken, received, "maximum supplied assets withdrawn") return &types.MsgMaxWithdrawResponse{ Withdrawn: uToken, @@ -201,6 +223,12 @@ func (s msgServer) Decollateralize( return nil, err } + // Fail here if borrower ends up over their borrow limit under current or historic prices + err = s.keeper.checkBorrowerHealth(ctx, borrowerAddr) + if err != nil { + return nil, err + } + s.keeper.Logger(ctx).Debug( "collateral removed", "borrower", msg.Borrower, @@ -227,6 +255,22 @@ func (s msgServer) Borrow( return nil, err } + // Fail here if borrower ends up over their borrow limit under current or historic prices + err = s.keeper.checkBorrowerHealth(ctx, borrowerAddr) + if err != nil { + return nil, err + } + + // Check MaxSupplyUtilization after transaction + if err = s.keeper.checkSupplyUtilization(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + + // Check MinCollateralLiquidity is still satisfied after the transaction + if err = s.keeper.checkCollateralLiquidity(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + s.keeper.Logger(ctx).Debug( "assets borrowed", "borrower", msg.Borrower, From f481911734229634a66e1796d7ca2a9e3ae29e28 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 12:07:11 -0700 Subject: [PATCH 09/36] refactor MsgCollateralize --- x/leverage/keeper/keeper.go | 13 +++---------- x/leverage/keeper/msg_server.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index cc8a08af80..06734e5ed7 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -251,6 +251,8 @@ func (k Keeper) Repay(ctx sdk.Context, borrowerAddr sdk.AccAddress, payment sdk. } // Collateralize enables selected uTokens for use as collateral by a single borrower. +// This function does NOT check that collateral share and collateral liquidity remain healthy. +// Those assertions have been moved to MsgServer. func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uToken sdk.Coin) error { if err := k.validateCollateralize(ctx, uToken); err != nil { return err @@ -261,16 +263,7 @@ func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uTok return err } - err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrowerAddr, types.ModuleName, sdk.NewCoins(uToken)) - if err != nil { - return err - } - - if err := k.checkCollateralLiquidity(ctx, types.ToTokenDenom(uToken.Denom)); err != nil { - return err - } - - return k.checkCollateralShare(ctx, uToken.Denom) + return k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrowerAddr, types.ModuleName, sdk.NewCoins(uToken)) } // Decollateralize disables selected uTokens for use as collateral by a single borrower. diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 58c9d79ca8..a84286ff4e 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -152,6 +152,14 @@ func (s msgServer) Collateralize( return nil, err } + if err := s.keeper.checkCollateralLiquidity(ctx, types.ToTokenDenom(msg.Asset.Denom)); err != nil { + return nil, err + } + + if err := s.keeper.checkCollateralShare(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + s.keeper.Logger(ctx).Debug( "collateral added", "borrower", msg.Borrower, @@ -182,6 +190,15 @@ func (s msgServer) SupplyCollateral( return nil, err } + // Fail here if collateral share or liquidity restrictions are violated + if err := s.keeper.checkCollateralLiquidity(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + + if err := s.keeper.checkCollateralShare(ctx, uToken.Denom); err != nil { + return nil, err + } + s.keeper.Logger(ctx).Debug( "assets supplied", "supplier", msg.Supplier, From 37298a5aca9fbe7ecbfb3ba7d5d6b6f1a36d0f67 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 12:20:50 -0700 Subject: [PATCH 10/36] move some tests from Keeper to MsgServer due to liquidity checks being moved --- x/leverage/keeper/keeper_test.go | 90 ----------------------- x/leverage/keeper/msg_server_test.go | 106 +++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 90 deletions(-) diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index 462370f427..2d915d6ec7 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -956,93 +956,3 @@ func (s *IntegrationTestSuite) TestLiquidate() { } } } - -func (s *IntegrationTestSuite) TestMaxCollateralShare() { - app, ctx, require := s.app, s.ctx, s.Require() - - // update initial ATOM to have a limited MaxCollateralShare - atom, err := app.LeverageKeeper.GetTokenSettings(ctx, atomDenom) - require.NoError(err) - atom.MaxCollateralShare = sdk.MustNewDecFromStr("0.1") - s.registerToken(atom) - - // Mock oracle prices: - // UMEE $4.21 - // ATOM $39.38 - - // create a supplier to collateralize 100 UMEE, worth $421.00 - umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) - - // create an ATOM supplier - atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(atomSupplier, coin(atomDenom, 100_000000)) - - // collateralize 1.18 ATOM, worth $46.46, with no error. - // total collateral value (across all denoms) will be $467.46 - // so ATOM's collateral share ($46.46 / $467.46) is barely below 10% - s.collateralize(atomSupplier, coin("u/"+atomDenom, 1_180000)) - - // attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM - err = app.LeverageKeeper.Collateralize(ctx, atomSupplier, coin("u/"+atomDenom, 10000)) - require.ErrorIs(err, types.ErrMaxCollateralShare) -} - -func (s *IntegrationTestSuite) TestMinCollateralLiquidity() { - app, ctx, require := s.app, s.ctx, s.Require() - - // update initial UMEE to have a limited MinCollateralLiquidity - umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) - require.NoError(err) - umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") - s.registerToken(umee) - - // create a supplier to collateralize 100 UMEE - umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) - - // create an ATOM supplier and borrow 49 UMEE - atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(atomSupplier, coin(atomDenom, 100_000000)) - s.collateralize(atomSupplier, coin("u/"+atomDenom, 100_000000)) - s.borrow(atomSupplier, coin(umeeDenom, 49_000000)) - - // collateral liquidity (liquidity / collateral) of UMEE is 51/100 - - // withdrawal would reduce collateral liquidity to 41/90 - _, err = app.LeverageKeeper.Withdraw(ctx, umeeSupplier, coin("u/"+umeeDenom, 10_000000)) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "withdraw") - - // borrow would reduce collateral liquidity to 41/100 - err = app.LeverageKeeper.Borrow(ctx, umeeSupplier, coin(umeeDenom, 10_000000)) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "borrow") -} - -func (s *IntegrationTestSuite) TestMinCollateralLiquidity_Collateralize() { - app, ctx, require := s.app, s.ctx, s.Require() - - // update initial UMEE to have a limited MinCollateralLiquidity - umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) - require.NoError(err) - umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") - s.registerToken(umee) - - // create a supplier to supply 200 UMEE, and collateralize 100 UMEE - umeeSupplier := s.newAccount(coin(umeeDenom, 200)) - s.supply(umeeSupplier, coin(umeeDenom, 200)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100)) - - // create an ATOM supplier and borrow 149 UMEE - atomSupplier := s.newAccount(coin(atomDenom, 100)) - s.supply(atomSupplier, coin(atomDenom, 100)) - s.collateralize(atomSupplier, coin("u/"+atomDenom, 100)) - s.borrow(atomSupplier, coin(umeeDenom, 149)) - - // collateral liquidity (liquidity / collateral) of UMEE is 51/100 - - // collateralize would reduce collateral liquidity to 51/200 - err = app.LeverageKeeper.Collateralize(ctx, umeeSupplier, coin("u/"+umeeDenom, 100)) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "collateralize") -} diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 0964dedf5c..aa00b1f166 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -168,3 +168,109 @@ func (s *IntegrationTestSuite) TestUpdateRegistry() { }) } } + +func (s *IntegrationTestSuite) TestMaxCollateralShare() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial ATOM to have a limited MaxCollateralShare + atom, err := app.LeverageKeeper.GetTokenSettings(ctx, atomDenom) + require.NoError(err) + atom.MaxCollateralShare = sdk.MustNewDecFromStr("0.1") + s.registerToken(atom) + + // Mock oracle prices: + // UMEE $4.21 + // ATOM $39.38 + + // create a supplier to collateralize 100 UMEE, worth $421.00 + umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) + + // create an ATOM supplier + atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(atomSupplier, coin(atomDenom, 100_000000)) + + // collateralize 1.18 ATOM, worth $46.46, with no error. + // total collateral value (across all denoms) will be $467.46 + // so ATOM's collateral share ($46.46 / $467.46) is barely below 10% + s.collateralize(atomSupplier, coin("u/"+atomDenom, 1_180000)) + + // attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM + msg := &types.MsgCollateralize{ + Borrower: atomSupplier.String(), + Asset: coin("u/"+atomDenom, 10000), + } + _, err = srv.Collateralize(ctx, msg) + require.ErrorIs(err, types.ErrMaxCollateralShare) +} + +func (s *IntegrationTestSuite) TestMinCollateralLiquidity() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial UMEE to have a limited MinCollateralLiquidity + umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) + require.NoError(err) + umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") + s.registerToken(umee) + + // create a supplier to collateralize 100 UMEE + umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) + + // create an ATOM supplier and borrow 49 UMEE + atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(atomSupplier, coin(atomDenom, 100_000000)) + s.collateralize(atomSupplier, coin("u/"+atomDenom, 100_000000)) + s.borrow(atomSupplier, coin(umeeDenom, 49_000000)) + + // collateral liquidity (liquidity / collateral) of UMEE is 51/100 + + // withdrawal would reduce collateral liquidity to 41/90 + msg1 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin("u/"+umeeDenom, 10_000000), + } + _, err = srv.Withdraw(ctx, msg1) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "withdraw") + + // borrow would reduce collateral liquidity to 41/100 + msg2 := &types.MsgBorrow{ + Borrower: umeeSupplier.String(), + Asset: coin(umeeDenom, 10_000000), + } + _, err = srv.Borrow(ctx, msg2) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "borrow") +} + +func (s *IntegrationTestSuite) TestMinCollateralLiquidity_Collateralize() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial UMEE to have a limited MinCollateralLiquidity + umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) + require.NoError(err) + umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") + s.registerToken(umee) + + // create a supplier to supply 200 UMEE, and collateralize 100 UMEE + umeeSupplier := s.newAccount(coin(umeeDenom, 200)) + s.supply(umeeSupplier, coin(umeeDenom, 200)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100)) + + // create an ATOM supplier and borrow 149 UMEE + atomSupplier := s.newAccount(coin(atomDenom, 100)) + s.supply(atomSupplier, coin(atomDenom, 100)) + s.collateralize(atomSupplier, coin("u/"+atomDenom, 100)) + s.borrow(atomSupplier, coin(umeeDenom, 149)) + + // collateral liquidity (liquidity / collateral) of UMEE is 51/100 + + // collateralize would reduce collateral liquidity to 51/200 + msg := &types.MsgCollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin("u/"+umeeDenom, 100), + } + _, err = srv.Collateralize(ctx, msg) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "collateralize") +} From 49fbedf703c2b7eae80c651b5a5e7a01d27ff1bf Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 12 Dec 2022 13:12:05 -0700 Subject: [PATCH 11/36] MaxSuply check moved --- x/leverage/keeper/borrows.go | 4 ++++ x/leverage/keeper/keeper.go | 15 --------------- x/leverage/keeper/msg_server.go | 10 ++++++++++ x/leverage/keeper/supply.go | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 7914bfebbb..0de7c69041 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -32,6 +32,10 @@ func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress /* + // TODO: Only do this if stamp amount is > 0 + + // TODO: Parameterize different math? (min, max, median, mean) + // TODO: Comment this back in once all tests have a mock oracle which supports historic prices // Check using historic prices diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 06734e5ed7..9f27482038 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -79,21 +79,6 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co return sdk.Coin{}, err } - token, err := k.GetTokenSettings(ctx, coin.Denom) - if err != nil { - return sdk.Coin{}, err - } - - total, err := k.GetTotalSupply(ctx, coin.Denom) - if err != nil { - return sdk.Coin{}, err - } - - if token.MaxSupply.IsPositive() && total.Add(coin).Amount.GTE(token.MaxSupply) { - return sdk.Coin{}, types.ErrMaxSupply.Wrapf("attempted: %s, current supply: %s, max supply: %s", - coin, total.Amount, token.MaxSupply) - } - // determine uToken amount to mint uToken, err := k.ExchangeToken(ctx, coin) if err != nil { diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index a84286ff4e..288d2879c0 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -36,6 +36,11 @@ func (s msgServer) Supply( return nil, err } + // Fail here if MaxSupply is exceeded + if err = s.keeper.checkMaxSupply(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + s.keeper.Logger(ctx).Debug( "assets supplied", "supplier", msg.Supplier, @@ -190,6 +195,11 @@ func (s msgServer) SupplyCollateral( return nil, err } + // Fail here if MaxSupply is exceeded + if err = s.keeper.checkMaxSupply(ctx, msg.Asset.Denom); err != nil { + return nil, err + } + // Fail here if collateral share or liquidity restrictions are violated if err := s.keeper.checkCollateralLiquidity(ctx, msg.Asset.Denom); err != nil { return nil, err diff --git a/x/leverage/keeper/supply.go b/x/leverage/keeper/supply.go index 41aec914b3..78a9bfdd50 100644 --- a/x/leverage/keeper/supply.go +++ b/x/leverage/keeper/supply.go @@ -52,3 +52,24 @@ func (k Keeper) GetTotalSupply(ctx sdk.Context, denom string) (sdk.Coin, error) uTokenDenom := types.ToUTokenDenom(denom) return k.ExchangeUToken(ctx, k.GetUTokenSupply(ctx, uTokenDenom)) } + +// checkMaxSupply returns the appropriate error if a token denom's +// supply has exceeded MaxSupply +func (k Keeper) checkMaxSupply(ctx sdk.Context, denom string) error { + token, err := k.GetTokenSettings(ctx, denom) + if err != nil { + return err + } + + total, err := k.GetTotalSupply(ctx, denom) + if err != nil { + return err + } + + if token.MaxSupply.IsPositive() && total.Amount.GTE(token.MaxSupply) { + return types.ErrMaxSupply.Wrapf("current supply: %s, max supply: %s", + total.Amount, token.MaxSupply) + } + + return nil +} From 588b01104dff0d5dbac67c011efdffb1dce5aa64 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:15:36 -0700 Subject: [PATCH 12/36] use AvailableMedians return value --- x/leverage/keeper/borrows.go | 40 ++++++++++++------------------ x/leverage/keeper/oracle.go | 10 +++++++- x/leverage/keeper/oracle_test.go | 6 ++--- x/leverage/types/expected_types.go | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 0de7c69041..5e1e7af3aa 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -8,8 +8,9 @@ import ( ) // checkBorrowerHealth returns an error if a borrower is currently above their borrow limit, -// under either recent (historic median) or current prices. It also returns an error if -// relevant prices cannot be calculated. +// under either recent (historic median) or current prices. It returns an error if current +// prices cannot be calculated, but will use current prices (without returning an error) +// for any token whose historic prices cannot be calculated. // This should be checked at the end of any transaction which is restricted by borrow limits, // i.e. Borrow, Decollateralize, Withdraw. func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { @@ -30,29 +31,20 @@ func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress "borrowed: %s, limit: %s (current prices)", currentValue, currentLimit) } - /* - - // TODO: Only do this if stamp amount is > 0 - - // TODO: Parameterize different math? (min, max, median, mean) - - // TODO: Comment this back in once all tests have a mock oracle which supports historic prices - - // Check using historic prices - historicValue, err := k.TotalTokenValue(ctx, borrowed, true) - if err != nil { - return err - } - historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) - if err != nil { - return err - } - if historicValue.GT(historicLimit) { - return types.ErrUndercollaterized.Wrapf( - "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) - } + // Check using historic prices + historicValue, err := k.TotalTokenValue(ctx, borrowed, true) + if err != nil { + return err + } + historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) + if err != nil { + return err + } + if historicValue.GT(historicLimit) { + return types.ErrUndercollaterized.Wrapf( + "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) + } - */ return nil } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index c791dc6519..f97d8135a1 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -56,9 +56,17 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string, histor } var price sdk.Dec + if historic { - price, err = k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) + // historic price + var numStamps uint32 + price, numStamps, err = k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) + if err == nil && numStamps == 0 { + // if no price medians were available, current price is used as the historic price + price, err = k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + } } else { + // current price price, err = k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) } if err != nil { diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index d5ab91cdfa..669de3c72e 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -27,13 +27,13 @@ func newMockOracleKeeper() *mockOracleKeeper { } func (m *mockOracleKeeper) MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64, -) (sdk.Dec, error) { +) (sdk.Dec, uint32, error) { p, ok := m.historicExchangeRates[denom] if !ok { - return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) + return sdk.ZeroDec(), 0, fmt.Errorf("invalid denom: %s", denom) } - return p, nil + return p, 24, nil } func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec, error) { diff --git a/x/leverage/types/expected_types.go b/x/leverage/types/expected_types.go index d614a9b83f..f408edc9fe 100644 --- a/x/leverage/types/expected_types.go +++ b/x/leverage/types/expected_types.go @@ -33,5 +33,5 @@ type BankKeeper interface { type OracleKeeper interface { GetExchangeRate(ctx sdk.Context, denom string) (sdk.Dec, error) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error) - MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64) (sdk.Dec, error) + MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64) (sdk.Dec, uint32, error) } From 0641e2529082c24faa01b7562a1a0969d6fb3377 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:21:24 -0700 Subject: [PATCH 13/36] lint++ --- x/leverage/keeper/keeper.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 9f27482038..894cbcbbee 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -202,11 +202,7 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. // Determine the total amount of denom borrowed (previously borrowed + newly borrowed) newBorrow := borrowed.AmountOf(borrow.Denom).Add(borrow.Amount) - if err := k.setBorrow(ctx, borrowerAddr, sdk.NewCoin(borrow.Denom, newBorrow)); err != nil { - return err - } - - return nil + return k.setBorrow(ctx, borrowerAddr, sdk.NewCoin(borrow.Denom, newBorrow)) } // Repay attempts to repay a borrow position. If asset type is invalid, account balance From 107ae1fb879c362b6b7e94d3c3c720ba9b67a39f Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 20 Dec 2022 21:25:08 -0700 Subject: [PATCH 14/36] previous tests fully refactored --- x/leverage/keeper/keeper_test.go | 958 -------------------------- x/leverage/keeper/msg_server_test.go | 985 +++++++++++++++++++++++++++ 2 files changed, 985 insertions(+), 958 deletions(-) delete mode 100644 x/leverage/keeper/keeper_test.go diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go deleted file mode 100644 index 2d915d6ec7..0000000000 --- a/x/leverage/keeper/keeper_test.go +++ /dev/null @@ -1,958 +0,0 @@ -package keeper_test - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - - "github.com/umee-network/umee/v3/x/leverage/types" -) - -func (s *IntegrationTestSuite) TestSupply() { - type testCase struct { - msg string - addr sdk.AccAddress - coin sdk.Coin - expectedUTokens sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a supplier with 100 UMEE and 100 ATOM - supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - - // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.5 - borrower := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(borrower, coin(atomDenom, 100_000000)) - s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) - s.borrow(borrower, coin(atomDenom, 10_000000)) - s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 60_000000)) - - // create a supplier that will exceed token's default MaxSupply - whale := s.newAccount(coin(umeeDenom, 1_000_000_000000)) - - tcs := []testCase{ - { - "unregistered denom", - supplier, - coin("abcd", 80_000000), - sdk.Coin{}, - types.ErrNotRegisteredToken, - }, - { - "uToken", - supplier, - coin("u/"+umeeDenom, 80_000000), - sdk.Coin{}, - types.ErrUToken, - }, - { - "no balance", - borrower, - coin(umeeDenom, 20_000000), - sdk.Coin{}, - sdkerrors.ErrInsufficientFunds, - }, - { - "insufficient balance", - supplier, - coin(umeeDenom, 120_000000), - sdk.Coin{}, - sdkerrors.ErrInsufficientFunds, - }, - { - "valid supply", - supplier, - coin(umeeDenom, 80_000000), - coin("u/"+umeeDenom, 80_000000), - nil, - }, - { - "additional supply", - supplier, - coin(umeeDenom, 20_000000), - coin("u/"+umeeDenom, 20_000000), - nil, - }, - { - "high exchange rate", - supplier, - coin(atomDenom, 60_000000), - coin("u/"+atomDenom, 40_000000), - nil, - }, - { - "max supply", - whale, - coin(umeeDenom, 1_000_000_000000), - sdk.Coin{}, - types.ErrMaxSupply, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - _, err := app.LeverageKeeper.Supply(ctx, tc.addr, tc.coin) - require.ErrorIs(err, tc.err, tc.msg) - } else { - denom := tc.coin.Denom - - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the outputs of supply function - uToken, err := app.LeverageKeeper.Supply(ctx, tc.addr, tc.coin) - require.NoError(err, tc.msg) - require.Equal(tc.expectedUTokens, uToken, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify token balance decreased and uToken balance increased by the expected amounts - require.Equal(iBalance.Sub(tc.coin).Add(tc.expectedUTokens), fBalance, tc.msg, "token balance") - // verify uToken collateral unchanged - require.Equal(iCollateral, fCollateral, tc.msg, "uToken collateral") - // verify uToken supply increased by the expected amount - require.Equal(iUTokenSupply.Add(tc.expectedUTokens), fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins are unchanged - require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestWithdraw() { - type testCase struct { - msg string - addr sdk.AccAddress - uToken sdk.Coin - expectFromBalance sdk.Coins - expectFromCollateral sdk.Coins - expectedTokens sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a supplier with 100 UMEE and 100 ATOM, then supply 100 UMEE and 50 ATOM - // also collateralize 75 of supplied UMEE - supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - s.supply(supplier, coin(umeeDenom, 100_000000)) - s.collateralize(supplier, coin("u/"+umeeDenom, 75_000000)) - s.supply(supplier, coin(atomDenom, 50_000000)) - - // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.2 - borrower := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(borrower, coin(atomDenom, 100_000000)) - s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) - s.borrow(borrower, coin(atomDenom, 10_000000)) - s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) - - // create an additional UMEE supplier - other := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(other, coin(umeeDenom, 100_000000)) - - tcs := []testCase{ - { - "unregistered base token", - supplier, - coin("abcd", 80_000000), - nil, - nil, - sdk.Coin{}, - types.ErrNotUToken, - }, - { - "base token", - supplier, - coin(umeeDenom, 80_000000), - nil, - nil, - sdk.Coin{}, - types.ErrNotUToken, - }, - { - "insufficient uTokens", - supplier, - coin("u/"+umeeDenom, 120_000000), - nil, - nil, - sdk.Coin{}, - types.ErrInsufficientBalance, - }, - { - "withdraw from balance", - supplier, - coin("u/"+umeeDenom, 10_000000), - sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), - nil, - coin(umeeDenom, 10_000000), - nil, - }, - { - "some from collateral", - supplier, - coin("u/"+umeeDenom, 80_000000), - sdk.NewCoins(coin("u/"+umeeDenom, 15_000000)), - sdk.NewCoins(coin("u/"+umeeDenom, 65_000000)), - coin(umeeDenom, 80_000000), - nil, - }, - { - "only from collateral", - supplier, - coin("u/"+umeeDenom, 10_000000), - nil, - sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), - coin(umeeDenom, 10_000000), - nil, - }, - { - "high exchange rate", - supplier, - coin("u/"+atomDenom, 50_000000), - sdk.NewCoins(coin("u/"+atomDenom, 50_000000)), - nil, - coin(atomDenom, 60_000000), - nil, - }, - { - "borrow limit", - borrower, - coin("u/"+atomDenom, 50_000000), - nil, - nil, - sdk.Coin{}, - types.ErrUndercollaterized, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - _, err := app.LeverageKeeper.Withdraw(ctx, tc.addr, tc.uToken) - require.ErrorIs(err, tc.err, tc.msg) - } else { - denom := types.ToTokenDenom(tc.uToken.Denom) - - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the outputs of withdraw function - token, err := app.LeverageKeeper.Withdraw(ctx, tc.addr, tc.uToken) - - require.NoError(err, tc.msg) - require.Equal(tc.expectedTokens, token, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify token balance increased by the expected amount - require.Equal(iBalance.Add(tc.expectedTokens).Sub(tc.expectFromBalance...), - fBalance, tc.msg, "token balance") - // verify uToken collateral decreased by the expected amount - s.requireEqualCoins(iCollateral.Sub(tc.expectFromCollateral...), fCollateral, tc.msg, "uToken collateral") - // verify uToken supply decreased by the expected amount - require.Equal(iUTokenSupply.Sub(tc.uToken), fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins are unchanged - require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestCollateralize() { - type testCase struct { - msg string - addr sdk.AccAddress - uToken sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a supplier with 200 UMEE, then supply 100 UMEE - supplier := s.newAccount(coin(umeeDenom, 200_000000)) - s.supply(supplier, coin(umeeDenom, 100_000000)) - - // create and fund another supplier - otherSupplier := s.newAccount(coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000)) - s.supply(otherSupplier, coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000)) - - tcs := []testCase{ - { - "base token", - supplier, - coin(umeeDenom, 80_000000), - types.ErrNotUToken, - }, - { - "unregistered uToken", - supplier, - coin("u/abcd", 80_000000), - types.ErrNotRegisteredToken, - }, - { - "wrong balance", - supplier, - coin("u/"+atomDenom, 10_000000), - sdkerrors.ErrInsufficientFunds, - }, - { - "valid collateralize", - supplier, - coin("u/"+umeeDenom, 80_000000), - nil, - }, - { - "additional collateralize", - supplier, - coin("u/"+umeeDenom, 10_000000), - nil, - }, - { - "insufficient balance", - supplier, - coin("u/"+umeeDenom, 40_000000), - sdkerrors.ErrInsufficientFunds, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - err := app.LeverageKeeper.Collateralize(ctx, tc.addr, tc.uToken) - require.ErrorIs(err, tc.err, tc.msg) - } else { - denom := types.ToTokenDenom(tc.uToken.Denom) - - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the output of collateralize function - err := app.LeverageKeeper.Collateralize(ctx, tc.addr, tc.uToken) - require.NoError(err, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify uToken balance decreased by the expected amount - require.Equal(iBalance.Sub(tc.uToken), fBalance, tc.msg, "uToken balance") - // verify uToken collateral increased by the expected amount - require.Equal(iCollateral.Add(tc.uToken), fCollateral, tc.msg, "uToken collateral") - // verify uToken supply is unchanged - require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins are unchanged - require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestDecollateralize() { - type testCase struct { - msg string - addr sdk.AccAddress - uToken sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a supplier with 200 UMEE, then supply and collateralize 100 UMEE - supplier := s.newAccount(coin(umeeDenom, 200_000000)) - s.supply(supplier, coin(umeeDenom, 100_000000)) - s.collateralize(supplier, coin("u/"+umeeDenom, 100_000000)) - - // create a borrower which supplies, collateralizes, then borrows ATOM - borrower := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(borrower, coin(atomDenom, 100_000000)) - s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) - s.borrow(borrower, coin(atomDenom, 10_000000)) - - tcs := []testCase{ - { - "base token", - supplier, - coin(umeeDenom, 80_000000), - types.ErrNotUToken, - }, - { - "no collateral", - supplier, - coin("u/"+atomDenom, 40_000000), - types.ErrInsufficientCollateral, - }, - { - "valid decollateralize", - supplier, - coin("u/"+umeeDenom, 80_000000), - nil, - }, - { - "additional decollateralize", - supplier, - coin("u/"+umeeDenom, 10_000000), - nil, - }, - { - "insufficient collateral", - supplier, - coin("u/"+umeeDenom, 40_000000), - types.ErrInsufficientCollateral, - }, - { - "borrow limit", - borrower, - coin("u/"+atomDenom, 100_000000), - types.ErrUndercollaterized, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - err := app.LeverageKeeper.Decollateralize(ctx, tc.addr, tc.uToken) - require.ErrorIs(err, tc.err, tc.msg) - } else { - denom := types.ToTokenDenom(tc.uToken.Denom) - - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the output of decollateralize function - err := app.LeverageKeeper.Decollateralize(ctx, tc.addr, tc.uToken) - require.NoError(err, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify uToken balance increased by the expected amount - require.Equal(iBalance.Add(tc.uToken), fBalance, tc.msg, "uToken balance") - // verify uToken collateral decreased by the expected amount - require.Equal(iCollateral.Sub(tc.uToken), fCollateral, tc.msg, "uToken collateral") - // verify uToken supply is unchanged - require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins are unchanged - require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestBorrow() { - type testCase struct { - msg string - addr sdk.AccAddress - coin sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a supplier which supplies UMEE and ATOM - supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - - // create a borrower which supplies and collateralizes 100 ATOM - borrower := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(borrower, coin(atomDenom, 100_000000)) - s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) - - tcs := []testCase{ - { - "uToken", - borrower, - coin("u/"+umeeDenom, 100_000000), - types.ErrUToken, - }, - { - "unregistered token", - borrower, - coin("abcd", 100_000000), - types.ErrNotRegisteredToken, - }, - { - "lending pool insufficient", - borrower, - coin(umeeDenom, 200_000000), - types.ErrLendingPoolInsufficient, - }, - { - "valid borrow", - borrower, - coin(umeeDenom, 70_000000), - nil, - }, - { - "additional borrow", - borrower, - coin(umeeDenom, 20_000000), - nil, - }, - { - "max supply utilization", - borrower, - coin(umeeDenom, 10_000000), - types.ErrMaxSupplyUtilization, - }, - { - "atom borrow", - borrower, - coin(atomDenom, 1_000000), - nil, - }, - { - "borrow limit", - borrower, - coin(atomDenom, 100_000000), - types.ErrUndercollaterized, - }, - { - "zero collateral", - supplier, - coin(atomDenom, 100_000000), - types.ErrUndercollaterized, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - err := app.LeverageKeeper.Borrow(ctx, tc.addr, tc.coin) - require.ErrorIs(err, tc.err, tc.msg) - } else { - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the output of borrow function - err := app.LeverageKeeper.Borrow(ctx, tc.addr, tc.coin) - require.NoError(err, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify token balance is increased by expected amount - require.Equal(iBalance.Add(tc.coin), fBalance, tc.msg, "balances") - // verify uToken collateral unchanged - require.Equal(iCollateral, fCollateral, tc.msg, "collateral") - // verify uToken supply is unchanged - require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins increased by expected amount - require.Equal(iBorrowed.Add(tc.coin), fBorrowed, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestRepay() { - type testCase struct { - msg string - addr sdk.AccAddress - coin sdk.Coin - expectedRepay sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a borrower which supplies and collateralizes UMEE, then borrows 10 UMEE - borrower := s.newAccount(coin(umeeDenom, 200_000000)) - s.supply(borrower, coin(umeeDenom, 150_000000)) - s.collateralize(borrower, coin("u/"+umeeDenom, 120_000000)) - s.borrow(borrower, coin(umeeDenom, 10_000000)) - - // create and fund a borrower which engages in a supply->borrow->supply loop - looper := s.newAccount(coin(umeeDenom, 50_000000)) - s.supply(looper, coin(umeeDenom, 50_000000)) - s.collateralize(looper, coin("u/"+umeeDenom, 50_000000)) - s.borrow(looper, coin(umeeDenom, 5_000000)) - s.supply(looper, coin(umeeDenom, 5_000000)) - - tcs := []testCase{ - { - "uToken", - borrower, - coin("u/"+umeeDenom, 100_000000), - sdk.Coin{}, - types.ErrUToken, - }, - { - "unregistered token", - borrower, - coin("abcd", 100_000000), - sdk.Coin{}, - types.ErrDenomNotBorrowed, - }, - { - "not borrowed", - borrower, - coin(atomDenom, 100_000000), - sdk.Coin{}, - types.ErrDenomNotBorrowed, - }, - { - "valid repay", - borrower, - coin(umeeDenom, 1_000000), - coin(umeeDenom, 1_000000), - nil, - }, - { - "additional repay", - borrower, - coin(umeeDenom, 3_000000), - coin(umeeDenom, 3_000000), - nil, - }, - { - "overpay", - borrower, - coin(umeeDenom, 30_000000), - coin(umeeDenom, 6_000000), - nil, - }, - { - "insufficient balance", - looper, - coin(umeeDenom, 1_000000), - sdk.Coin{}, - sdkerrors.ErrInsufficientFunds, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - _, err := app.LeverageKeeper.Repay(ctx, tc.addr, tc.coin) - require.ErrorIs(err, tc.err, tc.msg) - } else { - // initial state - iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) - iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify the output of repay function - repaid, err := app.LeverageKeeper.Repay(ctx, tc.addr, tc.coin) - require.NoError(err, tc.msg) - require.Equal(tc.expectedRepay, repaid, tc.msg) - - // final state - fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) - fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) - fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) - fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) - - // verify token balance is decreased by expected amount - require.Equal(iBalance.Sub(tc.expectedRepay), fBalance, tc.msg, "balances") - // verify uToken collateral unchanged - require.Equal(iCollateral, fCollateral, tc.msg, "collateral") - // verify uToken supply is unchanged - require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") - // verify uToken exchange rate is unchanged - require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") - // verify borrowed coins decreased by expected amount - s.requireEqualCoins(iBorrowed.Sub(tc.expectedRepay), fBorrowed, "borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} - -func (s *IntegrationTestSuite) TestLiquidate() { - type testCase struct { - msg string - liquidator sdk.AccAddress - borrower sdk.AccAddress - attemptedRepay sdk.Coin - rewardDenom string - expectedRepay sdk.Coin - expectedLiquidate sdk.Coin - expectedReward sdk.Coin - err error - } - - app, ctx, require := s.app, s.ctx, s.Require() - - // create and fund a liquidator which supplies plenty of UMEE and ATOM to the module - supplier := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) - s.supply(supplier, coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) - - // create and fund a liquidator which has 1000 UMEE and 1000 ATOM - liquidator := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) - - // create a healthy borrower - healthyBorrower := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(healthyBorrower, coin(umeeDenom, 100_000000)) - s.collateralize(healthyBorrower, coin("u/"+umeeDenom, 100_000000)) - s.borrow(healthyBorrower, coin(umeeDenom, 10_000000)) - - // create a borrower which supplies and collateralizes 1000 ATOM - atomBorrower := s.newAccount(coin(atomDenom, 1000_000000)) - s.supply(atomBorrower, coin(atomDenom, 1000_000000)) - s.collateralize(atomBorrower, coin("u/"+atomDenom, 1000_000000)) - // artificially borrow 500 ATOM - this can be liquidated without bad debt - s.forceBorrow(atomBorrower, coin(atomDenom, 500_000000)) - - // create a borrower which collateralizes 110 UMEE - umeeBorrower := s.newAccount(coin(umeeDenom, 300_000000)) - s.supply(umeeBorrower, coin(umeeDenom, 200_000000)) - s.collateralize(umeeBorrower, coin("u/"+umeeDenom, 110_000000)) - // artificially borrow 200 UMEE - this will create a bad debt when liquidated - s.forceBorrow(umeeBorrower, coin(umeeDenom, 200_000000)) - - // creates a complex borrower with multiple denoms active - complexBorrower := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - s.supply(complexBorrower, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) - s.collateralize(complexBorrower, coin("u/"+umeeDenom, 100_000000), coin("u/"+atomDenom, 100_000000)) - // artificially borrow multiple denoms - s.forceBorrow(complexBorrower, coin(atomDenom, 30_000000), coin(umeeDenom, 30_000000)) - - // creates a realistic borrower with 400 UMEE collateral which will have a close factor < 1 - closeBorrower := s.newAccount(coin(umeeDenom, 400_000000)) - s.supply(closeBorrower, coin(umeeDenom, 400_000000)) - s.collateralize(closeBorrower, coin("u/"+umeeDenom, 400_000000)) - // artificially borrow just barely above liquidation threshold to simulate interest accruing - s.forceBorrow(closeBorrower, coin(umeeDenom, 102_000000)) - - tcs := []testCase{ - { - "healthy borrower", - liquidator, - healthyBorrower, - coin(atomDenom, 1_000000), - atomDenom, - sdk.Coin{}, - sdk.Coin{}, - sdk.Coin{}, - types.ErrLiquidationIneligible, - }, - { - "not borrowed denom", - liquidator, - umeeBorrower, - coin(atomDenom, 1_000000), - atomDenom, - sdk.Coin{}, - sdk.Coin{}, - sdk.Coin{}, - types.ErrLiquidationRepayZero, - }, - { - "direct atom liquidation", - liquidator, - atomBorrower, - coin(atomDenom, 100_000000), - atomDenom, - coin(atomDenom, 100_000000), - coin("u/"+atomDenom, 109_000000), - coin(atomDenom, 109_000000), - nil, - }, - { - "u/atom liquidation", - liquidator, - atomBorrower, - coin(atomDenom, 100_000000), - "u/" + atomDenom, - coin(atomDenom, 100_000000), - coin("u/"+atomDenom, 110_000000), - coin("u/"+atomDenom, 110_000000), - nil, - }, - { - "complete u/atom liquidation", - liquidator, - atomBorrower, - coin(atomDenom, 500_000000), - "u/" + atomDenom, - coin(atomDenom, 300_000000), - coin("u/"+atomDenom, 330_000000), - coin("u/"+atomDenom, 330_000000), - nil, - }, - { - "bad debt u/umee liquidation", - liquidator, - umeeBorrower, - coin(umeeDenom, 200_000000), - "u/" + umeeDenom, - coin(umeeDenom, 100_000000), - coin("u/"+umeeDenom, 110_000000), - coin("u/"+umeeDenom, 110_000000), - nil, - }, - { - "complex borrower", - liquidator, - complexBorrower, - coin(umeeDenom, 200_000000), - "u/" + atomDenom, - coin(umeeDenom, 30_000000), - coin("u/"+atomDenom, 3_527932), - coin("u/"+atomDenom, 3_527932), - nil, - }, - { - "close factor < 1", - liquidator, - closeBorrower, - coin(umeeDenom, 200_000000), - "u/" + umeeDenom, - coin(umeeDenom, 7_752000), - coin("u/"+umeeDenom, 8_527200), - coin("u/"+umeeDenom, 8_527200), - nil, - }, - } - - for _, tc := range tcs { - if tc.err != nil { - _, _, _, err := app.LeverageKeeper.Liquidate( - ctx, tc.liquidator, tc.borrower, tc.attemptedRepay, tc.rewardDenom, - ) - require.ErrorIs(err, tc.err, tc.msg) - } else { - baseRewardDenom := types.ToTokenDenom(tc.expectedLiquidate.Denom) - - // initial state (borrowed denom) - biUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - biExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) - - // initial state (liquidated denom) - liUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - liExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) - - // borrower initial state - biBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) - biCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) - biBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) - - // liquidator initial state - liBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) - liCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) - liBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) - - // verify the output of liquidate function - repaid, liquidated, reward, err := app.LeverageKeeper.Liquidate( - ctx, tc.liquidator, tc.borrower, tc.attemptedRepay, tc.rewardDenom, - ) - require.NoError(err, tc.msg) - require.Equal(tc.expectedRepay, repaid, tc.msg, "repaid") - require.Equal(tc.expectedLiquidate, liquidated, tc.msg, "liquidated") - require.Equal(tc.expectedReward, reward, tc.msg, "reward") - - // final state (liquidated denom) - lfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - lfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) - - // borrower final state - bfBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) - bfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) - bfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) - - // liquidator final state - lfBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) - lfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) - lfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) - - // if borrowed denom and reward denom are different, then borrowed denom uTokens should be unaffected - if tc.rewardDenom != tc.attemptedRepay.Denom { - // final state (borrowed denom) - bfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) - bfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) - - // verify borrowed denom uToken supply is unchanged - require.Equal(biUTokenSupply, bfUTokenSupply, tc.msg, "uToken supply (borrowed denom") - // verify borrowed denom uToken exchange rate is unchanged - require.Equal(biExchangeRate, bfExchangeRate, tc.msg, "uToken exchange rate (borrowed denom") - } - - // verify liquidated denom uToken supply is unchanged if indirect liquidation, or reduced if direct - expectedLiquidatedUTokenSupply := liUTokenSupply - if !types.HasUTokenPrefix(tc.rewardDenom) { - expectedLiquidatedUTokenSupply = expectedLiquidatedUTokenSupply.Sub(tc.expectedLiquidate) - } - require.Equal(expectedLiquidatedUTokenSupply, lfUTokenSupply, tc.msg, "uToken supply (liquidated denom") - // verify liquidated denom uToken exchange rate is unchanged - require.Equal(liExchangeRate, lfExchangeRate, tc.msg, "uToken exchange rate (liquidated denom") - - // verify borrower balances unchanged - require.Equal(biBalance, bfBalance, tc.msg, "borrower balances") - // verify borrower collateral reduced by the expected amount - s.requireEqualCoins(biCollateral.Sub(tc.expectedLiquidate), bfCollateral, tc.msg, "borrower collateral") - // verify borrowed coins decreased by expected amount - s.requireEqualCoins(biBorrowed.Sub(tc.expectedRepay), bfBorrowed, "borrowed coins") - - // verify liquidator balance changes by expected amounts - require.Equal(liBalance.Sub(tc.expectedRepay).Add(tc.expectedReward), lfBalance, - tc.msg, "liquidator balances") - // verify liquidator collateral unchanged - require.Equal(liCollateral, lfCollateral, tc.msg, "liquidator collateral") - // verify liquidator borrowed coins unchanged - s.requireEqualCoins(liBorrowed, lfBorrowed, "liquidator borrowed coins") - - // check all available invariants - s.checkInvariants(tc.msg) - } - } -} diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index aa00b1f166..f58ee8e345 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/umee-network/umee/v3/x/leverage/fixtures" "github.com/umee-network/umee/v3/x/leverage/types" ) @@ -274,3 +275,987 @@ func (s *IntegrationTestSuite) TestMinCollateralLiquidity_Collateralize() { _, err = srv.Collateralize(ctx, msg) require.ErrorIs(err, types.ErrMinCollateralLiquidity, "collateralize") } + +// +// +// TODO: Composite message types - SupplyCollateral, MaxWithdraw, ... +// +// + +func (s *IntegrationTestSuite) TestMsgSupply() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + expectedUTokens sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.5 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 60_000000)) + + // create a supplier that will exceed token's default MaxSupply + whale := s.newAccount(coin(umeeDenom, 1_000_000_000000)) + + tcs := []testCase{ + { + "unregistered denom", + supplier, + coin("abcd", 80_000000), + sdk.Coin{}, + types.ErrNotRegisteredToken, + }, + { + "uToken", + supplier, + coin("u/"+umeeDenom, 80_000000), + sdk.Coin{}, + types.ErrUToken, + }, + { + "no balance", + borrower, + coin(umeeDenom, 20_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "insufficient balance", + supplier, + coin(umeeDenom, 120_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "valid supply", + supplier, + coin(umeeDenom, 80_000000), + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional supply", + supplier, + coin(umeeDenom, 20_000000), + coin("u/"+umeeDenom, 20_000000), + nil, + }, + { + "high exchange rate", + supplier, + coin(atomDenom, 60_000000), + coin("u/"+atomDenom, 40_000000), + nil, + }, + { + "max supply", + whale, + coin(umeeDenom, 1_000_000_000000), + sdk.Coin{}, + types.ErrMaxSupply, + }, + } + + for _, tc := range tcs { + msg := &types.MsgSupply{ + Supplier: tc.addr.String(), + Asset: tc.coin, + } + if tc.err != nil { + _, err := srv.Supply(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := tc.coin.Denom + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of supply function + resp, err := srv.Supply(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedUTokens, resp.Received, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance decreased and uToken balance increased by the expected amounts + require.Equal(iBalance.Sub(tc.coin).Add(tc.expectedUTokens), fBalance, tc.msg, "token balance") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "uToken collateral") + // verify uToken supply increased by the expected amount + require.Equal(iUTokenSupply.Add(tc.expectedUTokens), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgWithdraw() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + expectFromBalance sdk.Coins + expectFromCollateral sdk.Coins + expectedTokens sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM, then supply 100 UMEE and 50 ATOM + // also collateralize 75 of supplied UMEE + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 75_000000)) + s.supply(supplier, coin(atomDenom, 50_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.2 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) + + // create an additional UMEE supplier + other := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(other, coin(umeeDenom, 100_000000)) + + tcs := []testCase{ + { + "unregistered base token", + supplier, + coin("abcd", 80_000000), + nil, + nil, + sdk.Coin{}, + types.ErrNotUToken, + }, + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + nil, + nil, + sdk.Coin{}, + types.ErrNotUToken, + }, + { + "insufficient uTokens", + supplier, + coin("u/"+umeeDenom, 120_000000), + nil, + nil, + sdk.Coin{}, + types.ErrInsufficientBalance, + }, + { + "withdraw from balance", + supplier, + coin("u/"+umeeDenom, 10_000000), + sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), + nil, + coin(umeeDenom, 10_000000), + nil, + }, + { + "some from collateral", + supplier, + coin("u/"+umeeDenom, 80_000000), + sdk.NewCoins(coin("u/"+umeeDenom, 15_000000)), + sdk.NewCoins(coin("u/"+umeeDenom, 65_000000)), + coin(umeeDenom, 80_000000), + nil, + }, + { + "only from collateral", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), + coin(umeeDenom, 10_000000), + nil, + }, + { + "high exchange rate", + supplier, + coin("u/"+atomDenom, 50_000000), + sdk.NewCoins(coin("u/"+atomDenom, 50_000000)), + nil, + coin(atomDenom, 60_000000), + nil, + }, + { + "borrow limit", + borrower, + coin("u/"+atomDenom, 50_000000), + nil, + nil, + sdk.Coin{}, + types.ErrUndercollaterized, + }, + } + + for _, tc := range tcs { + msg := &types.MsgWithdraw{ + Supplier: tc.addr.String(), + Asset: tc.uToken, + } + if tc.err != nil { + _, err := srv.Withdraw(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of withdraw function + resp, err := srv.Withdraw(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedTokens, resp.Received, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance increased by the expected amount + require.Equal(iBalance.Add(tc.expectedTokens).Sub(tc.expectFromBalance...), + fBalance, tc.msg, "token balance") + // verify uToken collateral decreased by the expected amount + s.requireEqualCoins(iCollateral.Sub(tc.expectFromCollateral...), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply decreased by the expected amount + require.Equal(iUTokenSupply.Sub(tc.uToken), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgCollateralize() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 200 UMEE, then supply 100 UMEE + supplier := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + + // create and fund another supplier + otherSupplier := s.newAccount(coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000)) + s.supply(otherSupplier, coin(umeeDenom, 200_000000), coin(atomDenom, 200_000000)) + + tcs := []testCase{ + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + types.ErrNotUToken, + }, + { + "unregistered uToken", + supplier, + coin("u/abcd", 80_000000), + types.ErrNotRegisteredToken, + }, + { + "wrong balance", + supplier, + coin("u/"+atomDenom, 10_000000), + sdkerrors.ErrInsufficientFunds, + }, + { + "valid collateralize", + supplier, + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional collateralize", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + }, + { + "insufficient balance", + supplier, + coin("u/"+umeeDenom, 40_000000), + sdkerrors.ErrInsufficientFunds, + }, + } + + for _, tc := range tcs { + msg := &types.MsgCollateralize{ + Borrower: tc.addr.String(), + Asset: tc.uToken, + } + if tc.err != nil { + _, err := srv.Collateralize(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of collateralize function + resp, err := srv.Collateralize(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(&types.MsgCollateralizeResponse{}, resp, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify uToken balance decreased by the expected amount + require.Equal(iBalance.Sub(tc.uToken), fBalance, tc.msg, "uToken balance") + // verify uToken collateral increased by the expected amount + require.Equal(iCollateral.Add(tc.uToken), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgDecollateralize() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 200 UMEE, then supply and collateralize 100 UMEE + supplier := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 100_000000)) + + // create a borrower which supplies, collateralizes, then borrows ATOM + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + + tcs := []testCase{ + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + types.ErrNotUToken, + }, + { + "no collateral", + supplier, + coin("u/"+atomDenom, 40_000000), + types.ErrInsufficientCollateral, + }, + { + "valid decollateralize", + supplier, + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional decollateralize", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + }, + { + "insufficient collateral", + supplier, + coin("u/"+umeeDenom, 40_000000), + types.ErrInsufficientCollateral, + }, + { + "borrow limit", + borrower, + coin("u/"+atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + } + + for _, tc := range tcs { + msg := &types.MsgDecollateralize{ + Borrower: tc.addr.String(), + Asset: tc.uToken, + } + if tc.err != nil { + _, err := srv.Decollateralize(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of decollateralize function + resp, err := srv.Decollateralize(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(&types.MsgDecollateralizeResponse{}, resp, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify uToken balance increased by the expected amount + require.Equal(iBalance.Add(tc.uToken), fBalance, tc.msg, "uToken balance") + // verify uToken collateral decreased by the expected amount + require.Equal(iCollateral.Sub(tc.uToken), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgBorrow() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier which supplies UMEE and ATOM + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + + // create a borrower which supplies and collateralizes 100 ATOM + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + + tcs := []testCase{ + { + "uToken", + borrower, + coin("u/"+umeeDenom, 100_000000), + types.ErrUToken, + }, + { + "unregistered token", + borrower, + coin("abcd", 100_000000), + types.ErrNotRegisteredToken, + }, + { + "lending pool insufficient", + borrower, + coin(umeeDenom, 200_000000), + types.ErrLendingPoolInsufficient, + }, + { + "valid borrow", + borrower, + coin(umeeDenom, 70_000000), + nil, + }, + { + "additional borrow", + borrower, + coin(umeeDenom, 20_000000), + nil, + }, + { + "max supply utilization", + borrower, + coin(umeeDenom, 10_000000), + types.ErrMaxSupplyUtilization, + }, + { + "atom borrow", + borrower, + coin(atomDenom, 1_000000), + nil, + }, + { + "borrow limit", + borrower, + coin(atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + { + "zero collateral", + supplier, + coin(atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + } + + for _, tc := range tcs { + msg := &types.MsgBorrow{ + Borrower: tc.addr.String(), + Asset: tc.coin, + } + if tc.err != nil { + _, err := srv.Borrow(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of borrow function + resp, err := srv.Borrow(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(&types.MsgBorrowResponse{}, resp, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance is increased by expected amount + require.Equal(iBalance.Add(tc.coin), fBalance, tc.msg, "balances") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins increased by expected amount + require.Equal(iBorrowed.Add(tc.coin), fBorrowed, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgRepay() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + expectedRepay sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a borrower which supplies and collateralizes UMEE, then borrows 10 UMEE + borrower := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(borrower, coin(umeeDenom, 150_000000)) + s.collateralize(borrower, coin("u/"+umeeDenom, 120_000000)) + s.borrow(borrower, coin(umeeDenom, 10_000000)) + + // create and fund a borrower which engages in a supply->borrow->supply loop + looper := s.newAccount(coin(umeeDenom, 50_000000)) + s.supply(looper, coin(umeeDenom, 50_000000)) + s.collateralize(looper, coin("u/"+umeeDenom, 50_000000)) + s.borrow(looper, coin(umeeDenom, 5_000000)) + s.supply(looper, coin(umeeDenom, 5_000000)) + + tcs := []testCase{ + { + "uToken", + borrower, + coin("u/"+umeeDenom, 100_000000), + sdk.Coin{}, + types.ErrUToken, + }, + { + "unregistered token", + borrower, + coin("abcd", 100_000000), + sdk.Coin{}, + types.ErrDenomNotBorrowed, + }, + { + "not borrowed", + borrower, + coin(atomDenom, 100_000000), + sdk.Coin{}, + types.ErrDenomNotBorrowed, + }, + { + "valid repay", + borrower, + coin(umeeDenom, 1_000000), + coin(umeeDenom, 1_000000), + nil, + }, + { + "additional repay", + borrower, + coin(umeeDenom, 3_000000), + coin(umeeDenom, 3_000000), + nil, + }, + { + "overpay", + borrower, + coin(umeeDenom, 30_000000), + coin(umeeDenom, 6_000000), + nil, + }, + { + "insufficient balance", + looper, + coin(umeeDenom, 1_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + } + + for _, tc := range tcs { + msg := &types.MsgRepay{ + Borrower: tc.addr.String(), + Asset: tc.coin, + } + if tc.err != nil { + _, err := srv.Repay(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of repay function + resp, err := srv.Repay(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedRepay, resp.Repaid, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance is decreased by expected amount + require.Equal(iBalance.Sub(tc.expectedRepay), fBalance, tc.msg, "balances") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins decreased by expected amount + s.requireEqualCoins(iBorrowed.Sub(tc.expectedRepay), fBorrowed, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + +func (s *IntegrationTestSuite) TestMsgLiquidate() { + type testCase struct { + msg string + liquidator sdk.AccAddress + borrower sdk.AccAddress + attemptedRepay sdk.Coin + rewardDenom string + expectedRepay sdk.Coin + expectedLiquidate sdk.Coin + expectedReward sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a liquidator which supplies plenty of UMEE and ATOM to the module + supplier := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + s.supply(supplier, coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + + // create and fund a liquidator which has 1000 UMEE and 1000 ATOM + liquidator := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + + // create a healthy borrower + healthyBorrower := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(healthyBorrower, coin(umeeDenom, 100_000000)) + s.collateralize(healthyBorrower, coin("u/"+umeeDenom, 100_000000)) + s.borrow(healthyBorrower, coin(umeeDenom, 10_000000)) + + // create a borrower which supplies and collateralizes 1000 ATOM + atomBorrower := s.newAccount(coin(atomDenom, 1000_000000)) + s.supply(atomBorrower, coin(atomDenom, 1000_000000)) + s.collateralize(atomBorrower, coin("u/"+atomDenom, 1000_000000)) + // artificially borrow 500 ATOM - this can be liquidated without bad debt + s.forceBorrow(atomBorrower, coin(atomDenom, 500_000000)) + + // create a borrower which collateralizes 110 UMEE + umeeBorrower := s.newAccount(coin(umeeDenom, 300_000000)) + s.supply(umeeBorrower, coin(umeeDenom, 200_000000)) + s.collateralize(umeeBorrower, coin("u/"+umeeDenom, 110_000000)) + // artificially borrow 200 UMEE - this will create a bad debt when liquidated + s.forceBorrow(umeeBorrower, coin(umeeDenom, 200_000000)) + + // creates a complex borrower with multiple denoms active + complexBorrower := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(complexBorrower, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.collateralize(complexBorrower, coin("u/"+umeeDenom, 100_000000), coin("u/"+atomDenom, 100_000000)) + // artificially borrow multiple denoms + s.forceBorrow(complexBorrower, coin(atomDenom, 30_000000), coin(umeeDenom, 30_000000)) + + // creates a realistic borrower with 400 UMEE collateral which will have a close factor < 1 + closeBorrower := s.newAccount(coin(umeeDenom, 400_000000)) + s.supply(closeBorrower, coin(umeeDenom, 400_000000)) + s.collateralize(closeBorrower, coin("u/"+umeeDenom, 400_000000)) + // artificially borrow just barely above liquidation threshold to simulate interest accruing + s.forceBorrow(closeBorrower, coin(umeeDenom, 102_000000)) + + tcs := []testCase{ + { + "healthy borrower", + liquidator, + healthyBorrower, + coin(atomDenom, 1_000000), + atomDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrLiquidationIneligible, + }, + { + "not borrowed denom", + liquidator, + umeeBorrower, + coin(atomDenom, 1_000000), + atomDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrLiquidationRepayZero, + }, + { + "direct atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 100_000000), + atomDenom, + coin(atomDenom, 100_000000), + coin("u/"+atomDenom, 109_000000), + coin(atomDenom, 109_000000), + nil, + }, + { + "u/atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 100_000000), + "u/" + atomDenom, + coin(atomDenom, 100_000000), + coin("u/"+atomDenom, 110_000000), + coin("u/"+atomDenom, 110_000000), + nil, + }, + { + "complete u/atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 500_000000), + "u/" + atomDenom, + coin(atomDenom, 300_000000), + coin("u/"+atomDenom, 330_000000), + coin("u/"+atomDenom, 330_000000), + nil, + }, + { + "bad debt u/umee liquidation", + liquidator, + umeeBorrower, + coin(umeeDenom, 200_000000), + "u/" + umeeDenom, + coin(umeeDenom, 100_000000), + coin("u/"+umeeDenom, 110_000000), + coin("u/"+umeeDenom, 110_000000), + nil, + }, + { + "complex borrower", + liquidator, + complexBorrower, + coin(umeeDenom, 200_000000), + "u/" + atomDenom, + coin(umeeDenom, 30_000000), + coin("u/"+atomDenom, 3_527932), + coin("u/"+atomDenom, 3_527932), + nil, + }, + { + "close factor < 1", + liquidator, + closeBorrower, + coin(umeeDenom, 200_000000), + "u/" + umeeDenom, + coin(umeeDenom, 7_752000), + coin("u/"+umeeDenom, 8_527200), + coin("u/"+umeeDenom, 8_527200), + nil, + }, + } + + for _, tc := range tcs { + msg := &types.MsgLiquidate{ + Liquidator: tc.liquidator.String(), + Borrower: tc.borrower.String(), + Repayment: tc.attemptedRepay, + RewardDenom: tc.rewardDenom, + } + if tc.err != nil { + _, err := srv.Liquidate(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + baseRewardDenom := types.ToTokenDenom(tc.expectedLiquidate.Denom) + + // initial state (borrowed denom) + biUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + biExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) + + // initial state (liquidated denom) + liUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + liExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) + + // borrower initial state + biBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) + biCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) + biBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) + + // liquidator initial state + liBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) + liCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) + liBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) + + // verify the output of liquidate function + resp, err := srv.Liquidate(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedRepay, resp.Repaid, tc.msg, "repaid") + require.Equal(tc.expectedLiquidate, resp.Collateral, tc.msg, "liquidated") + require.Equal(tc.expectedReward, resp.Reward, tc.msg, "reward") + + // final state (liquidated denom) + lfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + lfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) + + // borrower final state + bfBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) + bfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) + bfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) + + // liquidator final state + lfBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) + lfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) + lfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) + + // if borrowed denom and reward denom are different, then borrowed denom uTokens should be unaffected + if tc.rewardDenom != tc.attemptedRepay.Denom { + // final state (borrowed denom) + bfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + bfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) + + // verify borrowed denom uToken supply is unchanged + require.Equal(biUTokenSupply, bfUTokenSupply, tc.msg, "uToken supply (borrowed denom") + // verify borrowed denom uToken exchange rate is unchanged + require.Equal(biExchangeRate, bfExchangeRate, tc.msg, "uToken exchange rate (borrowed denom") + } + + // verify liquidated denom uToken supply is unchanged if indirect liquidation, or reduced if direct + expectedLiquidatedUTokenSupply := liUTokenSupply + if !types.HasUTokenPrefix(tc.rewardDenom) { + expectedLiquidatedUTokenSupply = expectedLiquidatedUTokenSupply.Sub(tc.expectedLiquidate) + } + require.Equal(expectedLiquidatedUTokenSupply, lfUTokenSupply, tc.msg, "uToken supply (liquidated denom") + // verify liquidated denom uToken exchange rate is unchanged + require.Equal(liExchangeRate, lfExchangeRate, tc.msg, "uToken exchange rate (liquidated denom") + + // verify borrower balances unchanged + require.Equal(biBalance, bfBalance, tc.msg, "borrower balances") + // verify borrower collateral reduced by the expected amount + s.requireEqualCoins(biCollateral.Sub(tc.expectedLiquidate), bfCollateral, tc.msg, "borrower collateral") + // verify borrowed coins decreased by expected amount + s.requireEqualCoins(biBorrowed.Sub(tc.expectedRepay), bfBorrowed, "borrowed coins") + + // verify liquidator balance changes by expected amounts + require.Equal(liBalance.Sub(tc.expectedRepay).Add(tc.expectedReward), lfBalance, + tc.msg, "liquidator balances") + // verify liquidator collateral unchanged + require.Equal(liCollateral, lfCollateral, tc.msg, "liquidator collateral") + // verify liquidator borrowed coins unchanged + s.requireEqualCoins(liBorrowed, lfBorrowed, "liquidator borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} From 58f22732bbb8ae223b3fa49e7c3564285e1e38c3 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 20 Dec 2022 21:28:36 -0700 Subject: [PATCH 15/36] fix case - new error precedence --- x/leverage/keeper/msg_server_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index f58ee8e345..0ed24fa7a2 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -787,7 +787,7 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() - // create and fund a supplier which supplies UMEE and ATOM + // create and fund a supplier which supplies 100 UMEE and 100 ATOM supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) @@ -848,7 +848,7 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { { "zero collateral", supplier, - coin(atomDenom, 100_000000), + coin(atomDenom, 1_000000), types.ErrUndercollaterized, }, } From 32cc4d850a380cad5334121da1357a60ae6776b7 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 20 Dec 2022 21:29:55 -0700 Subject: [PATCH 16/36] reorder --- x/leverage/keeper/msg_server_test.go | 218 +++++++++++++-------------- 1 file changed, 106 insertions(+), 112 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 0ed24fa7a2..7f00b66bfe 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -170,118 +170,6 @@ func (s *IntegrationTestSuite) TestUpdateRegistry() { } } -func (s *IntegrationTestSuite) TestMaxCollateralShare() { - app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() - - // update initial ATOM to have a limited MaxCollateralShare - atom, err := app.LeverageKeeper.GetTokenSettings(ctx, atomDenom) - require.NoError(err) - atom.MaxCollateralShare = sdk.MustNewDecFromStr("0.1") - s.registerToken(atom) - - // Mock oracle prices: - // UMEE $4.21 - // ATOM $39.38 - - // create a supplier to collateralize 100 UMEE, worth $421.00 - umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) - - // create an ATOM supplier - atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(atomSupplier, coin(atomDenom, 100_000000)) - - // collateralize 1.18 ATOM, worth $46.46, with no error. - // total collateral value (across all denoms) will be $467.46 - // so ATOM's collateral share ($46.46 / $467.46) is barely below 10% - s.collateralize(atomSupplier, coin("u/"+atomDenom, 1_180000)) - - // attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM - msg := &types.MsgCollateralize{ - Borrower: atomSupplier.String(), - Asset: coin("u/"+atomDenom, 10000), - } - _, err = srv.Collateralize(ctx, msg) - require.ErrorIs(err, types.ErrMaxCollateralShare) -} - -func (s *IntegrationTestSuite) TestMinCollateralLiquidity() { - app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() - - // update initial UMEE to have a limited MinCollateralLiquidity - umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) - require.NoError(err) - umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") - s.registerToken(umee) - - // create a supplier to collateralize 100 UMEE - umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) - s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) - - // create an ATOM supplier and borrow 49 UMEE - atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) - s.supply(atomSupplier, coin(atomDenom, 100_000000)) - s.collateralize(atomSupplier, coin("u/"+atomDenom, 100_000000)) - s.borrow(atomSupplier, coin(umeeDenom, 49_000000)) - - // collateral liquidity (liquidity / collateral) of UMEE is 51/100 - - // withdrawal would reduce collateral liquidity to 41/90 - msg1 := &types.MsgWithdraw{ - Supplier: umeeSupplier.String(), - Asset: coin("u/"+umeeDenom, 10_000000), - } - _, err = srv.Withdraw(ctx, msg1) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "withdraw") - - // borrow would reduce collateral liquidity to 41/100 - msg2 := &types.MsgBorrow{ - Borrower: umeeSupplier.String(), - Asset: coin(umeeDenom, 10_000000), - } - _, err = srv.Borrow(ctx, msg2) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "borrow") -} - -func (s *IntegrationTestSuite) TestMinCollateralLiquidity_Collateralize() { - app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() - - // update initial UMEE to have a limited MinCollateralLiquidity - umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) - require.NoError(err) - umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") - s.registerToken(umee) - - // create a supplier to supply 200 UMEE, and collateralize 100 UMEE - umeeSupplier := s.newAccount(coin(umeeDenom, 200)) - s.supply(umeeSupplier, coin(umeeDenom, 200)) - s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100)) - - // create an ATOM supplier and borrow 149 UMEE - atomSupplier := s.newAccount(coin(atomDenom, 100)) - s.supply(atomSupplier, coin(atomDenom, 100)) - s.collateralize(atomSupplier, coin("u/"+atomDenom, 100)) - s.borrow(atomSupplier, coin(umeeDenom, 149)) - - // collateral liquidity (liquidity / collateral) of UMEE is 51/100 - - // collateralize would reduce collateral liquidity to 51/200 - msg := &types.MsgCollateralize{ - Borrower: umeeSupplier.String(), - Asset: coin("u/"+umeeDenom, 100), - } - _, err = srv.Collateralize(ctx, msg) - require.ErrorIs(err, types.ErrMinCollateralLiquidity, "collateralize") -} - -// -// -// TODO: Composite message types - SupplyCollateral, MaxWithdraw, ... -// -// - func (s *IntegrationTestSuite) TestMsgSupply() { type testCase struct { msg string @@ -1259,3 +1147,109 @@ func (s *IntegrationTestSuite) TestMsgLiquidate() { } } } + +func (s *IntegrationTestSuite) TestMaxCollateralShare() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial ATOM to have a limited MaxCollateralShare + atom, err := app.LeverageKeeper.GetTokenSettings(ctx, atomDenom) + require.NoError(err) + atom.MaxCollateralShare = sdk.MustNewDecFromStr("0.1") + s.registerToken(atom) + + // Mock oracle prices: + // UMEE $4.21 + // ATOM $39.38 + + // create a supplier to collateralize 100 UMEE, worth $421.00 + umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) + + // create an ATOM supplier + atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(atomSupplier, coin(atomDenom, 100_000000)) + + // collateralize 1.18 ATOM, worth $46.46, with no error. + // total collateral value (across all denoms) will be $467.46 + // so ATOM's collateral share ($46.46 / $467.46) is barely below 10% + s.collateralize(atomSupplier, coin("u/"+atomDenom, 1_180000)) + + // attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM + msg := &types.MsgCollateralize{ + Borrower: atomSupplier.String(), + Asset: coin("u/"+atomDenom, 10000), + } + _, err = srv.Collateralize(ctx, msg) + require.ErrorIs(err, types.ErrMaxCollateralShare) +} + +func (s *IntegrationTestSuite) TestMinCollateralLiquidity() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial UMEE to have a limited MinCollateralLiquidity + umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) + require.NoError(err) + umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") + s.registerToken(umee) + + // create a supplier to collateralize 100 UMEE + umeeSupplier := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(umeeSupplier, coin(umeeDenom, 100_000000)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100_000000)) + + // create an ATOM supplier and borrow 49 UMEE + atomSupplier := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(atomSupplier, coin(atomDenom, 100_000000)) + s.collateralize(atomSupplier, coin("u/"+atomDenom, 100_000000)) + s.borrow(atomSupplier, coin(umeeDenom, 49_000000)) + + // collateral liquidity (liquidity / collateral) of UMEE is 51/100 + + // withdrawal would reduce collateral liquidity to 41/90 + msg1 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin("u/"+umeeDenom, 10_000000), + } + _, err = srv.Withdraw(ctx, msg1) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "withdraw") + + // borrow would reduce collateral liquidity to 41/100 + msg2 := &types.MsgBorrow{ + Borrower: umeeSupplier.String(), + Asset: coin(umeeDenom, 10_000000), + } + _, err = srv.Borrow(ctx, msg2) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "borrow") +} + +func (s *IntegrationTestSuite) TestMinCollateralLiquidity_Collateralize() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // update initial UMEE to have a limited MinCollateralLiquidity + umee, err := app.LeverageKeeper.GetTokenSettings(ctx, umeeDenom) + require.NoError(err) + umee.MinCollateralLiquidity = sdk.MustNewDecFromStr("0.5") + s.registerToken(umee) + + // create a supplier to supply 200 UMEE, and collateralize 100 UMEE + umeeSupplier := s.newAccount(coin(umeeDenom, 200)) + s.supply(umeeSupplier, coin(umeeDenom, 200)) + s.collateralize(umeeSupplier, coin("u/"+umeeDenom, 100)) + + // create an ATOM supplier and borrow 149 UMEE + atomSupplier := s.newAccount(coin(atomDenom, 100)) + s.supply(atomSupplier, coin(atomDenom, 100)) + s.collateralize(atomSupplier, coin("u/"+atomDenom, 100)) + s.borrow(atomSupplier, coin(umeeDenom, 149)) + + // collateral liquidity (liquidity / collateral) of UMEE is 51/100 + + // collateralize would reduce collateral liquidity to 51/200 + msg := &types.MsgCollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin("u/"+umeeDenom, 100), + } + _, err = srv.Collateralize(ctx, msg) + require.ErrorIs(err, types.ErrMinCollateralLiquidity, "collateralize") +} From b44be5947c79b6aa78eb569eacb9151a890ce21e Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 21 Dec 2022 07:06:46 -0700 Subject: [PATCH 17/36] test msg MaxWithdraw --- x/leverage/keeper/limits.go | 5 +- x/leverage/keeper/msg_server.go | 4 + x/leverage/keeper/msg_server_test.go | 258 +++++++++++++++++++++++++++ x/leverage/types/errors.go | 1 + 4 files changed, 267 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index f3a073dedb..b4e7b45f73 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -7,8 +7,11 @@ import ( ) // maxWithdraw calculates the maximum amount of uTokens an account can currently withdraw. -// input should be a base token. +// input denom should be a base token. func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { + if types.HasUTokenPrefix(denom) { + return sdk.Coin{}, types.ErrUToken + } uDenom := types.ToUTokenDenom(denom) availableTokens := sdk.NewCoin(denom, k.AvailableLiquidity(ctx, denom)) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 288d2879c0..0b28313ba1 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -105,6 +105,10 @@ func (s msgServer) MaxWithdraw( return nil, err } + if uToken.IsZero() { + return nil, types.ErrMaxWithdrawZero + } + received, err := s.keeper.Withdraw(ctx, supplierAddr, uToken) if err != nil { return nil, err diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 7f00b66bfe..f06e1810bd 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -454,6 +454,134 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { } } +func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { + type testCase struct { + msg string + addr sdk.AccAddress + denom string + expectedWithdraw sdk.Coin + expectFromCollateral sdk.Coin + expectedTokens sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM, then supply 100 UMEE and 50 ATOM + // also collateralize 75 of supplied UMEE + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 75_000000)) + s.supply(supplier, coin(atomDenom, 50_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.2 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) + + // create an additional UMEE supplier + other := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(other, coin(umeeDenom, 100_000000)) + + tcs := []testCase{ + { + "unregistered base token", + supplier, + "abcd", + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrMaxWithdrawZero, + }, + { + "uToken", + supplier, + "u/" + umeeDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrUToken, + }, + { + "max withdraw umee", + supplier, + umeeDenom, + coin("u/"+umeeDenom, 100_000000), + coin("u/"+umeeDenom, 75_000000), + coin(umeeDenom, 100_000000), + nil, + }, + { + "duplicate max withdraw umee", + supplier, + umeeDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrMaxWithdrawZero, + }, + { + "max withdraw atom", + supplier, + atomDenom, + coin("u/"+atomDenom, 50_000000), + coin("u/"+atomDenom, 0), + coin(atomDenom, 60_000000), + nil, + }, + } + + for _, tc := range tcs { + msg := &types.MsgMaxWithdraw{ + Supplier: tc.addr.String(), + Denom: tc.denom, + } + if tc.err != nil { + _, err := srv.MaxWithdraw(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + expectFromBalance := tc.expectedWithdraw.Sub(tc.expectFromCollateral) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of withdraw function + resp, err := srv.MaxWithdraw(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedWithdraw, resp.Withdrawn, tc.msg) + require.Equal(tc.expectedTokens, resp.Received, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance increased by the expected amount + require.Equal(iBalance.Add(tc.expectedTokens).Sub(expectFromBalance), + fBalance, tc.msg, "token balance") + // verify uToken collateral decreased by the expected amount + s.requireEqualCoins(iCollateral.Sub(tc.expectFromCollateral), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply decreased by the expected amount + require.Equal(iUTokenSupply.Sub(tc.expectedWithdraw), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + func (s *IntegrationTestSuite) TestMsgCollateralize() { type testCase struct { msg string @@ -665,6 +793,136 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { } } +func (s *IntegrationTestSuite) TestMsgSupplyCollateral() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + expectedUTokens sdk.Coin + err error + } + + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.5 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 60_000000)) + + // create a supplier that will exceed token's default MaxSupply + whale := s.newAccount(coin(umeeDenom, 1_000_000_000000)) + + tcs := []testCase{ + { + "unregistered denom", + supplier, + coin("abcd", 80_000000), + sdk.Coin{}, + types.ErrNotRegisteredToken, + }, + { + "uToken", + supplier, + coin("u/"+umeeDenom, 80_000000), + sdk.Coin{}, + types.ErrUToken, + }, + { + "no balance", + borrower, + coin(umeeDenom, 20_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "insufficient balance", + supplier, + coin(umeeDenom, 120_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "valid supply", + supplier, + coin(umeeDenom, 80_000000), + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional supply", + supplier, + coin(umeeDenom, 20_000000), + coin("u/"+umeeDenom, 20_000000), + nil, + }, + { + "high exchange rate", + supplier, + coin(atomDenom, 60_000000), + coin("u/"+atomDenom, 40_000000), + nil, + }, + { + "max supply", + whale, + coin(umeeDenom, 1_000_000_000000), + sdk.Coin{}, + types.ErrMaxSupply, + }, + } + + for _, tc := range tcs { + msg := &types.MsgSupplyCollateral{ + Supplier: tc.addr.String(), + Asset: tc.coin, + } + if tc.err != nil { + _, err := srv.SupplyCollateral(ctx, msg) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := tc.coin.Denom + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of supply collateral function + resp, err := srv.SupplyCollateral(ctx, msg) + require.NoError(err, tc.msg) + require.Equal(tc.expectedUTokens, resp.Collateralized, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance decreased and uToken balance unchanged + require.Equal(iBalance.Sub(tc.coin), fBalance, tc.msg, "token balance") + // verify uToken collateral increaaed + require.Equal(iCollateral.Add(tc.expectedUTokens), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply increased by the expected amount + require.Equal(iUTokenSupply.Add(tc.expectedUTokens), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } +} + func (s *IntegrationTestSuite) TestMsgBorrow() { type testCase struct { msg string diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index a229f0a226..7b9977ba3f 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -35,6 +35,7 @@ var ( ErrInvalidOraclePrice = sdkerrors.Register(ModuleName, 401, "invalid oracle price") ErrUndercollaterized = sdkerrors.Register(ModuleName, 402, "borrow positions are undercollaterized") ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 403, "borrower not eligible for liquidation") + ErrMaxWithdrawZero = sdkerrors.Register(ModuleName, 404, "max withdraw amount was zero") // 5XX = Market Conditions ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 500, "lending pool insufficient") From 3ba51e64ae9b02aa724201e40cf7a9e87f82fe84 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 21 Dec 2022 08:41:25 -0700 Subject: [PATCH 18/36] add additional test case --- x/leverage/keeper/msg_server_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index f06e1810bd..3898a7994b 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -481,9 +481,11 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { s.borrow(borrower, coin(atomDenom, 10_000000)) s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) - // create an additional UMEE supplier + // create an additional UMEE supplier with a small borrow other := s.newAccount(coin(umeeDenom, 100_000000)) s.supply(other, coin(umeeDenom, 100_000000)) + s.collateralize(other, coin("u/"+umeeDenom, 100_000000)) + s.borrow(other, coin(umeeDenom, 10_000000)) tcs := []testCase{ { @@ -523,12 +525,12 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { types.ErrMaxWithdrawZero, }, { - "max withdraw atom", - supplier, - atomDenom, - coin("u/"+atomDenom, 50_000000), - coin("u/"+atomDenom, 0), - coin(atomDenom, 60_000000), + "max withdraw with borrow", + other, + umeeDenom, + coin("u/"+umeeDenom, 60_000000), + coin("u/"+umeeDenom, 60_000000), + coin(umeeDenom, 60_000000), nil, }, } From 8001bcb2884fc8f83f85f27269eed324f9af95dc Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 21 Dec 2022 08:59:50 -0700 Subject: [PATCH 19/36] modify maxWithdraw and query to use minimum of historic and current max --- x/leverage/keeper/grpc_query.go | 11 ++++++++++- x/leverage/keeper/limits.go | 10 +++++----- x/leverage/keeper/msg_server.go | 11 ++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index a7b1b74176..0e7ca535fe 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -288,10 +288,19 @@ func (q Querier) MaxWithdraw( return nil, err } - uToken, err := q.Keeper.maxWithdraw(ctx, addr, req.Denom) + maxCurrentWithdraw, err := q.Keeper.maxWithdraw(ctx, addr, req.Denom, false) if err != nil { return nil, err } + maxHistoricWithdraw, err := q.Keeper.maxWithdraw(ctx, addr, req.Denom, true) + if err != nil { + return nil, err + } + + uToken := sdk.NewCoin( + maxCurrentWithdraw.Denom, + sdk.MinInt(maxCurrentWithdraw.Amount, maxHistoricWithdraw.Amount), + ) token, err := q.Keeper.ExchangeUToken(ctx, uToken) if err != nil { diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index b4e7b45f73..cadf5cbeec 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -7,8 +7,8 @@ import ( ) // maxWithdraw calculates the maximum amount of uTokens an account can currently withdraw. -// input denom should be a base token. -func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { +// input denom should be a base token. Uses either real or historic prices. +func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string, historic bool) (sdk.Coin, error) { if types.HasUTokenPrefix(denom) { return sdk.Coin{}, types.ErrUToken } @@ -25,7 +25,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) specificCollateral := sdk.NewCoin(uDenom, totalCollateral.AmountOf(uDenom)) // calculate borrowed value for the account - borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, false) + borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, historic) if err != nil { return sdk.Coin{}, err } @@ -38,7 +38,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) } // for nonzero borrows, calculations are based on unused borrow limit - borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral, false) + borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral, historic) if err != nil { return sdk.Coin{}, err } @@ -52,7 +52,7 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) unusedBorrowLimit := borrowLimit.Sub(borrowedValue) // calculate the contribution to borrow limit made by only the type of collateral being withdrawn - specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(specificCollateral), false) + specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(specificCollateral), historic) if err != nil { return sdk.Coin{}, err } diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 0b28313ba1..08ab54937c 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -100,10 +100,19 @@ func (s msgServer) MaxWithdraw( return nil, err } - uToken, err := s.keeper.maxWithdraw(ctx, supplierAddr, msg.Denom) + maxCurrentWithdraw, err := s.keeper.maxWithdraw(ctx, supplierAddr, msg.Denom, false) if err != nil { return nil, err } + maxHistoricWithdraw, err := s.keeper.maxWithdraw(ctx, supplierAddr, msg.Denom, true) + if err != nil { + return nil, err + } + + uToken := sdk.NewCoin( + maxCurrentWithdraw.Denom, + sdk.MinInt(maxCurrentWithdraw.Amount, maxHistoricWithdraw.Amount), + ) if uToken.IsZero() { return nil, types.ErrMaxWithdrawZero From 962546907eb4beb21208282b4a441e5894ca8f4d Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Wed, 21 Dec 2022 09:47:32 -0700 Subject: [PATCH 20/36] Update x/leverage/keeper/borrows.go --- x/leverage/keeper/borrows.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 5e1e7af3aa..7770747d4a 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -11,8 +11,8 @@ import ( // under either recent (historic median) or current prices. It returns an error if current // prices cannot be calculated, but will use current prices (without returning an error) // for any token whose historic prices cannot be calculated. -// This should be checked at the end of any transaction which is restricted by borrow limits, -// i.e. Borrow, Decollateralize, Withdraw. +// This should be checked in msg_server.go at the end of any transaction which is restricted +// by borrow limits, i.e. Borrow, Decollateralize, Withdraw. func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) From 046c8357f380be94e14d3015035093e5214ca484 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:15:53 -0700 Subject: [PATCH 21/36] Update x/leverage/keeper/oracle.go Co-authored-by: Robert Zaremba --- 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 f97d8135a1..5b41ca91f9 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -41,7 +41,7 @@ func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, erro return price, nil } -// TokenDefaultDenomPrice returns the USD value of a token's symbol denom, e.g. UMEE. Note, the input +// TokenDefaultDenomPrice returns the USD value of a token's symbol denom, e.g. `UMEE` (rather than `uumee`). Note, the input // denom must still be the base denomination, e.g. uumee. When error is nil, price is guaranteed // to be positive. Also returns the token's exponent to reduce redundant registry reads. // If the historic parameter is true, uses a median of recent prices instead of current price. From 1dc6d2116e688529ac14296a7579a51d626e399c Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:31:05 -0700 Subject: [PATCH 22/36] lint++ --- x/leverage/keeper/oracle.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 5b41ca91f9..c5497e4350 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -41,9 +41,9 @@ func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, erro return price, nil } -// TokenDefaultDenomPrice returns the USD value of a token's symbol denom, e.g. `UMEE` (rather than `uumee`). Note, the input -// denom must still be the base denomination, e.g. uumee. When error is nil, price is guaranteed -// to be positive. Also returns the token's exponent to reduce redundant registry reads. +// TokenDefaultDenomPrice returns the USD value of a token's symbol denom, e.g. `UMEE` (rather than `uumee`). +// Note, the input denom must still be the base denomination, e.g. uumee. When error is nil, price is +// guaranteed to be positive. Also returns the token's exponent to reduce redundant registry reads. // If the historic parameter is true, uses a median of recent prices instead of current price. func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string, historic bool) (sdk.Dec, uint32, error) { t, err := k.GetTokenSettings(ctx, baseDenom) From 034757a2a0d7ab602f6f256bea893ccf393583a4 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Dec 2022 11:57:43 -0700 Subject: [PATCH 23/36] historic withdraw test cases --- x/leverage/keeper/msg_server_test.go | 62 +++++++++++++++++++++++++++- x/leverage/keeper/suite_test.go | 48 ++++++++++++++------- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index a9ce2f4ddf..a6b1d984af 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -331,9 +331,31 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { s.borrow(borrower, coin(atomDenom, 10_000000)) s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) - // create an additional UMEE supplier - other := s.newAccount(coin(umeeDenom, 100_000000)) + // create an additional supplier (UMEE, DUMP, PUMP tokens) + other := s.newAccount(coin(umeeDenom, 100_000000), coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000)) s.supply(other, coin(umeeDenom, 100_000000)) + s.supply(other, coin(pumpDenom, 100_000000)) + s.supply(other, coin(dumpDenom, 100_000000)) + + // create a DUMP (historic price 1.00, current price 0.50) borrower + // using PUMP (historic price 1.00, current price 2.00) collateral + dumpborrower := s.newAccount(coin(pumpDenom, 100_000000)) + s.supply(dumpborrower, coin(pumpDenom, 100_000000)) + s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000)) + s.borrow(dumpborrower, coin(dumpDenom, 20_000000)) + // collateral value is $200 (current) or $100 (historic) + // borrowed value is $10 (current) or $20 (historic) + // collateral weights are always 0.25 in testing + + // create a PUMP (historic price 1.00, current price 2.00) borrower + // using DUMP (historic price 1.00, current price 0.50) collateral + pumpborrower := s.newAccount(coin(dumpDenom, 100_000000)) + s.supply(pumpborrower, coin(dumpDenom, 100_000000)) + s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000)) + s.borrow(pumpborrower, coin(pumpDenom, 5_000000)) + // collateral value is $50 (current) or $100 (historic) + // borrowed value is $10 (current) or $5 (historic) + // collateral weights are always 0.25 in testing tcs := []testCase{ { @@ -408,6 +430,42 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { sdk.Coin{}, types.ErrUndercollaterized, }, + { + "acceptable withdrawal (dump borrower)", + dumpborrower, + coin("u/"+pumpDenom, 20_000000), + nil, + sdk.NewCoins(coin("u/"+pumpDenom, 20_000000)), + coin(pumpDenom, 20_000000), + nil, + }, + { + "borrow limit (undercollateralized under historic prices but ok with current prices)", + dumpborrower, + coin("u/"+pumpDenom, 20_000000), + nil, + nil, + sdk.Coin{}, + types.ErrUndercollaterized, + }, + { + "acceptable withdrawal (pump borrower)", + pumpborrower, + coin("u/"+dumpDenom, 20_000000), + nil, + sdk.NewCoins(coin("u/"+dumpDenom, 20_000000)), + coin(dumpDenom, 20_000000), + nil, + }, + { + "borrow limit (undercollateralized under current prices but ok with historic prices)", + pumpborrower, + coin("u/"+dumpDenom, 20_000000), + nil, + nil, + sdk.Coin{}, + types.ErrUndercollaterized, + }, } for _, tc := range tcs { diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index bc02940f8e..f4facad5bd 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -148,50 +148,70 @@ func (s *IntegrationTestSuite) fundAccount(addr sdk.AccAddress, funds ...sdk.Coi // supply tokens from an account and require no errors. Use when setting up leverage scenarios. func (s *IntegrationTestSuite) supply(addr sdk.AccAddress, coins ...sdk.Coin) { - app, ctx, require := s.app, s.ctx, s.Require() + srv, ctx, require := s.msgSrvr, s.ctx, s.Require() for _, coin := range coins { - _, err := app.LeverageKeeper.Supply(ctx, addr, coin) + msg := &types.MsgSupply{ + Supplier: addr.String(), + Asset: coin, + } + _, err := srv.Supply(ctx, msg) require.NoError(err, "supply") } } // withdraw utokens from an account and require no errors. Use when setting up leverage scenarios. func (s *IntegrationTestSuite) withdraw(addr sdk.AccAddress, coins ...sdk.Coin) { - app, ctx, require := s.app, s.ctx, s.Require() + srv, ctx, require := s.msgSrvr, s.ctx, s.Require() for _, coin := range coins { - _, err := app.LeverageKeeper.Withdraw(ctx, addr, coin) + msg := &types.MsgWithdraw{ + Supplier: addr.String(), + Asset: coin, + } + _, err := srv.Withdraw(ctx, msg) require.NoError(err, "withdraw") } } // collateralize uTokens from an account and require no errors. Use when setting up leverage scenarios. func (s *IntegrationTestSuite) collateralize(addr sdk.AccAddress, uTokens ...sdk.Coin) { - app, ctx, require := s.app, s.ctx, s.Require() + srv, ctx, require := s.msgSrvr, s.ctx, s.Require() for _, coin := range uTokens { - err := app.LeverageKeeper.Collateralize(ctx, addr, coin) + msg := &types.MsgCollateralize{ + Borrower: addr.String(), + Asset: coin, + } + _, err := srv.Collateralize(ctx, msg) require.NoError(err, "collateralize") } } // decollateralize uTokens from an account and require no errors. Use when setting up leverage scenarios. func (s *IntegrationTestSuite) decollateralize(addr sdk.AccAddress, uTokens ...sdk.Coin) { - app, ctx, require := s.app, s.ctx, s.Require() + srv, ctx, require := s.msgSrvr, s.ctx, s.Require() for _, coin := range uTokens { - err := app.LeverageKeeper.Decollateralize(ctx, addr, coin) + msg := &types.MsgDecollateralize{ + Borrower: addr.String(), + Asset: coin, + } + _, err := srv.Decollateralize(ctx, msg) require.NoError(err, "decollateralize") } } // borrow tokens as an account and require no errors. Use when setting up leverage scenarios. func (s *IntegrationTestSuite) borrow(addr sdk.AccAddress, coins ...sdk.Coin) { - app, ctx, require := s.app, s.ctx, s.Require() + srv, ctx, require := s.msgSrvr, s.ctx, s.Require() for _, coin := range coins { - err := app.LeverageKeeper.Borrow(ctx, addr, coin) + msg := &types.MsgBorrow{ + Borrower: addr.String(), + Asset: coin, + } + _, err := srv.Borrow(ctx, msg) require.NoError(err, "borrow") } } @@ -202,13 +222,9 @@ func (s *IntegrationTestSuite) forceBorrow(addr sdk.AccAddress, coins ...sdk.Coi app, ctx, require := s.app, s.ctx, s.Require() for _, coin := range coins { - borrowed := s.tk.GetBorrow(ctx, addr, coin.Denom) - err := s.tk.SetBorrow(ctx, addr, borrowed.Add(coin)) - require.NoError(err, "forceBorrow") + err := app.LeverageKeeper.Borrow(ctx, addr, coin) + require.NoError(err, "borrow") } - - err := app.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins) - require.NoError(err, "forceBorroww") } // setReserves artificially sets reserves of one or more tokens to given values From f9ce97566a0b3a809d169b2c112162f653163758 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 27 Dec 2022 12:54:07 -0700 Subject: [PATCH 24/36] max withdraw scenarios - historic --- x/leverage/keeper/msg_server_test.go | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index a6b1d984af..4d8eb85378 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -549,6 +549,32 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { s.collateralize(other, coin("u/"+umeeDenom, 100_000000)) s.borrow(other, coin(umeeDenom, 10_000000)) + // create an additional supplier (UMEE, DUMP, PUMP tokens) + surplus := s.newAccount(coin(umeeDenom, 100_000000), coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(umeeDenom, 100_000000)) + s.supply(surplus, coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(dumpDenom, 100_000000)) + + // create a DUMP (historic price 1.00, current price 0.50) borrower + // using PUMP (historic price 1.00, current price 2.00) collateral + dumpborrower := s.newAccount(coin(pumpDenom, 100_000000)) + s.supply(dumpborrower, coin(pumpDenom, 100_000000)) + s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000)) + s.borrow(dumpborrower, coin(dumpDenom, 20_000000)) + // collateral value is $200 (current) or $100 (historic) + // borrowed value is $10 (current) or $20 (historic) + // collateral weights are always 0.25 in testing + + // create a PUMP (historic price 1.00, current price 2.00) borrower + // using DUMP (historic price 1.00, current price 0.50) collateral + pumpborrower := s.newAccount(coin(dumpDenom, 100_000000)) + s.supply(pumpborrower, coin(dumpDenom, 100_000000)) + s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000)) + s.borrow(pumpborrower, coin(pumpDenom, 5_000000)) + // collateral value is $50 (current) or $100 (historic) + // borrowed value is $10 (current) or $5 (historic) + // collateral weights are always 0.25 in testing + tcs := []testCase{ { "unregistered base token", @@ -595,6 +621,24 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { coin(umeeDenom, 60_000000), nil, }, + { + "max withdrawal (dump borrower)", + dumpborrower, + pumpDenom, + coin("u/"+pumpDenom, 20_000000), + coin("u/"+pumpDenom, 20_000000), + coin(pumpDenom, 20_000000), + nil, + }, + { + "max withdrawal (pump borrower)", + pumpborrower, + dumpDenom, + coin("u/"+dumpDenom, 20_000000), + coin("u/"+dumpDenom, 20_000000), + coin(dumpDenom, 20_000000), + nil, + }, } for _, tc := range tcs { From 726d74a84276e861bdf21b672337b43b4b0ba08c Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Dec 2022 05:52:28 -0700 Subject: [PATCH 25/36] historic decollateralize cases --- x/leverage/keeper/msg_server_test.go | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 4d8eb85378..634d572629 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -815,6 +815,32 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) s.borrow(borrower, coin(atomDenom, 10_000000)) + // create an additional supplier (UMEE, DUMP, PUMP tokens) + surplus := s.newAccount(coin(umeeDenom, 100_000000), coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(umeeDenom, 100_000000)) + s.supply(surplus, coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(dumpDenom, 100_000000)) + + // create a DUMP (historic price 1.00, current price 0.50) borrower + // using PUMP (historic price 1.00, current price 2.00) collateral + dumpborrower := s.newAccount(coin(pumpDenom, 100_000000)) + s.supply(dumpborrower, coin(pumpDenom, 100_000000)) + s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000)) + s.borrow(dumpborrower, coin(dumpDenom, 20_000000)) + // collateral value is $200 (current) or $100 (historic) + // borrowed value is $10 (current) or $20 (historic) + // collateral weights are always 0.25 in testing + + // create a PUMP (historic price 1.00, current price 2.00) borrower + // using DUMP (historic price 1.00, current price 0.50) collateral + pumpborrower := s.newAccount(coin(dumpDenom, 100_000000)) + s.supply(pumpborrower, coin(dumpDenom, 100_000000)) + s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000)) + s.borrow(pumpborrower, coin(pumpDenom, 5_000000)) + // collateral value is $50 (current) or $100 (historic) + // borrowed value is $10 (current) or $5 (historic) + // collateral weights are always 0.25 in testing + tcs := []testCase{ { "base token", @@ -852,6 +878,31 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { coin("u/"+atomDenom, 100_000000), types.ErrUndercollaterized, }, + + { + "acceptable decollateralize (dump borrower)", + dumpborrower, + coin("u/"+pumpDenom, 20_000000), + nil, + }, + { + "borrow limit (undercollateralized under historic prices but ok with current prices)", + dumpborrower, + coin("u/"+pumpDenom, 20_000000), + types.ErrUndercollaterized, + }, + { + "acceptable decollateralize (pump borrower)", + pumpborrower, + coin("u/"+dumpDenom, 20_000000), + nil, + }, + { + "borrow limit (undercollateralized under current prices but ok with historic prices)", + pumpborrower, + coin("u/"+dumpDenom, 20_000000), + types.ErrUndercollaterized, + }, } for _, tc := range tcs { From 4b97faaaaa2d5b87143a1a15f51c50500838ca77 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Dec 2022 06:01:28 -0700 Subject: [PATCH 26/36] historic borrow cases --- x/leverage/keeper/msg_server_test.go | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 634d572629..a9d6ee9168 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -1101,6 +1101,27 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { s.supply(borrower, coin(atomDenom, 100_000000)) s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + // create an additional supplier (DUMP, PUMP tokens) + surplus := s.newAccount(coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(pumpDenom, 100_000000)) + s.supply(surplus, coin(dumpDenom, 100_000000)) + + // this will be a DUMP (historic price 1.00, current price 0.50) borrower + // using PUMP (historic price 1.00, current price 2.00) collateral + dumpborrower := s.newAccount(coin(pumpDenom, 100_000000)) + s.supply(dumpborrower, coin(pumpDenom, 100_000000)) + s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000)) + // collateral value is $200 (current) or $100 (historic) + // collateral weights are always 0.25 in testing + + // this will be a PUMP (historic price 1.00, current price 2.00) borrower + // using DUMP (historic price 1.00, current price 0.50) collateral + pumpborrower := s.newAccount(coin(dumpDenom, 100_000000)) + s.supply(pumpborrower, coin(dumpDenom, 100_000000)) + s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000)) + // collateral value is $50 (current) or $100 (historic) + // collateral weights are always 0.25 in testing + tcs := []testCase{ { "uToken", @@ -1156,6 +1177,30 @@ func (s *IntegrationTestSuite) TestMsgBorrow() { coin(atomDenom, 1_000000), types.ErrUndercollaterized, }, + { + "dump borrower (acceptable)", + dumpborrower, + coin(dumpDenom, 20_000000), + nil, + }, + { + "dump borrower (borrow limit)", + dumpborrower, + coin(dumpDenom, 10_000000), + types.ErrUndercollaterized, + }, + { + "pump borrower (acceptable)", + pumpborrower, + coin(pumpDenom, 5_000000), + nil, + }, + { + "pump borrower (borrow limit)", + pumpborrower, + coin(pumpDenom, 2_000000), + types.ErrUndercollaterized, + }, } for _, tc := range tcs { From 877042853bd01fa0f47e7371093c909deb6b3da9 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Dec 2022 06:22:19 -0700 Subject: [PATCH 27/36] helper funtion --- x/leverage/keeper/borrows.go | 35 +++++++++++++++++---------------- x/leverage/keeper/msg_server.go | 8 ++++---- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 7770747d4a..68f87a8f34 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -7,44 +7,45 @@ import ( "github.com/umee-network/umee/v3/x/leverage/types" ) -// checkBorrowerHealth returns an error if a borrower is currently above their borrow limit, +// 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 current // prices cannot be calculated, but will use current prices (without returning an error) // for any token whose historic prices cannot be calculated. // This should be checked in msg_server.go at the end of any transaction which is restricted -// by borrow limits, i.e. Borrow, Decollateralize, Withdraw. -func (k Keeper) checkBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { +// by borrow limits, i.e. Borrow, Decollateralize, Withdraw, MaxWithdraw. +func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) // Check using current prices - currentValue, err := k.TotalTokenValue(ctx, borrowed, false) + err := k.checkPositionHealth(ctx, borrowed, collateral, false) if err != nil { return err } - currentLimit, err := k.CalculateBorrowLimit(ctx, collateral, false) - if err != nil { - return err - } - if currentValue.GT(currentLimit) { - return types.ErrUndercollaterized.Wrapf( - "borrowed: %s, limit: %s (current prices)", currentValue, currentLimit) - } // Check using historic prices - historicValue, err := k.TotalTokenValue(ctx, borrowed, true) + return k.checkPositionHealth(ctx, borrowed, collateral, true) +} + +// checkPositionHealth returns an error if a borrow + collateral position is not healthy. uses either +// current or historic prices. +func (k Keeper) checkPositionHealth(ctx sdk.Context, borrowed, collateral sdk.Coins, historic bool) error { + value, err := k.TotalTokenValue(ctx, borrowed, historic) if err != nil { return err } - historicLimit, err := k.CalculateBorrowLimit(ctx, collateral, true) + limit, err := k.CalculateBorrowLimit(ctx, collateral, historic) if err != nil { return err } - if historicValue.GT(historicLimit) { + if value.GT(limit) { + desc := "current" + if historic { + desc = "historic" + } return types.ErrUndercollaterized.Wrapf( - "borrowed: %s, limit: %s (historic prices)", historicValue, historicLimit) + "borrowed: %s, limit: %s (%s prices)", value, limit, desc) } - return nil } diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index ba5d16f01b..197cb9fcae 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -72,7 +72,7 @@ func (s msgServer) Withdraw( } // Fail here if supplier ends up over their borrow limit under current or historic prices - err = s.keeper.checkBorrowerHealth(ctx, supplierAddr) + err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) if err != nil { return nil, err } @@ -123,7 +123,7 @@ func (s msgServer) MaxWithdraw( } // Fail here if supplier ends up over their borrow limit under current or historic prices - err = s.keeper.checkBorrowerHealth(ctx, supplierAddr) + err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) if err != nil { return nil, err } @@ -263,7 +263,7 @@ func (s msgServer) Decollateralize( } // Fail here if borrower ends up over their borrow limit under current or historic prices - err = s.keeper.checkBorrowerHealth(ctx, borrowerAddr) + err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr) if err != nil { return nil, err } @@ -295,7 +295,7 @@ func (s msgServer) Borrow( } // Fail here if borrower ends up over their borrow limit under current or historic prices - err = s.keeper.checkBorrowerHealth(ctx, borrowerAddr) + err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr) if err != nil { return nil, err } From 285885a0460cc85c18c9ad93b6b0f3ca291bd458 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 28 Dec 2022 09:51:48 -0700 Subject: [PATCH 28/36] fail-safe on no historic prices - breaks tests --- app/test_helpers.go | 6 ++++++ x/leverage/keeper/oracle.go | 17 +++++++++++------ x/leverage/simulation/operations_test.go | 1 + x/leverage/types/errors.go | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/test_helpers.go b/app/test_helpers.go index 0a458eadb6..36c6950026 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -270,6 +270,12 @@ func IntegrationTestNetworkConfig() network.Config { oracleGenState.ExchangeRates = append(oracleGenState.ExchangeRates, oracletypes.NewExchangeRateTuple( params.DisplayDenom, sdk.MustNewDecFromStr("34.21"), )) + // Set mock historic prices + // + // TODO (to fix leverage tests now that historic is fail-safe) set 24 medians + // + // historicPrices := oracletypes.New... + // oracleGenState.HistoricPrices = append(oracleGenState.HistoricPrices, historicPrices) bz, err = cdc.MarshalJSON(&oracleGenState) if err != nil { diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index c5497e4350..fa4295982a 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -12,7 +12,9 @@ import ( var ten = sdk.MustNewDecFromStr("10") // TODO: Parameterize and move this -const numHistoracleStamps = uint64(10) +const ( + minHistoracleStamps = uint64(24) +) // TokenBasePrice returns the USD value of a base token. Note, the token's denomination // must be the base denomination, e.g. uumee. The x/oracle module must know of @@ -57,13 +59,16 @@ func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string, histor var price sdk.Dec - if historic { + if historic && minHistoracleStamps > 0 { // historic price var numStamps uint32 - price, numStamps, err = k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, numHistoracleStamps) - if err == nil && numStamps == 0 { - // if no price medians were available, current price is used as the historic price - price, err = k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + price, numStamps, err = k.oracleKeeper.MedianOfHistoricMedians(ctx, t.SymbolDenom, minHistoracleStamps) + if err == nil && numStamps < uint32(minHistoracleStamps) { + return sdk.ZeroDec(), t.Exponent, types.ErrNoHistoricMedians.Wrapf( + "requested %d, got %d", + minHistoracleStamps, + numStamps, + ) } } else { // current price diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index 5b0d67e051..99f8d9dfde 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -39,6 +39,7 @@ func (s *SimTestSuite) SetupTest() { // Use default umee token for sim tests s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE", 6))) app.OracleKeeper.SetExchangeRate(ctx, "UMEE", sdk.MustNewDecFromStr("100.0")) + // TODO: set historic prices (24 medians) to fix tests s.app = app s.ctx = ctx diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 7b9977ba3f..9593fcbb73 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -36,6 +36,7 @@ var ( ErrUndercollaterized = sdkerrors.Register(ModuleName, 402, "borrow positions are undercollaterized") ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 403, "borrower not eligible for liquidation") ErrMaxWithdrawZero = sdkerrors.Register(ModuleName, 404, "max withdraw amount was zero") + ErrNoHistoricMedians = sdkerrors.Register(ModuleName, 405, "insufficient historic medians available") // 5XX = Market Conditions ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 500, "lending pool insufficient") From f151eb612a083796e1b273ad823ba859ec89e1ea Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:03:27 -0700 Subject: [PATCH 29/36] Update x/leverage/keeper/msg_server_test.go Co-authored-by: Robert Zaremba --- x/leverage/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index a9d6ee9168..79c788735d 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -886,7 +886,7 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { nil, }, { - "borrow limit (undercollateralized under historic prices but ok with current prices)", + "above borrow limit (undercollateralized under historic prices but ok with current prices)", dumpborrower, coin("u/"+pumpDenom, 20_000000), types.ErrUndercollaterized, From 7385533c7030d37a0174d7b5321f877b7d9465aa Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:03:49 -0700 Subject: [PATCH 30/36] Update x/leverage/keeper/msg_server_test.go Co-authored-by: Robert Zaremba --- x/leverage/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 79c788735d..73f0d15230 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -368,7 +368,7 @@ func (s *IntegrationTestSuite) TestMsgWithdraw() { types.ErrNotUToken, }, { - "base token", + "only uToken can be withdrawn", supplier, coin(umeeDenom, 80_000000), nil, From 93cfa64b404620347834e5dd8377e1ec96cda3c9 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:04:21 -0700 Subject: [PATCH 31/36] Update x/leverage/keeper/msg_server_test.go Co-authored-by: Robert Zaremba --- x/leverage/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 73f0d15230..422871fc13 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -586,7 +586,7 @@ func (s *IntegrationTestSuite) TestMsgMaxWithdraw() { types.ErrMaxWithdrawZero, }, { - "uToken", + "can't borrow uToken", supplier, "u/" + umeeDenom, sdk.Coin{}, From 642144536936d03b111eb45234d00044219223bb Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:04:34 -0700 Subject: [PATCH 32/36] Update x/leverage/keeper/msg_server_test.go Co-authored-by: Robert Zaremba --- x/leverage/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 422871fc13..0fc52b0497 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -873,7 +873,7 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { types.ErrInsufficientCollateral, }, { - "borrow limit", + "above borrow limit", borrower, coin("u/"+atomDenom, 100_000000), types.ErrUndercollaterized, From 74dacb8a4ed3f8e30adfba26eab22b97f58aaa5b Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 12:04:47 -0700 Subject: [PATCH 33/36] Update x/leverage/keeper/msg_server_test.go Co-authored-by: Robert Zaremba --- x/leverage/keeper/msg_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 0fc52b0497..961073e621 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -898,7 +898,7 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() { nil, }, { - "borrow limit (undercollateralized under current prices but ok with historic prices)", + "above borrow limit (undercollateralized under current prices but ok with historic prices)", pumpborrower, coin("u/"+dumpDenom, 20_000000), types.ErrUndercollaterized, From 7eea870ea8b517126dac82264689ea8e7c1d9f5b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:46:39 -0700 Subject: [PATCH 34/36] fixed leverage simulations mock oracle --- x/leverage/simulation/operations_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index 99f8d9dfde..5f67eddaa2 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -39,7 +39,11 @@ func (s *SimTestSuite) SetupTest() { // Use default umee token for sim tests s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE", 6))) app.OracleKeeper.SetExchangeRate(ctx, "UMEE", sdk.MustNewDecFromStr("100.0")) - // TODO: set historic prices (24 medians) to fix tests + for i := 1; i <= 24; i++ { + // set historic medians for UMEE on blocks 1-24 (without actually advancing block height) + // this is to accomodate leverage module's default 24 historic median requirement + app.OracleKeeper.SetHistoricMedian(ctx, "UMEE", uint64(i), sdk.MustNewDecFromStr("100.0")) + } s.app = app s.ctx = ctx From f07af397cead49f1e6f4c3e2344871611ff23e90 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:53:56 -0700 Subject: [PATCH 35/36] fix leverage CLI test mock oracle --- app/test_helpers.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/test_helpers.go b/app/test_helpers.go index 36c6950026..7ecde72800 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -270,12 +270,17 @@ func IntegrationTestNetworkConfig() network.Config { oracleGenState.ExchangeRates = append(oracleGenState.ExchangeRates, oracletypes.NewExchangeRateTuple( params.DisplayDenom, sdk.MustNewDecFromStr("34.21"), )) - // Set mock historic prices - // - // TODO (to fix leverage tests now that historic is fail-safe) set 24 medians - // - // historicPrices := oracletypes.New... - // oracleGenState.HistoricPrices = append(oracleGenState.HistoricPrices, historicPrices) + // Set mock historic medians to satisfy leverage module's 24 median requirement + for i := 1; i <= 24; i++ { + median := oracletypes.Price{ + ExchangeRateTuple: oracletypes.NewExchangeRateTuple( + params.DisplayDenom, + sdk.MustNewDecFromStr("34.21"), + ), + BlockNum: uint64(i), + } + oracleGenState.Medians = append(oracleGenState.Medians, median) + } bz, err = cdc.MarshalJSON(&oracleGenState) if err != nil { From 7d867cfa2e387ae8d5d4117f58fc213cb33e16bd Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:57:43 -0700 Subject: [PATCH 36/36] cl++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8461a63f90..2fc49069f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1630](https://github.com/umee-network/umee/pull/1630) Incentive module proto. - [1588](https://github.com/umee-network/umee/pull/1588) Historacle proto. - [1653](https://github.com/umee-network/umee/pull/1653) Incentive Msg Server interface implementation. +- [1654](https://github.com/umee-network/umee/pull/1654) Leverage historacle integration. ## [v3.3.0](https://github.com/umee-network/umee/releases/tag/v3.3.0) - 2022-12-20