Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: increase price calc precision for high exponent assets #1633

Merged
merged 16 commits into from
Dec 2, 2022
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions x/leverage/client/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions x/leverage/fixtures/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 1 addition & 1 deletion x/leverage/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions x/leverage/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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"),
Expand Down
10 changes: 5 additions & 5 deletions x/leverage/keeper/iter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down
6 changes: 5 additions & 1 deletion x/leverage/keeper/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
4 changes: 2 additions & 2 deletions x/leverage/keeper/liquidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 8 additions & 8 deletions x/leverage/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
69 changes: 58 additions & 11 deletions x/leverage/keeper/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 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)
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
Expand All @@ -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 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
}

// FundOracle transfers requested coins to the oracle module account, as
Expand Down
Loading