From 74884ae023b07ca115f58cbe6b3872eb4b07c0d9 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:26:23 -0700 Subject: [PATCH 1/2] fix: increase price calc precision for high exponent assets (#1633) * fix: improve price precision for high exponent assets * cl++ * cl++ * Update x/leverage/keeper/oracle.go * ++ * add DAI tests * analysis++ * Update x/leverage/keeper/limits.go * TODOs in liquidate - all else is finished * liquidation rounding improvement 1 * lint++ * move close factor computation one function higher * SymbolPrice -> DefaultDenomPrice * fix var na,e --- CHANGELOG.md | 8 + proto/umee/leverage/v1/query.proto | 2 +- swagger/swagger.yaml | 189 +++++++++++++++++++++-- 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 | 6 +- x/leverage/keeper/iter_test.go | 10 +- x/leverage/keeper/keeper.go | 6 +- x/leverage/keeper/limits.go | 6 +- x/leverage/keeper/liquidate.go | 68 ++++---- x/leverage/keeper/liquidate_test.go | 46 ++---- x/leverage/keeper/msg_server_test.go | 16 +- x/leverage/keeper/oracle.go | 69 +++++++-- x/leverage/keeper/oracle_test.go | 68 ++++++-- x/leverage/keeper/suite_test.go | 10 +- x/leverage/keeper/token_test.go | 2 +- x/leverage/simulation/operations_test.go | 2 +- x/leverage/types/query.pb.go | 2 +- 19 files changed, 391 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb73ec3e7c..d9688d5341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +- [1633](https://github.com/umee-network/umee/pull/1633) MarketSummary query now displays symbol price instead of base price for readability. + +### Fixes + +- [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 Since `umeed v3.2` there is a new runtime dependency: `libwasmvm.x86_64.so v1.1.1` is required. 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/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..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.TokenPrice(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/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 592be1eae1..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() { @@ -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/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/limits.go b/x/leverage/keeper/limits.go index bef5577902..6f8672d46a 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -40,11 +40,15 @@ 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 } uTokenExchangeRate := k.DeriveExchangeRate(ctx, tokenDenom) + + // 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() // return the computed maximum or the current uToken supply, whichever is smaller diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index b850823338..e24f87a254 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,13 +65,17 @@ func (k Keeper) getLiquidationAmounts( params.MinimumCloseFactor, params.CompleteLiquidationThreshold, ) - - // get oracle prices for the reward and repay denoms - repayTokenPrice, err := k.TokenPrice(ctx, repayDenom) - 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() } - rewardTokenPrice, err := k.TokenPrice(ctx, rewardDenom) + + // 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 } @@ -82,17 +91,24 @@ func (k Keeper) getLiquidationAmounts( liqudationIncentive = liqudationIncentive.Mul(sdk.OneDec().Sub(params.DirectLiquidationFee)) } + // max repayment amount is limited by a number of factors + 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( - sdk.MinInt(sdk.MinInt(availableRepay, maxRepay.Amount), totalBorrowed.AmountOf(repayDenom)), + maxRepay, 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 @@ -105,50 +121,40 @@ 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 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() } // Start with the maximum possible repayment amount, as a decimal maxRepay := toDec(availableRepay) // Determine the base maxReward amount that would result from maximum repayment - 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) // 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 - 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 50dded6ddb..cf80d904bd 100644 --- a/x/leverage/keeper/liquidate_test.go +++ b/x/leverage/keeper/liquidate_test.go @@ -21,50 +21,44 @@ 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 } } 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, - 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 @@ -107,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) @@ -175,7 +159,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 +167,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() diff --git a/x/leverage/keeper/msg_server_test.go b/x/leverage/keeper/msg_server_test.go index 94a91e3dbb..629ca924b4 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, @@ -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) @@ -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, @@ -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 68b4868382..e5ad56906e 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 } +// 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) { + t, err := k.GetTokenSettings(ctx, baseDenom) + if err != nil { + return sdk.ZeroDec(), 0, err + } + + if t.Blacklist { + return sdk.ZeroDec(), t.Exponent, types.ErrBlacklisted + } + + price, err := k.oracleKeeper.GetExchangeRate(ctx, t.SymbolDenom) + if err != nil { + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(err, "oracle") + } + + if price.IsNil() || !price.IsPositive() { + return sdk.ZeroDec(), t.Exponent, sdkerrors.Wrap(types.ErrInvalidOraclePrice, baseDenom) + } + + return price, 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 default 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.TokenDefaultDenomPrice(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,25 @@ 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 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, err := k.TokenPrice(ctx, fromDenom) + p1, e1, err := k.TokenDefaultDenomPrice(ctx, fromDenom) if err != nil { return sdk.ZeroDec(), err } - p2, err := k.TokenPrice(ctx, toDenom) + 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 p1.Quo(p2), nil + return exponent(p1, powerDifference).Quo(p2), nil } // 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..ea273daea8 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.000000000000000001"), } } -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.TokenDefaultDenomPrice(ctx, appparams.BondDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("4.21"), p) + require.Equal(uint32(6), e) + + 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.TokenDefaultDenomPrice(ctx, "foo") require.ErrorIs(err, types.ErrNotRegisteredToken) require.Equal(sdk.ZeroDec(), p) + require.Equal(uint32(0), e) } func (s *IntegrationTestSuite) TestOracle_TokenValue() { @@ -109,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 845f6fe35d..ef473e0e8b 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,9 @@ 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))) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, newToken(daiDenom, "DAI", 18))) // override DefaultGenesis params with fixtures.Params app.LeverageKeeper.SetParams(ctx, fixtures.Params()) @@ -98,8 +100,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 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 827513977beb497c40b47f462a9c5ff4d715927e Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:55:17 -0800 Subject: [PATCH 2/2] docs: update price-feeder changelog for v2.0.1 (#1636) ## Description Updates the price feeder changelog for the expected v2.0.1 release --- ### Author Checklist _All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues._ I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] added appropriate labels to the PR - [ ] targeted the correct branch (see [PR Targeting](https://github.com/umee-network/umee/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist _All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items._ I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- price-feeder/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/price-feeder/CHANGELOG.md b/price-feeder/CHANGELOG.md index 9a4ce742cb..90759ba6a7 100644 --- a/price-feeder/CHANGELOG.md +++ b/price-feeder/CHANGELOG.md @@ -44,11 +44,15 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog -## [Unreleased] +## Unreleased + +## [v2.0.1](https://github.com/umee-network/umee/releases/tag/price-feeder/v2.0.1) 2022-12-01 ### Bugs - [1615](https://github.com/umee-network/umee/pull/1615) Parse multiple candles from OsmosisV2 response +- [1635](https://github.com/umee-network/umee/pull/1635) Vote on exchange rates even if one is missing. +- [1634](https://github.com/umee-network/umee/pull/1634) Add minimum candle volume for low-trading assets. ### Improvements