From eb259a65ac3aa32188f5e49edee969cda3db9486 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 10:57:50 -0700 Subject: [PATCH 01/14] fix: improve price precision for high exponent assets --- CHANGELOG.md | 4 ++ x/leverage/client/tests/tests.go | 4 +- x/leverage/fixtures/token.go | 6 +- x/leverage/keeper/grpc_query.go | 2 +- x/leverage/keeper/grpc_query_test.go | 4 +- x/leverage/keeper/iter_test.go | 10 ++-- x/leverage/keeper/limits.go | 2 +- x/leverage/keeper/liquidate.go | 4 +- x/leverage/keeper/msg_server_test.go | 12 ++-- x/leverage/keeper/oracle.go | 70 ++++++++++++++++++++---- x/leverage/keeper/oracle_test.go | 56 ++++++++++++++----- x/leverage/keeper/suite_test.go | 9 +-- x/leverage/keeper/token_test.go | 2 +- x/leverage/simulation/operations_test.go | 2 +- 14 files changed, 134 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb73ec3e7c..68456284bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Fixes + +- [XXXX](https://github.com/umee-network/umee/pull/XXXX) Increases price calculation precision for high exponent assets. + ## [v3.2.0](https://github.com/umee-network/umee/releases/tag/v3.2.0) - 2022-11-25 Since `umeed v3.2` there is a new runtime dependency: `libwasmvm.x86_64.so v1.1.1` is required. diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index 61f552e001..66f5887f0c 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -49,7 +49,7 @@ func (s *IntegrationTestSuite) TestInvalidQueries() { func (s *IntegrationTestSuite) TestLeverageScenario() { val := s.network.Validators[0] - oraclePrice := sdk.MustNewDecFromStr("0.00003421") + oracleSymbolPrice := sdk.MustNewDecFromStr("34.21") initialQueries := []testQuery{ { @@ -105,7 +105,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryMarketSummaryResponse{ SymbolDenom: "UMEE", Exponent: 6, - OraclePrice: &oraclePrice, + OraclePrice: &oracleSymbolPrice, UTokenExchangeRate: sdk.OneDec(), // Borrow rate * (1 - ReserveFactor - OracleRewardFactor) // 1.50 * (1 - 0.10 - 0.01) = 0.89 * 1.5 = 1.335 diff --git a/x/leverage/fixtures/token.go b/x/leverage/fixtures/token.go index 236c38e878..d7edf9e50c 100644 --- a/x/leverage/fixtures/token.go +++ b/x/leverage/fixtures/token.go @@ -9,14 +9,16 @@ import ( const ( // AtomDenom is an ibc denom to be used as ATOM's BaseDenom during testing. Matches mainnet. AtomDenom = "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9" + // DaiDenom is an ibc denom to be used as DAI's BaseDenom during testing. Matches mainnet. + DaiDenom = "ibc/C86651B4D30C1739BF8B061E36F4473A0C9D60380B52D01E56A6874037A5D060" ) // Token returns a valid token -func Token(base, symbol string) types.Token { +func Token(base, symbol string, exponent uint32) types.Token { return types.Token{ BaseDenom: base, SymbolDenom: symbol, - Exponent: 6, + Exponent: exponent, ReserveFactor: sdk.MustNewDecFromStr("0.2"), CollateralWeight: sdk.MustNewDecFromStr("0.25"), LiquidationThreshold: sdk.MustNewDecFromStr("0.25"), diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index e14a03d2e2..1f468f36c7 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.TokenPrice(ctx, req.Denom); oracleErr == nil { + if oraclePrice, _, oracleErr := q.Keeper.TokenSymbolPrice(ctx, req.Denom); oracleErr == nil { resp.OraclePrice = &oraclePrice } diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 592be1eae1..afef2a3ee8 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -36,12 +36,12 @@ func (s *IntegrationTestSuite) TestQuerier_MarketSummary() { resp, err := s.queryClient.MarketSummary(context.Background(), req) require.NoError(err) - oraclePrice := sdk.MustNewDecFromStr("0.00000421") + oracleSymbolPrice := sdk.MustNewDecFromStr("4.21") expected := types.QueryMarketSummaryResponse{ SymbolDenom: "UMEE", Exponent: 6, - OraclePrice: &oraclePrice, + OraclePrice: &oracleSymbolPrice, UTokenExchangeRate: sdk.OneDec(), Supply_APY: sdk.MustNewDecFromStr("1.2008"), Borrow_APY: sdk.MustNewDecFromStr("1.52"), diff --git a/x/leverage/keeper/iter_test.go b/x/leverage/keeper/iter_test.go index 7503e6b973..d309cd9083 100644 --- a/x/leverage/keeper/iter_test.go +++ b/x/leverage/keeper/iter_test.go @@ -20,7 +20,7 @@ func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrOneAsset require.Equal([]sdk.AccAddress{}, zeroAddresses) // Note: Setting umee liquidation threshold to 0.05 to make the user eligible to liquidation - umeeToken := newToken("uumee", "UMEE") + umeeToken := newToken("uumee", "UMEE", 6) umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") @@ -58,14 +58,14 @@ func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrTwoAsset s.borrow(addr, coin(atomDenom, 4_000000)) // Note: Setting umee liquidation threshold to 0.05 to make the user eligible for liquidation - umeeToken := newToken("uumee", "UMEE") + umeeToken := newToken("uumee", "UMEE", 6) umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) // Note: Setting atom collateral weight to 0.01 to make the user eligible for liquidation - atomIBCToken := newToken(atomDenom, "ATOM") + atomIBCToken := newToken(atomDenom, "ATOM", 6) atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") @@ -100,14 +100,14 @@ func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_TwoAddr() { s.borrow(addr2, coin(atomDenom, 24)) // Note: Setting umee liquidation threshold to 0.05 to make the first supplier eligible for liquidation - umeeToken := newToken("uumee", "UMEE") + umeeToken := newToken("uumee", "UMEE", 6) umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) // Note: Setting atom collateral weight to 0.01 to make the second supplier eligible for liquidation - atomIBCToken := newToken(atomDenom, "ATOM") + atomIBCToken := newToken(atomDenom, "ATOM", 6) atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index bef5577902..986fb91462 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -40,7 +40,7 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. // determine the amount of uTokens which would be required to reach maxValue tokenDenom := types.ToTokenDenom(denom) - tokenPrice, err := k.TokenPrice(ctx, tokenDenom) + tokenPrice, err := k.TokenBasePrice(ctx, tokenDenom) if err != nil { return sdk.ZeroInt(), err } diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index b850823338..fe3ee79743 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -62,11 +62,11 @@ func (k Keeper) getLiquidationAmounts( ) // get oracle prices for the reward and repay denoms - repayTokenPrice, err := k.TokenPrice(ctx, repayDenom) + repayTokenPrice, err := k.TokenBasePrice(ctx, repayDenom) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - rewardTokenPrice, err := k.TokenPrice(ctx, rewardDenom) + rewardTokenPrice, err := k.TokenBasePrice(ctx, rewardDenom) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 94a91e3dbb..98bc096215 100644 --- a/x/leverage/keeper/msg_server_test.go +++ b/x/leverage/keeper/msg_server_test.go @@ -10,8 +10,8 @@ import ( func (s *IntegrationTestSuite) TestAddTokensToRegistry() { govAccAddr := s.app.GovKeeper.GetGovernanceAccount(s.ctx).GetAddress().String() - registeredUmee := fixtures.Token("uumee", "UMEE") - newTokens := fixtures.Token("uabcd", "ABCD") + registeredUmee := fixtures.Token("uumee", "UMEE", 6) + newTokens := fixtures.Token("uabcd", "ABCD", 6) testCases := []struct { name string @@ -26,7 +26,7 @@ func (s *IntegrationTestSuite) TestAddTokensToRegistry() { Title: "test", Description: "test", AddTokens: []types.Token{ - fixtures.Token("uosmo", ""), // empty denom is invalid + fixtures.Token("uosmo", "", 6), // empty denom is invalid }, }, true, @@ -95,7 +95,7 @@ func (s *IntegrationTestSuite) TestAddTokensToRegistry() { func (s *IntegrationTestSuite) TestUpdateRegistry() { govAccAddr := s.app.GovKeeper.GetGovernanceAccount(s.ctx).GetAddress().String() - modifiedUmee := fixtures.Token("uumee", "UMEE") + modifiedUmee := fixtures.Token("uumee", "UMEE", 6) modifiedUmee.ReserveFactor = sdk.MustNewDecFromStr("0.69") testCases := []struct { @@ -111,7 +111,7 @@ func (s *IntegrationTestSuite) TestUpdateRegistry() { Title: "test", Description: "test", UpdateTokens: []types.Token{ - fixtures.Token("uosmo", ""), // empty denom is invalid + fixtures.Token("uosmo", "", 6), // empty denom is invalid }, }, true, @@ -124,7 +124,7 @@ func (s *IntegrationTestSuite) TestUpdateRegistry() { Title: "test", Description: "test", UpdateTokens: []types.Token{ - fixtures.Token("uosmo", ""), // empty denom is invalid + fixtures.Token("uosmo", "", 6), // empty denom is invalid }, }, true, diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 68b4868382..5a91d2e939 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -9,13 +9,15 @@ import ( "github.com/umee-network/umee/v3/x/leverage/types" ) -// TokenPrice returns the USD value of a base token. Note, the token's denomination +var ten = sdk.MustNewDecFromStr("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 // know about the UMEE/USD exchange rate along with the uumee base denomination // and the exponent. When error is nil, price is guaranteed to be positive. -func (k Keeper) TokenPrice(ctx sdk.Context, denom string) (sdk.Dec, error) { - t, err := k.GetTokenSettings(ctx, denom) +func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, error) { + t, err := k.GetTokenSettings(ctx, baseDenom) if err != nil { return sdk.ZeroDec(), err } @@ -24,26 +26,65 @@ func (k Keeper) TokenPrice(ctx sdk.Context, denom string) (sdk.Dec, error) { return sdk.ZeroDec(), types.ErrBlacklisted } - price, err := k.oracleKeeper.GetExchangeRateBase(ctx, denom) + price, err := k.oracleKeeper.GetExchangeRateBase(ctx, baseDenom) if err != nil { return sdk.ZeroDec(), sdkerrors.Wrap(err, "oracle") } if price.IsNil() || !price.IsPositive() { - return sdk.ZeroDec(), sdkerrors.Wrap(types.ErrInvalidOraclePrice, denom) + return sdk.ZeroDec(), sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) } return price, nil } +// TokenSymbolPrice returns the USD value of a token. Note, the token's denomination +// must be the symbol denomination, e.g. UMEE. When error is nil, price is guaranteed +// to be positive. Also returns the token's exponent to reduce redundant registry reads. +func (k Keeper) TokenSymbolPrice(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(), uint32(t.Exponent), types.ErrBlacklisted + } + + price, err := k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + if err != nil { + return sdk.ZeroDec(), uint32(t.Exponent), sdkerrors.Wrap(err, "oracle") + } + + if price.IsNil() || !price.IsPositive() { + return sdk.ZeroDec(), uint32(t.Exponent), sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) + } + + return price, uint32(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 { + return input + } + if n < 0 { + quotient := ten.Power(uint64(n * -1)) + return input.Quo(quotient) + } + return input.Mul(ten.Power(uint64(n))) +} + // TokenValue returns the total token value given a Coin. An error is // returned if we cannot get the token's price or if it's not an accepted token. +// Computation uses price of token's symbol denom to avoid rounding errors +// for exponent >= 18 tokens. func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { - p, err := k.TokenPrice(ctx, coin.Denom) + p, exp, err := k.TokenSymbolPrice(ctx, coin.Denom) if err != nil { return sdk.ZeroDec(), err } - return p.Mul(toDec(coin.Amount)), nil + return exponent(p.Mul(toDec(coin.Amount)), int32(exp)*-1), nil } // TotalTokenValue returns the total value of all supplied tokens. It is @@ -66,19 +107,26 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, erro return total, nil } -// PriceRatio computed the ratio of the USD prices of two tokens, as sdk.Dec(fromPrice/toPrice). +// 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 symbol 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, err := k.TokenPrice(ctx, fromDenom) + p1, e1, err := k.TokenSymbolPrice(ctx, fromDenom) if err != nil { return sdk.ZeroDec(), err } - p2, err := k.TokenPrice(ctx, toDenom) + p2, e2, err := k.TokenSymbolPrice(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 p1.Quo(p2), nil + return exponent(p1, powerDifference).Quo(p2), nil + // TODO: add test with differing exponents } // FundOracle transfers requested coins to the oracle module account, as diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 2cdb682c21..2e163469aa 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -10,12 +10,14 @@ import ( ) type mockOracleKeeper struct { - exchangeRates map[string]sdk.Dec + baseExchangeRates map[string]sdk.Dec + symbolExchangeRates map[string]sdk.Dec } func newMockOracleKeeper() *mockOracleKeeper { m := &mockOracleKeeper{ - exchangeRates: make(map[string]sdk.Dec), + baseExchangeRates: make(map[string]sdk.Dec), + symbolExchangeRates: make(map[string]sdk.Dec), } m.Reset() @@ -23,7 +25,7 @@ func newMockOracleKeeper() *mockOracleKeeper { } func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec, error) { - p, ok := m.exchangeRates[denom] + p, ok := m.symbolExchangeRates[denom] if !ok { return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) } @@ -32,36 +34,60 @@ func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec } func (m *mockOracleKeeper) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error) { - p, err := m.GetExchangeRate(ctx, denom) - if err != nil { - return sdk.ZeroDec(), err + p, ok := m.baseExchangeRates[denom] + if !ok { + return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) } - // assume 10^6 for the base denom - return p.Quo(sdk.MustNewDecFromStr("1000000.00")), nil + return p, nil } func (m *mockOracleKeeper) Reset() { - m.exchangeRates = map[string]sdk.Dec{ - appparams.BondDenom: sdk.MustNewDecFromStr("4.21"), - atomDenom: sdk.MustNewDecFromStr("39.38"), + m.symbolExchangeRates = map[string]sdk.Dec{ + "UMEE": sdk.MustNewDecFromStr("4.21"), + "ATOM": sdk.MustNewDecFromStr("39.38"), + "DAI": sdk.MustNewDecFromStr("1.00"), + } + m.baseExchangeRates = map[string]sdk.Dec{ + appparams.BondDenom: sdk.MustNewDecFromStr("0.00000421"), + atomDenom: sdk.MustNewDecFromStr("0.00003938"), + daiDenom: sdk.MustNewDecFromStr("0.00003938"), } } -func (s *IntegrationTestSuite) TestOracle_TokenPrice() { +func (s *IntegrationTestSuite) TestOracle_TokenBasePrice() { app, ctx, require := s.app, s.ctx, s.Require() - p, err := app.LeverageKeeper.TokenPrice(ctx, appparams.BondDenom) + p, err := app.LeverageKeeper.TokenBasePrice(ctx, appparams.BondDenom) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("0.00000421"), p) - p, err = app.LeverageKeeper.TokenPrice(ctx, atomDenom) + p, err = app.LeverageKeeper.TokenBasePrice(ctx, atomDenom) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("0.00003938"), p) - p, err = app.LeverageKeeper.TokenPrice(ctx, "foo") + p, err = app.LeverageKeeper.TokenBasePrice(ctx, "foo") + require.ErrorIs(err, types.ErrNotRegisteredToken) + require.Equal(sdk.ZeroDec(), p) +} + +func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { + app, ctx, require := s.app, s.ctx, s.Require() + + p, e, err := app.LeverageKeeper.TokenSymbolPrice(ctx, appparams.BondDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("4.21"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.TokenSymbolPrice(ctx, atomDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("39.38"), p) + require.Equal(uint32(6), e) + + p, e, err = app.LeverageKeeper.TokenSymbolPrice(ctx, "foo") require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) + require.Equal(uint32(0), e) } func (s *IntegrationTestSuite) TestOracle_TokenValue() { diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index 845f6fe35d..d9774b9e21 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -26,6 +26,7 @@ import ( const ( umeeDenom = appparams.BondDenom atomDenom = fixtures.AtomDenom + daiDenom = fixtures.DaiDenom ) type IntegrationTestSuite struct { @@ -71,8 +72,8 @@ func (s *IntegrationTestSuite) SetupTest() { // override DefaultGenesis token registry with fixtures.Token leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) - require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(appparams.BondDenom, "UMEE"))) - require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(atomDenom, "ATOM"))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(appparams.BondDenom, "UMEE", 6))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(atomDenom, "ATOM", 6))) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) @@ -98,8 +99,8 @@ func (s *IntegrationTestSuite) requireEqualCoins(coinsA, coinsB sdk.Coins, msgAn } // newToken creates a test token with reasonable initial parameters -func newToken(base, symbol string) types.Token { - return fixtures.Token(base, symbol) +func newToken(base, symbol string, exponent uint32) types.Token { + return fixtures.Token(base, symbol, exponent) } // coin creates a coin with a given base denom and amount diff --git a/x/leverage/keeper/token_test.go b/x/leverage/keeper/token_test.go index e92e130641..09384af74c 100644 --- a/x/leverage/keeper/token_test.go +++ b/x/leverage/keeper/token_test.go @@ -7,7 +7,7 @@ import ( func (s *IntegrationTestSuite) TestGetToken() { app, ctx, require := s.app, s.ctx, s.Require() - uabc := newToken("uabc", "ABC") + uabc := newToken("uabc", "ABC", 6) require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, uabc)) t, err := app.LeverageKeeper.GetTokenSettings(ctx, "uabc") diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index e9fa26c798..125f55b92d 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -37,7 +37,7 @@ func (s *SimTestSuite) SetupTest() { leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) // Use default umee token for sim tests - s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE"))) + s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE", 6))) app.OracleKeeper.SetExchangeRate(ctx, "UMEE", sdk.MustNewDecFromStr("100.0")) s.app = app From 39ae38f24dd445c52db3af28b720f5baf40cef94 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 10:59:03 -0700 Subject: [PATCH 02/14] cl++ --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68456284bb..124559efd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +- [XXXX](https://github.com/umee-network/umee/pull/XXXX) MarketSummary query now displays symbol price instead of base price for readability. + ### Fixes - [XXXX](https://github.com/umee-network/umee/pull/XXXX) Increases price calculation precision for high exponent assets. From 05d577bcf9ce6a8272ccfd80d61778fa2a39321d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:00:55 -0700 Subject: [PATCH 03/14] cl++ --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 124559efd3..d9688d5341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,11 +48,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features -- [XXXX](https://github.com/umee-network/umee/pull/XXXX) MarketSummary query now displays symbol price instead of base price for readability. +- [1633](https://github.com/umee-network/umee/pull/1633) MarketSummary query now displays symbol price instead of base price for readability. ### Fixes -- [XXXX](https://github.com/umee-network/umee/pull/XXXX) Increases price calculation precision for high exponent assets. +- [1633](https://github.com/umee-network/umee/pull/1633) Increases price calculation precision for high exponent assets. ## [v3.2.0](https://github.com/umee-network/umee/releases/tag/v3.2.0) - 2022-11-25 From 0805def10054aeb58fe7e1a9bc2daa111b8fc505 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:04:02 -0700 Subject: [PATCH 04/14] Update x/leverage/keeper/oracle.go --- x/leverage/keeper/oracle.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 5a91d2e939..56f74a634b 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -38,8 +38,8 @@ func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, erro return price, nil } -// TokenSymbolPrice returns the USD value of a token. Note, the token's denomination -// must be the symbol denomination, e.g. UMEE. When error is nil, price is guaranteed +// TokenSymbolPrice returns the USD value of a token. 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) TokenSymbolPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { t, err := k.GetTokenSettings(ctx, baseDenom) From 2db6cf9cfcb37397a68e64ed49854fdd5c4f2844 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:07:33 -0700 Subject: [PATCH 05/14] ++ --- x/leverage/keeper/oracle_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 2e163469aa..2450c076af 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -51,7 +51,7 @@ func (m *mockOracleKeeper) Reset() { m.baseExchangeRates = map[string]sdk.Dec{ appparams.BondDenom: sdk.MustNewDecFromStr("0.00000421"), atomDenom: sdk.MustNewDecFromStr("0.00003938"), - daiDenom: sdk.MustNewDecFromStr("0.00003938"), + daiDenom: sdk.MustNewDecFromStr("0.000000000000000001"), } } From 95d6b1e6679dead9b99ea64a4c258eb6f3e52f19 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:17:52 -0700 Subject: [PATCH 06/14] add DAI tests --- x/leverage/keeper/grpc_query_test.go | 2 +- x/leverage/keeper/msg_server_test.go | 4 ++-- x/leverage/keeper/oracle.go | 1 - x/leverage/keeper/oracle_test.go | 12 +++++++++++- x/leverage/keeper/suite_test.go | 1 + 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index afef2a3ee8..055c02f073 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, 2, "token registry length") + require.Len(resp.Registry, 3, "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 98bc096215..629ca924b4 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, 3) + s.Require().Len(tokens, 4) 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, 2) + s.Require().Len(tokens, 3) token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uumee") s.Require().NoError(err) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 56f74a634b..76ce4c2541 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -126,7 +126,6 @@ func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, powerDifference := int32(e2) - int32(e1) // Price ratio > 1 if fromDenom is worth more than toDenom. return exponent(p1, powerDifference).Quo(p2), nil - // TODO: add test with differing exponents } // FundOracle transfers requested coins to the oracle module account, as diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 2450c076af..6d61ad07ec 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -135,9 +135,19 @@ func (s *IntegrationTestSuite) TestOracle_PriceRatio() { r, err := app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, atomDenom) require.NoError(err) - // $4.21 / $39.38 + // $4.21 / $39.38 at same exponent require.Equal(sdk.MustNewDecFromStr("0.106907059421025901"), r) + r, err = app.LeverageKeeper.PriceRatio(ctx, appparams.BondDenom, daiDenom) + 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) + 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) require.ErrorIs(err, types.ErrNotRegisteredToken) diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go index d9774b9e21..ef473e0e8b 100644 --- a/x/leverage/keeper/suite_test.go +++ b/x/leverage/keeper/suite_test.go @@ -74,6 +74,7 @@ func (s *IntegrationTestSuite) SetupTest() { leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) 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))) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) From eb4d825ebc6d09c33d76b99773fe696931321e7f Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:24:34 -0700 Subject: [PATCH 07/14] analysis++ --- x/leverage/keeper/limits.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 986fb91462..af4f00b871 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -45,6 +45,10 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. return sdk.ZeroInt(), err } uTokenExchangeRate := k.DeriveExchangeRate(ctx, tokenDenom) + + // in the case of a small token price smaller than the smallest sdk.Dec (10^-18), + // this maxCollateralAmount will use the price of 10^-18 and thus derive a lower + // (more cautious) limit than a precise price would produce maxCollateralAmount := maxValue.Quo(tokenPrice).Quo(uTokenExchangeRate).TruncateInt() // return the computed maximum or the current uToken supply, whichever is smaller From f593b59f84c90e0a61d0cfaef249008ba6228ca2 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:26:34 -0700 Subject: [PATCH 08/14] Update x/leverage/keeper/limits.go --- x/leverage/keeper/limits.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index af4f00b871..6f8672d46a 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -46,7 +46,7 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. } uTokenExchangeRate := k.DeriveExchangeRate(ctx, tokenDenom) - // in the case of a small token price smaller than the smallest sdk.Dec (10^-18), + // in the case of a base token price smaller than the smallest sdk.Dec (10^-18), // this maxCollateralAmount will use the price of 10^-18 and thus derive a lower // (more cautious) limit than a precise price would produce maxCollateralAmount := maxValue.Quo(tokenPrice).Quo(uTokenExchangeRate).TruncateInt() From faf21e93d7de70c24d233f156123f1a4be73e75a Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 12:25:11 -0700 Subject: [PATCH 09/14] TODOs in liquidate - all else is finished --- x/leverage/keeper/liquidate.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index fe3ee79743..9fb6994167 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -130,6 +130,13 @@ func ComputeLiquidation( // Start with the maximum possible repayment amount, as a decimal maxRepay := toDec(availableRepay) // Determine the base maxReward amount that would result from maximum repayment + + // + // + // TODO 1: uses repay / reward (base), so can be improved + // + // + maxReward := maxRepay.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) // Determine the maxCollateral burn amount that corresponds to base reward amount maxCollateral := maxReward.Quo(uTokenExchangeRate) @@ -145,7 +152,14 @@ func ComputeLiquidation( // We will track limiting factors by the ratio by which the max repayment would need to be reduced to comply ratio := sdk.OneDec() - // Repaid value cannot exceed borrowed value times close factor + // Repaid value cannot exceed borrowed value times close factor. + + // + // + // TODO 2: This needs adjustment against rounding + // + // + ratio = sdk.MinDec(ratio, borrowedValue.Mul(closeFactor).Quo(maxRepay.Mul(repayTokenPrice)), ) From 68eb9d23236c5a2f03901237770dc5f704f9f63b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 12:39:53 -0700 Subject: [PATCH 10/14] liquidation rounding improvement 1 --- x/leverage/keeper/liquidate.go | 17 ++++++++++------- x/leverage/keeper/liquidate_test.go | 6 ++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 9fb6994167..5455678748 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -71,6 +71,12 @@ func (k Keeper) getLiquidationAmounts( return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } + // get more precise (less rounding at high exponent) price ratio + priceRatio, err := k.PriceRatio(ctx, repayDenom, rewardDenom) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + // get collateral uToken exchange rate exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) @@ -89,6 +95,7 @@ func (k Keeper) getLiquidationAmounts( k.AvailableLiquidity(ctx, rewardDenom), repayTokenPrice, rewardTokenPrice, + priceRatio, exchangeRate, liqudationIncentive, closeFactor, @@ -107,6 +114,7 @@ func (k Keeper) getLiquidationAmounts( // - availableReward: The amount of unreserved reward tokens in the module balance // - repayTokenPrice: The oracle price of the base repayment denom // - rewardTokenPrice: The oracle price of the base reward denom +// - priceRatio: The less rounded ratio of repay / reward, which is used when computing rewards // - uTokenExchangeRate: The uToken exchange rate from collateral uToken denom to reward base denom // - liquidationIncentive: The liquidation incentive of the token reward denomination // - closeFactor: The dynamic close factor computed from the borrower's borrowed value and liquidation threshold @@ -117,6 +125,7 @@ func ComputeLiquidation( availableReward sdkmath.Int, repayTokenPrice, rewardTokenPrice, + priceRatio, uTokenExchangeRate, liquidationIncentive, closeFactor, @@ -131,13 +140,7 @@ func ComputeLiquidation( maxRepay := toDec(availableRepay) // Determine the base maxReward amount that would result from maximum repayment - // - // - // TODO 1: uses repay / reward (base), so can be improved - // - // - - maxReward := maxRepay.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) + maxReward := maxRepay.Mul(priceRatio).Mul(sdk.OneDec().Add(liquidationIncentive)) // Determine the maxCollateral burn amount that corresponds to base reward amount maxCollateral := maxReward.Quo(uTokenExchangeRate) diff --git a/x/leverage/keeper/liquidate_test.go b/x/leverage/keeper/liquidate_test.go index 50dded6ddb..139493f3e8 100644 --- a/x/leverage/keeper/liquidate_test.go +++ b/x/leverage/keeper/liquidate_test.go @@ -40,12 +40,14 @@ func TestComputeLiquidation(t *testing.T) { } runTestCase := func(tc testCase, expectedRepay, expectedCollateral, expectedReward int64, msg string) { + priceRatio := tc.repayTokenPrice.Quo(tc.rewardTokenPrice) repay, collateral, reward := keeper.ComputeLiquidation( tc.availableRepay, tc.availableCollateral, tc.availableReward, tc.repayTokenPrice, tc.rewardTokenPrice, + priceRatio, tc.uTokenExchangeRate, tc.liquidationIncentive, tc.closeFactor, @@ -175,7 +177,7 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustUp.repayTokenPrice = sdk.MustNewDecFromStr("2") expensiveCollateralDustUp.rewardTokenPrice = sdk.MustNewDecFromStr("40.1") expensiveCollateralDustUp.liquidationIncentive = sdk.MustNewDecFromStr("0") - runTestCase(expensiveCollateralDustUp, 21, 1, 1, "expensive collateral dust with price up") + runTestCase(expensiveCollateralDustUp, 21, 0, 0, "expensive collateral dust with price up") // collateral dust case, with high collateral token value rounds required repayment up expensiveCollateralDustDown := baseCase() @@ -183,7 +185,7 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustDown.repayTokenPrice = sdk.MustNewDecFromStr("2") expensiveCollateralDustDown.rewardTokenPrice = sdk.MustNewDecFromStr("39.9") expensiveCollateralDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") - runTestCase(expensiveCollateralDustDown, 20, 1, 1, "expensive collateral dust with price down") + runTestCase(expensiveCollateralDustDown, 20, 0, 0, "expensive collateral dust with price down") // collateral dust case, with low collateral token value rounds required repayment up cheapCollateralDust := baseCase() From 8af5a0c1e5429ccede87feb5858a4dd82dc80c7d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 1 Dec 2022 12:46:31 -0700 Subject: [PATCH 11/14] lint++ --- x/leverage/keeper/oracle.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 76ce4c2541..31060fa713 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -48,19 +48,19 @@ func (k Keeper) TokenSymbolPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, ui } if t.Blacklist { - return sdk.ZeroDec(), uint32(t.Exponent), types.ErrBlacklisted + return sdk.ZeroDec(), t.Exponent, types.ErrBlacklisted } price, err := k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) if err != nil { - return sdk.ZeroDec(), uint32(t.Exponent), sdkerrors.Wrap(err, "oracle") + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(err, "oracle") } if price.IsNil() || !price.IsPositive() { - return sdk.ZeroDec(), uint32(t.Exponent), sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) } - return price, uint32(t.Exponent), nil + return price, t.Exponent, nil } // exponent multiplies an sdk.Dec by 10^n. n can be negative. From 2d619705a8f28bc6199fd21ac721717eae2a990a Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Fri, 2 Dec 2022 10:14:01 -0700 Subject: [PATCH 12/14] move close factor computation one function higher --- x/leverage/keeper/keeper.go | 6 +-- x/leverage/keeper/liquidate.go | 71 ++++++++++++----------------- x/leverage/keeper/liquidate_test.go | 40 +++++----------- 3 files changed, 44 insertions(+), 73 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 686025c9b9..65bc965902 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -372,9 +372,9 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, uT // Because partial liquidation is possible and exchange rates vary, Liquidate returns the actual amount of // tokens repaid, collateral liquidated, and base tokens or uTokens rewarded. func (k Keeper) Liquidate( - ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, maxRepay sdk.Coin, rewardDenom string, + ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, requestedRepay sdk.Coin, rewardDenom string, ) (repaid sdk.Coin, liquidated sdk.Coin, reward sdk.Coin, err error) { - if err := k.validateAcceptedAsset(ctx, maxRepay); err != nil { + if err := k.validateAcceptedAsset(ctx, requestedRepay); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } @@ -393,7 +393,7 @@ func (k Keeper) Liquidate( ctx, liquidatorAddr, borrowerAddr, - maxRepay, + requestedRepay, rewardDenom, directLiquidation, ) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 5455678748..65bc99613a 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -14,17 +14,18 @@ func (k Keeper) getLiquidationAmounts( ctx sdk.Context, liquidatorAddr, targetAddr sdk.AccAddress, - maxRepay sdk.Coin, + requestedRepay sdk.Coin, rewardDenom string, directLiquidation bool, ) (tokenRepay sdk.Coin, collateralLiquidate sdk.Coin, tokenReward sdk.Coin, err error) { - repayDenom := maxRepay.Denom + repayDenom := requestedRepay.Denom collateralDenom := types.ToUTokenDenom(rewardDenom) // get relevant liquidator, borrower, and module balances borrowerCollateral := k.GetBorrowerCollateral(ctx, targetAddr) totalBorrowed := k.GetBorrowerBorrows(ctx, targetAddr) availableRepay := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(repayDenom) + repayDenomBorrowed := sdk.NewCoin(repayDenom, totalBorrowed.AmountOf(repayDenom)) // calculate borrower health in USD values borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed) @@ -43,6 +44,10 @@ 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) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } // get liquidation incentive ts, err := k.GetTokenSettings(ctx, rewardDenom) @@ -60,18 +65,16 @@ func (k Keeper) getLiquidationAmounts( params.MinimumCloseFactor, params.CompleteLiquidationThreshold, ) - - // get oracle prices for the reward and repay denoms - repayTokenPrice, err := k.TokenBasePrice(ctx, repayDenom) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - rewardTokenPrice, err := k.TokenBasePrice(ctx, rewardDenom) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + // maximum USD value that can be repaid + maxRepayValue := borrowedValue.Mul(closeFactor) + // determine fraction of borrowed repayDenom which can be repaid after close factor + maxRepayAfterCloseFactor := totalBorrowed.AmountOf(repayDenom) + if maxRepayValue.LT(repayDenomBorrowedValue) { + maxRepayRatio := maxRepayValue.Quo(repayDenomBorrowedValue) + maxRepayAfterCloseFactor = maxRepayRatio.MulInt(totalBorrowed.AmountOf(repayDenom)).RoundInt() } - // get more precise (less rounding at high exponent) price ratio + // get precise (less rounding at high exponent) price ratio priceRatio, err := k.PriceRatio(ctx, repayDenom, rewardDenom) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err @@ -88,18 +91,24 @@ func (k Keeper) getLiquidationAmounts( liqudationIncentive = liqudationIncentive.Mul(sdk.OneDec().Sub(params.DirectLiquidationFee)) } + // max repayment amount is limited by a number of factors + xxx := requestedRepay.Amount // maximum allowed by liquidator + xxx = sdk.MinInt(xxx, availableRepay) // liquidator account balance + xxx = sdk.MinInt(xxx, totalBorrowed.AmountOf(repayDenom)) // borrower position + xxx = sdk.MinInt(xxx, maxRepayAfterCloseFactor) // close factor + // compute final liquidation amounts repay, burn, reward := ComputeLiquidation( - sdk.MinInt(sdk.MinInt(availableRepay, maxRepay.Amount), totalBorrowed.AmountOf(repayDenom)), + xxx, borrowerCollateral.AmountOf(collateralDenom), k.AvailableLiquidity(ctx, rewardDenom), - repayTokenPrice, - rewardTokenPrice, + // repayTokenPrice, + // rewardTokenPrice, priceRatio, exchangeRate, liqudationIncentive, - closeFactor, - borrowedValue, + // closeFactor, + // borrowedValue, ) return sdk.NewCoin(repayDenom, repay), sdk.NewCoin(collateralDenom, burn), sdk.NewCoin(rewardDenom, reward), nil @@ -112,27 +121,19 @@ func (k Keeper) getLiquidationAmounts( // - availableRepay: The lowest (in repay denom) of either liquidator balance, max repayment, or borrowed amount. // - availableCollateral: The amount of the reward uToken denom which borrower has as collateral // - availableReward: The amount of unreserved reward tokens in the module balance -// - repayTokenPrice: The oracle price of the base repayment denom -// - rewardTokenPrice: The oracle price of the base reward denom -// - priceRatio: The less rounded ratio of repay / reward, which is used when computing rewards +// - priceRatio: The ratio of repayPrice / rewardPrice, which is used when computing rewards // - uTokenExchangeRate: The uToken exchange rate from collateral uToken denom to reward base denom // - liquidationIncentive: The liquidation incentive of the token reward denomination -// - closeFactor: The dynamic close factor computed from the borrower's borrowed value and liquidation threshold -// - borrowedValue: The borrower's borrowed value in USD func ComputeLiquidation( availableRepay, availableCollateral, availableReward sdkmath.Int, - repayTokenPrice, - rewardTokenPrice, priceRatio, uTokenExchangeRate, - liquidationIncentive, - closeFactor, - borrowedValue sdk.Dec, + liquidationIncentive sdk.Dec, ) (tokenRepay sdkmath.Int, collateralBurn sdkmath.Int, tokenReward sdkmath.Int) { // Prevent division by zero - if uTokenExchangeRate.IsZero() || rewardTokenPrice.IsZero() || repayTokenPrice.IsZero() { + if uTokenExchangeRate.IsZero() || priceRatio.IsZero() { return sdkmath.ZeroInt(), sdkmath.ZeroInt(), sdkmath.ZeroInt() } @@ -147,25 +148,13 @@ func ComputeLiquidation( // Catch no-ops early if maxRepay.IsZero() || maxReward.IsZero() || - maxCollateral.IsZero() || - closeFactor.IsZero() || - borrowedValue.IsZero() { + maxCollateral.IsZero() { return sdk.ZeroInt(), sdk.ZeroInt(), sdk.ZeroInt() } // We will track limiting factors by the ratio by which the max repayment would need to be reduced to comply ratio := sdk.OneDec() - // Repaid value cannot exceed borrowed value times close factor. - // - // - // TODO 2: This needs adjustment against rounding - // - // - - ratio = sdk.MinDec(ratio, - borrowedValue.Mul(closeFactor).Quo(maxRepay.Mul(repayTokenPrice)), - ) // Collateral burned cannot exceed borrower's collateral ratio = sdk.MinDec(ratio, toDec(availableCollateral).Quo(maxCollateral), diff --git a/x/leverage/keeper/liquidate_test.go b/x/leverage/keeper/liquidate_test.go index 139493f3e8..cf80d904bd 100644 --- a/x/leverage/keeper/liquidate_test.go +++ b/x/leverage/keeper/liquidate_test.go @@ -21,21 +21,17 @@ func TestComputeLiquidation(t *testing.T) { rewardTokenPrice sdk.Dec uTokenExchangeRate sdk.Dec liquidationIncentive sdk.Dec - closeFactor sdk.Dec - borrowedValue sdk.Dec } baseCase := func() testCase { return testCase{ - sdkmath.NewInt(1000), // 1000 Token A to repay - sdkmath.NewInt(5000), // 5000 uToken B collateral - sdkmath.NewInt(5000), // 5000 Token B liquidity - sdk.OneDec(), // price(A) = $1 - sdk.OneDec(), // price(B) = $1 - sdk.OneDec(), // utoken exchange rate 1 u/B => 1 B - sdk.MustNewDecFromStr("0.1"), // reward value is 110% repay value - sdk.OneDec(), // unlimited close factor - sdk.MustNewDecFromStr("10000"), // $10000 borrowed value + sdkmath.NewInt(1000), // 1000 Token A to repay + sdkmath.NewInt(5000), // 5000 uToken B collateral + sdkmath.NewInt(5000), // 5000 Token B liquidity + sdk.OneDec(), // price(A) = $1 + sdk.OneDec(), // price(B) = $1 + sdk.OneDec(), // utoken exchange rate 1 u/B => 1 B + sdk.MustNewDecFromStr("0.1"), // reward value is 110% repay value } } @@ -45,28 +41,24 @@ func TestComputeLiquidation(t *testing.T) { tc.availableRepay, tc.availableCollateral, tc.availableReward, - tc.repayTokenPrice, - tc.rewardTokenPrice, priceRatio, tc.uTokenExchangeRate, tc.liquidationIncentive, - tc.closeFactor, - tc.borrowedValue, ) require.True(sdkmath.NewInt(expectedRepay).Equal(repay), - msg+" (repay); got: %d, expected: %s", expectedRepay, repay) + msg+" (repay); expected: %d, got: %s", expectedRepay, repay) require.True(sdkmath.NewInt(expectedCollateral).Equal(collateral), - msg+" (collateral); got: %d, expected: %s", expectedCollateral, collateral) + msg+" (collateral); expected: %d, got: %s", expectedCollateral, collateral) require.True(sdkmath.NewInt(expectedReward).Equal(reward), msg+" (reward); got: %d, expected: %s", expectedReward, reward) } // basic liquidation of 1000 borrowed tokens with plenty of available rewards and collateral runTestCase(baseCase(), 1000, 1100, 1100, "base case") - // borrower is healthy (as implied by a close factor of zero) so liquidation cannot occur + // borrower is healthy (zero max repay would result from close factor of zero) so liquidation cannot occur healthyCase := baseCase() - healthyCase.closeFactor = sdk.ZeroDec() + healthyCase.availableRepay = sdk.ZeroInt() runTestCase(healthyCase, 0, 0, 0, "healthy borrower") // limiting factor is available repay @@ -109,16 +101,6 @@ func TestComputeLiquidation(t *testing.T) { noIncentive.liquidationIncentive = sdk.ZeroDec() runTestCase(noIncentive, 1000, 1000, 1000, "no liquidation incentive") - // partial close factor - partialClose := baseCase() - partialClose.closeFactor = sdk.MustNewDecFromStr("0.03") - runTestCase(partialClose, 300, 330, 330, "close factor") - - // lowered borrowed value - lowValue := baseCase() - lowValue.borrowedValue = sdk.MustNewDecFromStr("700") - runTestCase(lowValue, 700, 770, 770, "lowered borrowed value") - // complex case, limited by available repay, with various nontrivial values complexCase := baseCase() complexCase.availableRepay = sdkmath.NewInt(300) From 760fdb33da5f97f579df32af596c253674e5472d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:01:17 -0700 Subject: [PATCH 13/14] SymbolPrice -> DefaultDenomPrice --- proto/umee/leverage/v1/query.proto | 2 +- swagger/swagger.yaml | 189 +++++++++++++++++++++++++++-- x/leverage/keeper/grpc_query.go | 2 +- x/leverage/keeper/oracle.go | 16 +-- x/leverage/keeper/oracle_test.go | 6 +- x/leverage/types/query.pb.go | 2 +- 6 files changed, 194 insertions(+), 23 deletions(-) diff --git a/proto/umee/leverage/v1/query.proto b/proto/umee/leverage/v1/query.proto index 2cd6f8ee23..cc52b2feda 100644 --- a/proto/umee/leverage/v1/query.proto +++ b/proto/umee/leverage/v1/query.proto @@ -86,7 +86,7 @@ message QueryMarketSummaryResponse { string symbol_denom = 1; // Exponent is the power of ten required to get from base denom to symbol denom. For example, an exponent of 6 means 10^6 uumee = 1 UMEE. uint32 exponent = 2; - // Oracle Price is the current USD value of a base token. Exponent must be applied to reach the price from symbol_denom. For example, a price of $0.000001 for 1 uumee is equivalent to $1.00 for 1 UMEE. Oracle price is nil when the oracle is down. + // Oracle Price is the current USD value of a token. Oracle price is nil when the oracle is down. string oracle_price = 3 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = true diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 7343d65f80..82fc4300a4 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -311,10 +311,8 @@ paths: oracle_price: type: string description: >- - Oracle Price is the current USD value of a base token. - Exponent must be applied to reach the price from symbol_denom. - For example, a price of $0.000001 for 1 uumee is equivalent to - $1.00 for 1 UMEE. Oracle price is nil when the oracle is down. + Oracle Price is the current USD value of a token. Oracle price + is nil when the oracle is down. uToken_exchange_rate: type: string description: >- @@ -862,9 +860,77 @@ paths: format: byte tags: - Query + /umee/historacle/v1/denoms/median_deviations: + get: + summary: |- + MedianDeviations returns median deviations of all denoms, + or, if specified, returns a single median deviation + operationId: MedianDeviations + responses: + '200': + description: A successful response. + schema: + type: object + properties: + medianDeviations: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + DecCoin defines a token with a denomination and a decimal + amount. + + + NOTE: The amount field is an Dec which implements the custom + method + + signatures required by gogoproto. + description: >- + medians defines a list of the median deviations for all + stamped denoms. + description: |- + QueryMedianDeviationsResponse is response type for the + Query/MedianDeviations RPC method. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + parameters: + - name: denom + description: denom defines the denomination to query for. + in: query + required: false + type: string + tags: + - Query /umee/historacle/v1/denoms/medians: get: - summary: Medians returns medians of all denoms + summary: |- + Medians returns medians of all denoms, + or, if specified, returns a single median operationId: Medians responses: '200': @@ -1098,6 +1164,38 @@ paths: we want to keep a record of our set of exchange rates. + median_period: + type: string + format: uint64 + description: >- + Median Period represents the amount blocks we will wait + between + + calculating the median and standard deviation of the + median of + + historic prices in the last Prune Period. + historic_accept_list: + type: array + items: + type: object + properties: + base_denom: + type: string + symbol_denom: + type: string + exponent: + type: integer + format: int64 + title: Denom - the object to hold configurations of each denom + description: >- + Historic Asset List is a list of assets which will use the + historic + + price stamping protection methodology (mainly + manipulatable assets). + + Any assets not on this list will not be stamped. description: >- QueryParamsResponse is the response type for the Query/Params RPC method. @@ -1797,10 +1895,8 @@ definitions: oracle_price: type: string description: >- - Oracle Price is the current USD value of a base token. Exponent must - be applied to reach the price from symbol_denom. For example, a price - of $0.000001 for 1 uumee is equivalent to $1.00 for 1 UMEE. Oracle - price is nil when the oracle is down. + Oracle Price is the current USD value of a token. Oracle price is nil + when the oracle is down. uToken_exchange_rate: type: string description: >- @@ -2515,6 +2611,30 @@ definitions: description: |- Prune Period represents the maximum amount of blocks which we want to keep a record of our set of exchange rates. + median_period: + type: string + format: uint64 + description: |- + Median Period represents the amount blocks we will wait between + calculating the median and standard deviation of the median of + historic prices in the last Prune Period. + historic_accept_list: + type: array + items: + type: object + properties: + base_denom: + type: string + symbol_denom: + type: string + exponent: + type: integer + format: int64 + title: Denom - the object to hold configurations of each denom + description: |- + Historic Asset List is a list of assets which will use the historic + price stamping protection methodology (mainly manipulatable assets). + Any assets not on this list will not be stamped. description: Params defines the parameters for the oracle module. umee.oracle.v1.QueryActiveExchangeRatesResponse: type: object @@ -2688,6 +2808,29 @@ definitions: description: |- QueryFeederDelegationResponse is response type for the Query/FeederDelegation RPC method. + umee.oracle.v1.QueryMedianDeviationsResponse: + type: object + properties: + medianDeviations: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + DecCoin defines a token with a denomination and a decimal amount. + + NOTE: The amount field is an Dec which implements the custom method + signatures required by gogoproto. + description: >- + medians defines a list of the median deviations for all stamped + denoms. + description: |- + QueryMedianDeviationsResponse is response type for the + Query/MedianDeviations RPC method. umee.oracle.v1.QueryMediansResponse: type: object properties: @@ -2768,6 +2911,34 @@ definitions: description: |- Prune Period represents the maximum amount of blocks which we want to keep a record of our set of exchange rates. + median_period: + type: string + format: uint64 + description: |- + Median Period represents the amount blocks we will wait between + calculating the median and standard deviation of the median of + historic prices in the last Prune Period. + historic_accept_list: + type: array + items: + type: object + properties: + base_denom: + type: string + symbol_denom: + type: string + exponent: + type: integer + format: int64 + title: Denom - the object to hold configurations of each denom + description: >- + Historic Asset List is a list of assets which will use the + historic + + price stamping protection methodology (mainly manipulatable + assets). + + Any assets not on this list will not be stamped. description: QueryParamsResponse is the response type for the Query/Params RPC method. umee.oracle.v1.QuerySlashWindowResponse: type: object diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index 1f468f36c7..6735720f8a 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.TokenSymbolPrice(ctx, req.Denom); oracleErr == nil { + if oraclePrice, _, oracleErr := q.Keeper.TokenDefaultDenomPrice(ctx, req.Denom); oracleErr == nil { resp.OraclePrice = &oraclePrice } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 31060fa713..e5ad56906e 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -38,10 +38,10 @@ func (k Keeper) TokenBasePrice(ctx sdk.Context, baseDenom string) (sdk.Dec, erro return price, nil } -// TokenSymbolPrice returns the USD value of a token. Note, the input denom must -// still be the base denomination, e.g. uumee. When error is nil, price is guaranteed +// 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) TokenSymbolPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { +func (k Keeper) TokenDefaultDenomPrice(ctx sdk.Context, baseDenom string) (sdk.Dec, uint32, error) { t, err := k.GetTokenSettings(ctx, baseDenom) if err != nil { return sdk.ZeroDec(), 0, err @@ -77,10 +77,10 @@ func exponent(input sdk.Dec, n int32) sdk.Dec { // TokenValue returns the total token value given a Coin. An error is // returned if we cannot get the token's price or if it's not an accepted token. -// Computation uses price of token's symbol denom to avoid rounding errors +// 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.TokenSymbolPrice(ctx, coin.Denom) + p, exp, err := k.TokenDefaultDenomPrice(ctx, coin.Denom) if err != nil { return sdk.ZeroDec(), err } @@ -109,14 +109,14 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, erro // 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 symbol denom to avoid rounding errors for exponent >= 18 tokens, +// 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.TokenSymbolPrice(ctx, fromDenom) + p1, e1, err := k.TokenDefaultDenomPrice(ctx, fromDenom) if err != nil { return sdk.ZeroDec(), err } - p2, e2, err := k.TokenSymbolPrice(ctx, toDenom) + p2, e2, err := k.TokenDefaultDenomPrice(ctx, toDenom) if err != nil { return sdk.ZeroDec(), err } diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 6d61ad07ec..ea273daea8 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -74,17 +74,17 @@ func (s *IntegrationTestSuite) TestOracle_TokenBasePrice() { func (s *IntegrationTestSuite) TestOracle_TokenSymbolPrice() { app, ctx, require := s.app, s.ctx, s.Require() - p, e, err := app.LeverageKeeper.TokenSymbolPrice(ctx, appparams.BondDenom) + p, e, err := app.LeverageKeeper.TokenDefaultDenomPrice(ctx, appparams.BondDenom) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("4.21"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenSymbolPrice(ctx, atomDenom) + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, atomDenom) require.NoError(err) require.Equal(sdk.MustNewDecFromStr("39.38"), p) require.Equal(uint32(6), e) - p, e, err = app.LeverageKeeper.TokenSymbolPrice(ctx, "foo") + p, e, err = app.LeverageKeeper.TokenDefaultDenomPrice(ctx, "foo") require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) require.Equal(uint32(0), e) diff --git a/x/leverage/types/query.pb.go b/x/leverage/types/query.pb.go index c935cad5cb..331e25b3b0 100644 --- a/x/leverage/types/query.pb.go +++ b/x/leverage/types/query.pb.go @@ -229,7 +229,7 @@ type QueryMarketSummaryResponse struct { SymbolDenom string `protobuf:"bytes,1,opt,name=symbol_denom,json=symbolDenom,proto3" json:"symbol_denom,omitempty"` // Exponent is the power of ten required to get from base denom to symbol denom. For example, an exponent of 6 means 10^6 uumee = 1 UMEE. Exponent uint32 `protobuf:"varint,2,opt,name=exponent,proto3" json:"exponent,omitempty"` - // Oracle Price is the current USD value of a base token. Exponent must be applied to reach the price from symbol_denom. For example, a price of $0.000001 for 1 uumee is equivalent to $1.00 for 1 UMEE. Oracle price is nil when the oracle is down. + // Oracle Price is the current USD value of a token. Oracle price is nil when the oracle is down. OraclePrice *github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,3,opt,name=oracle_price,json=oraclePrice,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"oracle_price,omitempty"` // uToken Exchange Rate is the amount of base tokens received when withdrawing 1 uToken. For example, a uToken exchange rate of 1.5 means a supplier receives 3 uumee for every 2 u/uumee they wish to withdraw. The same applies in reverse: supplying 3 uumee would award 2 u/uumee at that time. UTokenExchangeRate github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,4,opt,name=uToken_exchange_rate,json=uTokenExchangeRate,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"utoken_exchange_rate"` From 7517b625715913f88f186fe725f173b5d3e8f7ba Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:04:41 -0700 Subject: [PATCH 14/14] fix var na,e --- x/leverage/keeper/liquidate.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 65bc99613a..e24f87a254 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -92,14 +92,14 @@ func (k Keeper) getLiquidationAmounts( } // max repayment amount is limited by a number of factors - xxx := requestedRepay.Amount // maximum allowed by liquidator - xxx = sdk.MinInt(xxx, availableRepay) // liquidator account balance - xxx = sdk.MinInt(xxx, totalBorrowed.AmountOf(repayDenom)) // borrower position - xxx = sdk.MinInt(xxx, maxRepayAfterCloseFactor) // close factor + maxRepay := requestedRepay.Amount // maximum allowed by liquidator + maxRepay = sdk.MinInt(maxRepay, availableRepay) // liquidator account balance + maxRepay = sdk.MinInt(maxRepay, totalBorrowed.AmountOf(repayDenom)) // borrower position + maxRepay = sdk.MinInt(maxRepay, maxRepayAfterCloseFactor) // close factor // compute final liquidation amounts repay, burn, reward := ComputeLiquidation( - xxx, + maxRepay, borrowerCollateral.AmountOf(collateralDenom), k.AvailableLiquidity(ctx, rewardDenom), // repayTokenPrice,