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 {
toteki marked this conversation as resolved.
Show resolved Hide resolved
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")
toteki marked this conversation as resolved.
Show resolved Hide resolved
}

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)
toteki marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
25 changes: 21 additions & 4 deletions x/leverage/keeper/liquidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@ 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
}

// 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
}
Expand All @@ -89,6 +95,7 @@ func (k Keeper) getLiquidationAmounts(
k.AvailableLiquidity(ctx, rewardDenom),
repayTokenPrice,
rewardTokenPrice,
priceRatio,
exchangeRate,
liqudationIncentive,
closeFactor,
Expand All @@ -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
Expand All @@ -117,6 +125,7 @@ func ComputeLiquidation(
availableReward sdkmath.Int,
repayTokenPrice,
rewardTokenPrice,
priceRatio,
uTokenExchangeRate,
liquidationIncentive,
closeFactor,
Expand All @@ -130,7 +139,8 @@ 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
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)

Expand All @@ -145,7 +155,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
//
//
toteki marked this conversation as resolved.
Show resolved Hide resolved

ratio = sdk.MinDec(ratio,
borrowedValue.Mul(closeFactor).Quo(maxRepay.Mul(repayTokenPrice)),
)
Expand Down
6 changes: 4 additions & 2 deletions x/leverage/keeper/liquidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -175,15 +177,15 @@ 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()
expensiveCollateralDustDown.availableCollateral = sdkmath.NewInt(1)
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()
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)
toteki marked this conversation as resolved.
Show resolved Hide resolved

token, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uumee")
s.Require().NoError(err)
Expand Down
Loading