From 3527534d5bb5e6289abc9f519e8559e63583f24d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:52:39 -0700 Subject: [PATCH 01/17] fix: allow safe leverage operations during partial oracle outages --- proto/umee/leverage/v1/query.proto | 11 ++- swagger/swagger.yaml | 45 ++++++++++++ x/leverage/client/tests/tests.go | 4 +- x/leverage/keeper/borrows.go | 44 +++++++++++- x/leverage/keeper/grpc_query.go | 72 ++++++++++--------- x/leverage/keeper/grpc_query_test.go | 3 +- x/leverage/keeper/iter.go | 18 ++--- x/leverage/keeper/limits.go | 52 +++++++++----- x/leverage/keeper/liquidate.go | 8 +-- x/leverage/keeper/msg_server.go | 19 ++++- x/leverage/keeper/oracle.go | 17 +++++ x/leverage/types/query.pb.go | 103 +++++++++++++++------------ 12 files changed, 279 insertions(+), 117 deletions(-) diff --git a/proto/umee/leverage/v1/query.proto b/proto/umee/leverage/v1/query.proto index c1a95d50fb..a120bcede7 100644 --- a/proto/umee/leverage/v1/query.proto +++ b/proto/umee/leverage/v1/query.proto @@ -222,31 +222,40 @@ message QueryAccountSummary { // QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler. message QueryAccountSummaryResponse { // Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned. + // Computation skips assets which are missing oracle prices, potentially resulting in a lower supplied + // value than if prices were all available. string supplied_value = 1 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; // Collateral Value is the sum of the USD value of all uTokens the account has collateralized. + // Computation skips collateral which is missing an oracle price, potentially resulting in a lower collateral + // value than if prices were all available. string collateral_value = 2 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; // Borrowed Value is the sum of the USD value of all tokens the account has borrowed, including interest owed. // It always uses spot prices. + // Computation skips borrows which are missing oracle prices, potentially resulting in a lower borrowed + // value than if prices were all available. string borrowed_value = 3 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; // Borrow Limit is the maximum Borrowed Value the account is allowed to reach through direct borrowing. // The lower of spot or historic price for each collateral token is used when calculating borrow limits. + // Computation skips collateral which is missing an oracle price, potentially resulting in a lower borrow + // limit than if prices were all available. string borrow_limit = 4 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false ]; // Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation. + // Will be null if an oracle price required for computation is missing. string liquidation_threshold = 5 [ (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", - (gogoproto.nullable) = false + (gogoproto.nullable) = true ]; } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 102ca388d9..9eef1bf4e0 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -132,11 +132,21 @@ paths: description: >- Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned. + + Computation skips assets which are missing oracle prices, + potentially resulting in a lower supplied + + value than if prices were all available. collateral_value: type: string description: >- Collateral Value is the sum of the USD value of all uTokens the account has collateralized. + + Computation skips collateral which is missing an oracle price, + potentially resulting in a lower collateral + + value than if prices were all available. borrowed_value: type: string description: >- @@ -144,6 +154,11 @@ paths: account has borrowed, including interest owed. It always uses spot prices. + + Computation skips borrows which are missing oracle prices, + potentially resulting in a lower borrowed + + value than if prices were all available. borrow_limit: type: string description: >- @@ -152,11 +167,19 @@ paths: The lower of spot or historic price for each collateral token is used when calculating borrow limits. + + Computation skips collateral which is missing an oracle price, + potentially resulting in a lower borrow + + limit than if prices were all available. liquidation_threshold: type: string description: >- Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation. + + Will be null if an oracle price required for computation is + missing. description: >- QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler. @@ -2023,11 +2046,21 @@ definitions: description: >- Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned. + + Computation skips assets which are missing oracle prices, potentially + resulting in a lower supplied + + value than if prices were all available. collateral_value: type: string description: >- Collateral Value is the sum of the USD value of all uTokens the account has collateralized. + + Computation skips collateral which is missing an oracle price, + potentially resulting in a lower collateral + + value than if prices were all available. borrowed_value: type: string description: >- @@ -2035,6 +2068,11 @@ definitions: has borrowed, including interest owed. It always uses spot prices. + + Computation skips borrows which are missing oracle prices, potentially + resulting in a lower borrowed + + value than if prices were all available. borrow_limit: type: string description: >- @@ -2043,11 +2081,18 @@ definitions: The lower of spot or historic price for each collateral token is used when calculating borrow limits. + + Computation skips collateral which is missing an oracle price, + potentially resulting in a lower borrow + + limit than if prices were all available. liquidation_threshold: type: string description: >- Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation. + + Will be null if an oracle price required for computation is missing. description: >- QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler. diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index d1d81b79b7..da7fd310bc 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -252,6 +252,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { nil, } + lt1 := sdk.MustNewDecFromStr("0.0085610525") + nonzeroQueries := []testQuery{ { "query account balances", @@ -294,7 +296,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { // (1001 / 1000000) * 34.21 * 0.25 = 0.0085610525 BorrowLimit: sdk.MustNewDecFromStr("0.0085610525"), // (1001 / 1000000) * 0.25 * 34.21 = 0.0085610525 - LiquidationThreshold: sdk.MustNewDecFromStr("0.0085610525"), + LiquidationThreshold: <1, }, }, { diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 02af472cf8..d7e43817b7 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -9,7 +9,9 @@ import ( // assertBorrowerHealth returns an error if a borrower is currently above their borrow limit, // under either recent (historic median) or current prices. It returns an error if -// prices cannot be calculated. +// borrowed asset prices cannot be calculated, but will try to treat collateral whose prices are +// unavailable as having zero value. This can still result in a borrow limit being too low, +// unless the remaining collateral is enough to cover all borrows. // This should be checked in msg_server.go at the end of any transaction which is restricted // by borrow limits, i.e. Borrow, Decollateralize, Withdraw, MaxWithdraw. func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { @@ -20,8 +22,9 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres if err != nil { return err } - limit, err := k.CalculateBorrowLimit(ctx, collateral) + limit, err := k.VisibleBorrowLimit(ctx, collateral) if err != nil { + // This error is not a missing price return err } if value.GT(limit) { @@ -127,6 +130,43 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk return limit, nil } +// VisibleBorrowLimit uses the price oracle to determine the borrow limit (in USD) provided by +// collateral sdk.Coins, using each token's uToken exchange rate and collateral weight. +// The lower of spot price or historic price is used for each collateral token. +// An error is returned if any input coins are not uTokens. +// This function skips assets that are missing prices, which will lead to a lower borrow +// limit when prices are down instead of a complete loss of borrowing ability. +func (k Keeper) VisibleBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) { + limit := sdk.ZeroDec() + + for _, coin := range collateral { + // convert uToken collateral to base assets + baseAsset, err := k.ExchangeUToken(ctx, coin) + if err != nil { + return sdk.ZeroDec(), err + } + + ts, err := k.GetTokenSettings(ctx, baseAsset.Denom) + if err != nil { + return sdk.ZeroDec(), err + } + + // ignore blacklisted tokens + if !ts.Blacklist { + // get USD value of base assets using the chosen price mode + v, err := k.TokenValue(ctx, baseAsset, types.PriceModeLow) + if err == nil { + // if both spot and historic (if requeired) prices exist, + // add each collateral coin's weighted value to borrow limit + limit = limit.Add(v.Mul(ts.CollateralWeight)) + } + + } + } + + return limit, nil +} + // CalculateLiquidationThreshold determines the maximum borrowed value (in USD) that a // borrower with given collateral could reach before being eligible for liquidation, using // each token's oracle price, uToken exchange rate, and liquidation threshold. diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index e3c9ae7936..272efd0576 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -208,42 +208,45 @@ func (q Querier) AccountSummary( collateral := q.Keeper.GetBorrowerCollateral(ctx, addr) borrowed := q.Keeper.GetBorrowerBorrows(ctx, addr) - // supplied value always uses spot prices - suppliedValue, err := q.Keeper.TotalTokenValue(ctx, supplied, types.PriceModeSpot) - if err != nil { - return nil, err - } - // borrowed value here is shown using spot prices, but leverage logic instead uses + // supplied value always uses spot prices, and skips supplied assets that are missing prices + suppliedValue := q.Keeper.VisibleTokenValue(ctx, supplied, types.PriceModeSpot) + + // borrowed value uses spot prices here, but leverage logic instead uses // the higher of spot or historic prices for each borrowed token when comparing it - // to borrow limit. - borrowedValue, err := q.Keeper.TotalTokenValue(ctx, borrowed, types.PriceModeSpot) - if err != nil { - return nil, err - } - // collateral value always uses spot prices - collateralValue, err := q.Keeper.CalculateCollateralValue(ctx, collateral) + // to borrow limit. This line also skips borrowed assets that are missing prices. + borrowedValue := q.Keeper.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) + + // collateral value always uses spot prices, and this line skips assets that are missing prices + collateralValue, err := q.Keeper.VisibleCollateralValue(ctx, collateral) if err != nil { + // this error isn't a missing price - it would be non-uToken collateral return nil, err } + // borrow limit shown here as it is used in leverage logic: // using the lower of spot or historic prices for each collateral token - borrowLimit, err := q.Keeper.CalculateBorrowLimit(ctx, collateral) + // skips collateral tokens with missing oracle prices + borrowLimit, err := q.Keeper.VisibleBorrowLimit(ctx, collateral) if err != nil { + // this error isn't a missing price - it would be non-uToken collateral return nil, err } - // liquidation always uses spot prices + + resp := &types.QueryAccountSummaryResponse{ + SuppliedValue: suppliedValue, + CollateralValue: collateralValue, + BorrowedValue: borrowedValue, + BorrowLimit: borrowLimit, + } + + // liquidation always uses spot prices. This response field will be null + // if a price is missing liquidationThreshold, err := q.Keeper.CalculateLiquidationThreshold(ctx, collateral) - if err != nil { - return nil, err + if err == nil { + resp.LiquidationThreshold = &liquidationThreshold } - return &types.QueryAccountSummaryResponse{ - SuppliedValue: suppliedValue, - CollateralValue: collateralValue, - BorrowedValue: borrowedValue, - BorrowLimit: borrowLimit, - LiquidationThreshold: liquidationThreshold, - }, nil + return resp, nil } func (q Querier) LiquidationTargets( @@ -320,11 +323,13 @@ func (q Querier) MaxWithdraw( } for _, denom := range denoms { + // If a price is missing for the borrower's collateral, + // but not this uToken or any of their borrows, error + // will be nil and the resulting value will be what + // can safely be withdrawn even with missing prices. + // On non-nil error here, max withdraw is zero. uToken, err := q.Keeper.maxWithdraw(ctx, addr, denom) - if err != nil { - return nil, err - } - if uToken.IsPositive() { + if err == nil && uToken.IsPositive() { token, err := q.Keeper.ExchangeUToken(ctx, uToken) if err != nil { return nil, err @@ -374,14 +379,15 @@ func (q Querier) MaxBorrow( } for _, denom := range denoms { + // If a price is missing for the borrower's collateral, + // but not this token or any of their borrows, error + // will be nil and the resulting value will be what + // can safely be borrowed even with missing prices. + // On non-nil error here, max borrow is zero. maxBorrow, err := q.Keeper.maxBorrow(ctx, addr, denom) - if err != nil { - return nil, err - } - if maxBorrow.IsPositive() { + if err == nil && maxBorrow.IsPositive() { maxTokens = maxTokens.Add(maxBorrow) } - } return &types.QueryMaxBorrowResponse{ diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 48c1149262..bd07a8864e 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -98,6 +98,7 @@ func (s *IntegrationTestSuite) TestQuerier_AccountSummary() { resp, err := s.queryClient.AccountSummary(ctx.Context(), &types.QueryAccountSummary{Address: addr.String()}) require.NoError(err) + lt := sdk.MustNewDecFromStr("1052.5") expected := types.QueryAccountSummaryResponse{ // This result is umee's oracle exchange rate from // from .Reset() in x/leverage/keeper/oracle_test.go @@ -112,7 +113,7 @@ func (s *IntegrationTestSuite) TestQuerier_AccountSummary() { // (1000) * 4.21 * 0.25 = 1052.5 BorrowLimit: sdk.MustNewDecFromStr("1052.5"), // (1000) * 4.21 * 0.25 = 1052.5 - LiquidationThreshold: sdk.MustNewDecFromStr("1052.5"), + LiquidationThreshold: <, } require.Equal(expected, *resp) diff --git a/x/leverage/keeper/iter.go b/x/leverage/keeper/iter.go index fe7d730bff..80679240a3 100644 --- a/x/leverage/keeper/iter.go +++ b/x/leverage/keeper/iter.go @@ -168,20 +168,16 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress collateral := k.GetBorrowerCollateral(ctx, addr) // use oracle helper functions to find total borrowed value in USD - borrowValue, err := k.TotalTokenValue(ctx, borrowed, types.PriceModeSpot) - if err != nil { - return err - } + // skips denoms without prices + borrowValue := k.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) // compute liquidation threshold from enabled collateral + // in this case, we can't reasonable skil missing prices but can move on + // to the next borrower instead of stopping the entire query liquidationLimit, err := k.CalculateLiquidationThreshold(ctx, collateral) - if err != nil { - return err - } - - // If liquidation limit is smaller than borrowed value then the - // address is eligible for liquidation. - if liquidationLimit.LT(borrowValue) { + if err == nil && liquidationLimit.LT(borrowValue) { + // If liquidation limit is smaller than borrowed value then the + // address is eligible for liquidation. liquidationTargets = append(liquidationTargets, addr) } diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 36ea8c77be..7ed73cf2fe 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -7,7 +7,9 @@ import ( ) // maxWithdraw calculates the maximum amount of uTokens an account can currently withdraw. -// input denom should be a base token. +// input denom should be a base token. If oracle prices are missing for some of the borrower's +// collateral (other than the denom being withdrawn), computes the maximum safe withdraw allowed +// by only the collateral whose prices are known func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { uDenom := types.ToUTokenDenom(denom) availableTokens := sdk.NewCoin(denom, k.AvailableLiquidity(ctx, denom)) @@ -18,7 +20,8 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) totalBorrowed := k.GetBorrowerBorrows(ctx, addr) walletUtokens := k.bankKeeper.SpendableCoins(ctx, addr).AmountOf(uDenom) totalCollateral := k.GetBorrowerCollateral(ctx, addr) - specificCollateral := sdk.NewCoin(uDenom, totalCollateral.AmountOf(uDenom)) + thisCollateral := sdk.NewCoin(uDenom, totalCollateral.AmountOf(uDenom)) + otherCollateral := totalCollateral.Sub(thisCollateral) // calculate borrowed value for the account, using the higher of spot or historic prices for each token borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, types.PriceModeHigh) @@ -33,8 +36,23 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) return sdk.NewCoin(uDenom, withdrawAmount), nil } + // compute the borrower's borrow limit using all their collateral + // except the denom being withdrawn (also excluding collateral missing oracle prices) + otherBorrowLimit, err := k.VisibleBorrowLimit(ctx, otherCollateral) + if err != nil { + return sdk.Coin{}, err + } + // if their other collateral fully covers all borrows, withdraw the maximum available amount + if borrowedValue.LT(otherBorrowLimit) { + withdrawAmount := walletUtokens.Add(totalCollateral.AmountOf(uDenom)) + withdrawAmount = sdk.MinInt(withdrawAmount, availableUTokens.Amount) + return sdk.NewCoin(uDenom, withdrawAmount), nil + } + // for nonzero borrows, calculations are based on unused borrow limit - borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral) + // this treats collateral which is missing oracle prices as having zero value, + // resulting in a lower borrow limit but not in an error + borrowLimit, err := k.VisibleBorrowLimit(ctx, totalCollateral) if err != nil { return sdk.Coin{}, err } @@ -48,20 +66,16 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) unusedBorrowLimit := borrowLimit.Sub(borrowedValue) // calculate the contribution to borrow limit made by only the type of collateral being withdrawn - specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(specificCollateral)) + // this WILL error on a missing price, since the cases where we know other collateral is sufficient + // have all been eliminated + specificBorrowLimit, err := k.CalculateBorrowLimit(ctx, sdk.NewCoins(thisCollateral)) if err != nil { return sdk.Coin{}, err } - if unusedBorrowLimit.GT(specificBorrowLimit) { - // If borrow limit is sufficiently high even without this collateral, withdraw the full amount - withdrawAmount := walletUtokens.Add(specificCollateral.Amount) - withdrawAmount = sdk.MinInt(withdrawAmount, availableUTokens.Amount) - return sdk.NewCoin(uDenom, withdrawAmount), nil - } // if only a portion of collateral is unused, withdraw only that portion unusedCollateralFraction := unusedBorrowLimit.Quo(specificBorrowLimit) - unusedCollateral := unusedCollateralFraction.MulInt(specificCollateral.Amount).TruncateInt() + unusedCollateral := unusedCollateralFraction.MulInt(thisCollateral.Amount).TruncateInt() // add wallet uTokens to the unused amount from collateral withdrawAmount := unusedCollateral.Add(walletUtokens) @@ -73,7 +87,8 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) } // maxBorrow calculates the maximum amount of a given token an account can currently borrow. -// input denom should be a base token. +// input denom should be a base token. If oracle prices are missing for some of the borrower's +// collateral, computes the maximum safe borrow allowed by only the collateral whose prices are known func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) (sdk.Coin, error) { if types.HasUTokenPrefix(denom) { return sdk.Coin{}, types.ErrUToken @@ -89,8 +104,8 @@ func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) ( return sdk.Coin{}, err } - // calculate borrow limit for the account - borrowLimit, err := k.CalculateBorrowLimit(ctx, totalCollateral) + // calculate borrow limit for the account, using only collateral whose price is known + borrowLimit, err := k.VisibleBorrowLimit(ctx, totalCollateral) if err != nil { return sdk.Coin{}, err } @@ -116,7 +131,8 @@ func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) ( // maxCollateralFromShare calculates the maximum amount of collateral a utoken denom // is allowed to have, taking into account its associated token's MaxCollateralShare -// under current market conditions +// under current market conditions. If any collateral denoms other than this are missing +// oracle prices, calculates a (lower) maximum amount using the collateral with known prices. func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath.Int, error) { token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(denom)) if err != nil { @@ -137,8 +153,8 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. systemCollateral := k.GetAllTotalCollateral(ctx) thisDenomCollateral := sdk.NewCoin(denom, systemCollateral.AmountOf(denom)) - // get USD collateral value for all other denoms - otherDenomsValue, err := k.CalculateCollateralValue(ctx, systemCollateral.Sub(thisDenomCollateral)) + // get USD collateral value for all other denoms, skipping those which are missing oracle prices + otherDenomsValue, err := k.VisibleCollateralValue(ctx, systemCollateral.Sub(thisDenomCollateral)) if err != nil { return sdk.ZeroInt(), err } @@ -147,7 +163,7 @@ func (k *Keeper) maxCollateralFromShare(ctx sdk.Context, denom string) (sdkmath. maxValue := otherDenomsValue.Quo(sdk.OneDec().Sub(token.MaxCollateralShare)).Mul(token.MaxCollateralShare) // determine the amount of base tokens which would be required to reach maxValue, - // using the hgiher of spot or historic prices + // using the higher of spot or historic prices udenom := types.ToUTokenDenom(denom) maxUTokens, err := k.UTokenWithValue(ctx, udenom, maxValue, types.PriceModeHigh) if err != nil { diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 685b7b7141..41ec1c2d9c 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -27,11 +27,9 @@ func (k Keeper) getLiquidationAmounts( 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, types.PriceModeSpot) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } + // calculate borrower health in USD values, using spot prices only (no historic) + // borrowed value will skip borrowed tokens with unknown oracle prices, treating them as zero value + borrowedValue := k.VisibleTokenValue(ctx, totalBorrowed, types.PriceModeSpot) collateralValue, err := k.CalculateCollateralValue(ctx, borrowerCollateral) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index a36fc8dbe8..b441952c97 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -74,6 +74,7 @@ func (s msgServer) Withdraw( } // Fail here if supplier ends up over their borrow limit under current or historic prices + // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) if err != nil { return nil, err @@ -107,6 +108,10 @@ func (s msgServer) MaxWithdraw( return nil, err } + // If a price is missing for the borrower's collateral, + // but not this uToken or any of their borrows, error + // will be nil and the resulting value will be what + // can safely be withdrawn even with missing prices. uToken, err := s.keeper.maxWithdraw(ctx, supplierAddr, msg.Denom) if err != nil { return nil, err @@ -123,6 +128,7 @@ func (s msgServer) MaxWithdraw( } // Fail here if supplier ends up over their borrow limit under current or historic prices + // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) if err != nil { return nil, err @@ -172,6 +178,8 @@ func (s msgServer) Collateralize( return nil, err } + // Fail here if collateral share restrictions are violated, + // based on only collateral with known oracle prices if err := s.keeper.checkCollateralShare(ctx, msg.Asset.Denom); err != nil { return nil, err } @@ -211,11 +219,13 @@ func (s msgServer) SupplyCollateral( return nil, err } - // Fail here if collateral share or liquidity restrictions are violated + // Fail here if collateral liquidity restrictions are violated if err := s.keeper.checkCollateralLiquidity(ctx, msg.Asset.Denom); err != nil { return nil, err } + // Fail here if collateral share restrictions are violated, + // based on only collateral with known oracle prices if err := s.keeper.checkCollateralShare(ctx, uToken.Denom); err != nil { return nil, err } @@ -260,6 +270,7 @@ func (s msgServer) Decollateralize( } // Fail here if borrower ends up over their borrow limit under current or historic prices + // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr) if err != nil { return nil, err @@ -292,6 +303,7 @@ func (s msgServer) Borrow( } // Fail here if borrower ends up over their borrow limit under current or historic prices + // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr) if err != nil { return nil, err @@ -330,6 +342,10 @@ func (s msgServer) MaxBorrow( return nil, err } + // If a price is missing for the borrower's collateral, + // but not this uToken or any of their borrows, error + // will be nil and the resulting value will be what + // can safely be withdrawn even with missing prices. maxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom) if err != nil { return nil, err @@ -343,6 +359,7 @@ func (s msgServer) MaxBorrow( } // Fail here if borrower ends up over their borrow limit under current or historic prices + // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr) if err != nil { return nil, err diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 6734760dfd..ae9f31d2c8 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -119,6 +119,23 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.Pri return total, nil } +// VisibleTokenValue functions like TotalTokenValue, but interprets missing oracle prices +// as zero value instead of returning an error. +func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) sdk.Dec { + total := sdk.ZeroDec() + + accepted := k.filterAcceptedCoins(ctx, coins) + + for _, c := range accepted { + v, err := k.TokenValue(ctx, c, mode) + if err == nil { + total = total.Add(v) + } + } + + return total +} + // TokenWithValue creates a token of a given denom with an given USD value. // Returns an error on invalid price or denom. Rounds down, i.e. the // value of the token returned may be slightly less than the requested value. diff --git a/x/leverage/types/query.pb.go b/x/leverage/types/query.pb.go index eef9b56949..34061ec5de 100644 --- a/x/leverage/types/query.pb.go +++ b/x/leverage/types/query.pb.go @@ -421,17 +421,26 @@ var xxx_messageInfo_QueryAccountSummary proto.InternalMessageInfo // QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler. type QueryAccountSummaryResponse struct { // Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned. + // Computation skips assets which are missing oracle prices, potentially resulting in a lower supplied + // value than if prices were all available. SuppliedValue github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,1,opt,name=supplied_value,json=suppliedValue,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"supplied_value"` // Collateral Value is the sum of the USD value of all uTokens the account has collateralized. + // Computation skips collateral which is missing an oracle price, potentially resulting in a lower collateral + // value than if prices were all available. CollateralValue github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,2,opt,name=collateral_value,json=collateralValue,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"collateral_value"` // Borrowed Value is the sum of the USD value of all tokens the account has borrowed, including interest owed. // It always uses spot prices. + // Computation skips borrows which are missing oracle prices, potentially resulting in a lower borrowed + // value than if prices were all available. BorrowedValue github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,3,opt,name=borrowed_value,json=borrowedValue,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"borrowed_value"` // Borrow Limit is the maximum Borrowed Value the account is allowed to reach through direct borrowing. // The lower of spot or historic price for each collateral token is used when calculating borrow limits. + // Computation skips collateral which is missing an oracle price, potentially resulting in a lower borrow + // limit than if prices were all available. BorrowLimit github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,4,opt,name=borrow_limit,json=borrowLimit,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"borrow_limit"` // Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation. - LiquidationThreshold github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=liquidation_threshold,json=liquidationThreshold,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"liquidation_threshold"` + // Will be null if an oracle price required for computation is missing. + LiquidationThreshold *github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=liquidation_threshold,json=liquidationThreshold,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"liquidation_threshold,omitempty"` } func (m *QueryAccountSummaryResponse) Reset() { *m = QueryAccountSummaryResponse{} } @@ -806,7 +815,7 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/query.proto", fileDescriptor_1e8137dcabb0ccc7) } var fileDescriptor_1e8137dcabb0ccc7 = []byte{ - // 1442 bytes of a gzipped FileDescriptorProto + // 1443 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x98, 0xcf, 0x6f, 0x1b, 0x45, 0x1b, 0xc7, 0xb3, 0x49, 0x9b, 0x1f, 0x8f, 0xe3, 0x24, 0x9d, 0xa6, 0xcd, 0xd6, 0x4d, 0x6d, 0x77, 0xdb, 0xb4, 0x69, 0xdf, 0x37, 0xde, 0xa6, 0xef, 0x2b, 0x24, 0x04, 0x12, 0xd4, 0x2d, 0x08, 0x50, @@ -866,38 +875,38 @@ var fileDescriptor_1e8137dcabb0ccc7 = []byte{ 0x7a, 0x24, 0x78, 0x3c, 0x13, 0x78, 0xbe, 0xcf, 0x11, 0xe8, 0x07, 0x30, 0x17, 0xa7, 0x43, 0x82, 0x27, 0xb2, 0x29, 0x8e, 0x29, 0x02, 0x7b, 0x17, 0x66, 0xe5, 0xf5, 0xec, 0x3a, 0x9e, 0x13, 0xca, 0x8e, 0xe5, 0xb0, 0xd0, 0x9c, 0x60, 0x6c, 0x44, 0x08, 0x64, 0xc1, 0x29, 0x51, 0x98, 0x79, 0x27, - 0x5d, 0x0f, 0xb7, 0x18, 0x09, 0xb6, 0xa8, 0x6b, 0xcb, 0xee, 0xe4, 0xb0, 0xec, 0xc5, 0x04, 0xec, - 0x7e, 0xcc, 0x32, 0xce, 0xc0, 0x12, 0xdf, 0xdf, 0x8d, 0xc4, 0x24, 0x66, 0x4d, 0x12, 0x06, 0xc6, - 0x5b, 0x50, 0x1a, 0x32, 0xa5, 0xb6, 0x5f, 0x87, 0xa9, 0x50, 0x3c, 0xe2, 0x6f, 0xe3, 0x4c, 0x2d, - 0x1e, 0x1a, 0xf3, 0x90, 0xe7, 0xce, 0x55, 0x6c, 0xdf, 0x22, 0x8d, 0x30, 0x30, 0x6a, 0xb2, 0x59, - 0x8e, 0x1f, 0x24, 0x7a, 0xe1, 0x7d, 0x8c, 0xe8, 0xec, 0xa7, 0x5a, 0x61, 0xe9, 0x24, 0x9b, 0x61, - 0x15, 0xa4, 0x0a, 0x0b, 0xb2, 0xbd, 0xed, 0xaa, 0xca, 0x3a, 0xf4, 0x2c, 0xf7, 0x7b, 0xe4, 0xf1, - 0x64, 0x8f, 0xfc, 0xa7, 0x06, 0xfa, 0x20, 0x44, 0x69, 0x23, 0x30, 0x25, 0x2e, 0x9c, 0xe0, 0x28, - 0xaa, 0x4d, 0xcc, 0x46, 0x16, 0x4c, 0x86, 0x22, 0xca, 0x11, 0x14, 0x1a, 0x89, 0x36, 0xde, 0x85, - 0xb9, 0x78, 0x9d, 0xf2, 0x8e, 0x3b, 0x6c, 0xaa, 0x9e, 0xc2, 0xe9, 0xfd, 0x04, 0x95, 0xa7, 0xfe, - 0x02, 0xb4, 0x23, 0x5b, 0xc0, 0xf5, 0xbf, 0x66, 0xe0, 0x38, 0x8f, 0x8f, 0x5a, 0x30, 0x29, 0x3e, - 0xc8, 0xd0, 0xb9, 0xf4, 0x59, 0x49, 0x7c, 0xe1, 0x15, 0x56, 0x5e, 0x39, 0x1d, 0xcb, 0x37, 0xca, - 0x9f, 0xff, 0xf8, 0xc7, 0x37, 0xe3, 0x05, 0xa4, 0x9b, 0xa9, 0xcf, 0x50, 0xf1, 0xa9, 0x87, 0xbe, - 0xd3, 0x60, 0x61, 0xf0, 0x6b, 0x0e, 0x5d, 0x1e, 0x42, 0x1f, 0x34, 0x2c, 0x98, 0x23, 0x1a, 0x2a, - 0x41, 0xff, 0xe1, 0x82, 0x56, 0xd0, 0x85, 0xb4, 0x20, 0xa6, 0x7c, 0xea, 0x22, 0x2f, 0xe8, 0x4b, - 0x0d, 0xf2, 0xfb, 0xbf, 0x06, 0x2f, 0x0e, 0x89, 0xb7, 0xcf, 0xaa, 0xf0, 0xdf, 0x51, 0xac, 0x94, - 0xa4, 0x55, 0x2e, 0xc9, 0x40, 0xe5, 0xb4, 0x24, 0x8f, 0x3b, 0xd4, 0x03, 0x19, 0xfd, 0x5b, 0x0d, - 0xe6, 0x07, 0xaf, 0xfc, 0x4b, 0x43, 0x62, 0x0d, 0xd8, 0x15, 0x2a, 0xa3, 0xd9, 0x29, 0x55, 0x57, - 0xb9, 0xaa, 0x8b, 0xc8, 0x48, 0xab, 0xc2, 0xc2, 0xa5, 0xde, 0x88, 0x35, 0x7c, 0xad, 0xc1, 0xdc, - 0xc0, 0xc5, 0xb7, 0xf2, 0xea, 0x70, 0x71, 0xa6, 0xd6, 0x46, 0x32, 0x53, 0xa2, 0xae, 0x70, 0x51, - 0x17, 0xd0, 0xf9, 0xe1, 0xa2, 0xe2, 0x5c, 0x7d, 0xaf, 0x01, 0x4a, 0xd7, 0x57, 0x74, 0x65, 0x48, - 0xc0, 0xb4, 0x69, 0x61, 0x7d, 0x64, 0x53, 0xa5, 0x6f, 0x8d, 0xeb, 0xbb, 0x8c, 0x56, 0xd2, 0xfa, - 0xf6, 0x5d, 0x38, 0x52, 0x4c, 0x0f, 0xa6, 0xe3, 0xa2, 0x8d, 0x4a, 0x43, 0xa2, 0xc5, 0x06, 0x85, - 0xcb, 0xaf, 0x31, 0x50, 0x22, 0x2e, 0x70, 0x11, 0xe7, 0xd0, 0xd9, 0xb4, 0x88, 0x06, 0xb6, 0xeb, - 0x36, 0x0f, 0xf7, 0x85, 0x06, 0xb9, 0x64, 0x71, 0x37, 0x86, 0x1e, 0x59, 0x65, 0x53, 0xb8, 0xfa, - 0x7a, 0x1b, 0x25, 0xe2, 0x12, 0x17, 0x51, 0x46, 0xc5, 0x83, 0x0e, 0x75, 0x57, 0xf5, 0xfd, 0xe8, - 0x29, 0xcc, 0xf4, 0xcb, 0x66, 0x79, 0x78, 0x00, 0x61, 0x51, 0x58, 0x7d, 0x9d, 0x85, 0x12, 0x70, - 0x91, 0x0b, 0x28, 0xa2, 0xe5, 0x83, 0x05, 0x88, 0x76, 0xa0, 0x7a, 0x67, 0xf7, 0xf7, 0xe2, 0xd8, - 0xee, 0xf3, 0xa2, 0xf6, 0xec, 0x79, 0x51, 0xfb, 0xed, 0x79, 0x51, 0xfb, 0xea, 0x45, 0x71, 0xec, - 0xd9, 0x8b, 0xe2, 0xd8, 0x4f, 0x2f, 0x8a, 0x63, 0x8f, 0xae, 0x25, 0x2a, 0x69, 0x44, 0x59, 0xf3, - 0x49, 0xb8, 0x43, 0xd9, 0xb6, 0x40, 0x76, 0xfe, 0x6f, 0x76, 0xfb, 0x5c, 0x5e, 0x57, 0x1b, 0x93, - 0xfc, 0xbf, 0x63, 0xff, 0xfb, 0x27, 0x00, 0x00, 0xff, 0xff, 0x2b, 0xa0, 0xd2, 0xea, 0xe4, 0x13, - 0x00, 0x00, + 0x5d, 0x0f, 0xb7, 0x18, 0x09, 0xb6, 0xa8, 0x6b, 0xcb, 0xee, 0xe4, 0xb0, 0xaf, 0xee, 0x62, 0x02, + 0x76, 0x3f, 0x66, 0x19, 0x67, 0x60, 0x89, 0xef, 0xef, 0x46, 0x62, 0x12, 0xb3, 0x26, 0x09, 0x03, + 0xe3, 0x2d, 0x28, 0x0d, 0x99, 0x52, 0xdb, 0xaf, 0xc3, 0x54, 0x28, 0x1e, 0xf1, 0xb7, 0x71, 0xa6, + 0x16, 0x0f, 0x8d, 0x79, 0xc8, 0x73, 0xe7, 0x2a, 0xb6, 0x6f, 0x91, 0x46, 0x18, 0x18, 0x35, 0xd9, + 0x2c, 0xc7, 0x0f, 0x12, 0xbd, 0xf0, 0x3e, 0x46, 0x74, 0xf6, 0x53, 0xad, 0xb0, 0x74, 0x92, 0xcd, + 0xb0, 0x0a, 0x52, 0x85, 0x05, 0xd9, 0xde, 0x76, 0x55, 0x65, 0x1d, 0x7a, 0x96, 0xfb, 0x3d, 0xf2, + 0x78, 0xb2, 0x47, 0xfe, 0x53, 0x03, 0x7d, 0x10, 0xa2, 0xb4, 0x11, 0x98, 0x12, 0x17, 0x4e, 0x70, + 0x14, 0xd5, 0x26, 0x66, 0x23, 0x0b, 0x26, 0x43, 0x11, 0xe5, 0x08, 0x0a, 0x8d, 0x44, 0x1b, 0xef, + 0xc2, 0x5c, 0xbc, 0x4e, 0x79, 0xc7, 0x1d, 0x36, 0x55, 0x4f, 0xe1, 0xf4, 0x7e, 0x82, 0xca, 0x53, + 0x7f, 0x01, 0xda, 0x91, 0x2d, 0xe0, 0xfa, 0x5f, 0x33, 0x70, 0x9c, 0xc7, 0x47, 0x2d, 0x98, 0x14, + 0x1f, 0x64, 0xe8, 0x5c, 0xfa, 0xac, 0x24, 0xbe, 0xf0, 0x0a, 0x2b, 0xaf, 0x9c, 0x8e, 0xe5, 0x1b, + 0xe5, 0xcf, 0x7f, 0xfc, 0xe3, 0x9b, 0xf1, 0x02, 0xd2, 0xcd, 0xd4, 0x67, 0xa8, 0xf8, 0xd4, 0x43, + 0xdf, 0x69, 0xb0, 0x30, 0xf8, 0x35, 0x87, 0x2e, 0x0f, 0xa1, 0x0f, 0x1a, 0x16, 0xcc, 0x11, 0x0d, + 0x95, 0xa0, 0xff, 0x70, 0x41, 0x2b, 0xe8, 0x42, 0x5a, 0x10, 0x53, 0x3e, 0x75, 0x91, 0x17, 0xf4, + 0xa5, 0x06, 0xf9, 0xfd, 0x5f, 0x83, 0x17, 0x87, 0xc4, 0xdb, 0x67, 0x55, 0xf8, 0xef, 0x28, 0x56, + 0x4a, 0xd2, 0x2a, 0x97, 0x64, 0xa0, 0x72, 0x5a, 0x92, 0xc7, 0x1d, 0xea, 0x81, 0x8c, 0xfe, 0xad, + 0x06, 0xf3, 0x83, 0x57, 0xfe, 0xa5, 0x21, 0xb1, 0x06, 0xec, 0x0a, 0x95, 0xd1, 0xec, 0x94, 0xaa, + 0xab, 0x5c, 0xd5, 0x45, 0x64, 0xa4, 0x55, 0x61, 0xe1, 0x52, 0x6f, 0xc4, 0x1a, 0xbe, 0xd6, 0x60, + 0x6e, 0xe0, 0xe2, 0x5b, 0x79, 0x75, 0xb8, 0x38, 0x53, 0x6b, 0x23, 0x99, 0x29, 0x51, 0x57, 0xb8, + 0xa8, 0x0b, 0xe8, 0xfc, 0x70, 0x51, 0x71, 0xae, 0xbe, 0xd7, 0x00, 0xa5, 0xeb, 0x2b, 0xba, 0x32, + 0x24, 0x60, 0xda, 0xb4, 0xb0, 0x3e, 0xb2, 0xa9, 0xd2, 0xb7, 0xc6, 0xf5, 0x5d, 0x46, 0x2b, 0x69, + 0x7d, 0xfb, 0x2e, 0x1c, 0x29, 0xa6, 0x07, 0xd3, 0x71, 0xd1, 0x46, 0xa5, 0x21, 0xd1, 0x62, 0x83, + 0xc2, 0xe5, 0xd7, 0x18, 0x28, 0x11, 0x17, 0xb8, 0x88, 0x73, 0xe8, 0x6c, 0x5a, 0x44, 0x03, 0xdb, + 0x75, 0x9b, 0x87, 0xfb, 0x42, 0x83, 0x5c, 0xb2, 0xb8, 0x1b, 0x43, 0x8f, 0xac, 0xb2, 0x29, 0x5c, + 0x7d, 0xbd, 0x8d, 0x12, 0x71, 0x89, 0x8b, 0x28, 0xa3, 0xe2, 0x41, 0x87, 0xba, 0xab, 0xfa, 0x7e, + 0xf4, 0x14, 0x66, 0xfa, 0x65, 0xb3, 0x3c, 0x3c, 0x80, 0xb0, 0x28, 0xac, 0xbe, 0xce, 0x42, 0x09, + 0xb8, 0xc8, 0x05, 0x14, 0xd1, 0xf2, 0xc1, 0x02, 0x44, 0x3b, 0x50, 0xbd, 0xb3, 0xfb, 0x7b, 0x71, + 0x6c, 0xf7, 0x79, 0x51, 0x7b, 0xf6, 0xbc, 0xa8, 0xfd, 0xf6, 0xbc, 0xa8, 0x7d, 0xf5, 0xa2, 0x38, + 0xf6, 0xec, 0x45, 0x71, 0xec, 0xa7, 0x17, 0xc5, 0xb1, 0x47, 0xd7, 0x12, 0x95, 0x34, 0xa2, 0xac, + 0xf9, 0x24, 0xdc, 0xa1, 0x6c, 0x5b, 0x20, 0x3b, 0xff, 0x37, 0xbb, 0x7d, 0x2e, 0xaf, 0xab, 0x8d, + 0x49, 0xfe, 0xdf, 0xb1, 0xff, 0xfd, 0x13, 0x00, 0x00, 0xff, 0xff, 0x73, 0x45, 0xf1, 0xf0, 0xe4, + 0x13, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -1803,16 +1812,18 @@ func (m *QueryAccountSummaryResponse) MarshalToSizedBuffer(dAtA []byte) (int, er _ = i var l int _ = l - { - size := m.LiquidationThreshold.Size() - i -= size - if _, err := m.LiquidationThreshold.MarshalTo(dAtA[i:]); err != nil { - return 0, err + if m.LiquidationThreshold != nil { + { + size := m.LiquidationThreshold.Size() + i -= size + if _, err := m.LiquidationThreshold.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintQuery(dAtA, i, uint64(size)) } - i = encodeVarintQuery(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x2a } - i-- - dAtA[i] = 0x2a { size := m.BorrowLimit.Size() i -= size @@ -2326,8 +2337,10 @@ func (m *QueryAccountSummaryResponse) Size() (n int) { n += 1 + l + sovQuery(uint64(l)) l = m.BorrowLimit.Size() n += 1 + l + sovQuery(uint64(l)) - l = m.LiquidationThreshold.Size() - n += 1 + l + sovQuery(uint64(l)) + if m.LiquidationThreshold != nil { + l = m.LiquidationThreshold.Size() + n += 1 + l + sovQuery(uint64(l)) + } return n } @@ -4030,6 +4043,8 @@ func (m *QueryAccountSummaryResponse) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } + var v github_com_cosmos_cosmos_sdk_types.Dec + m.LiquidationThreshold = &v if err := m.LiquidationThreshold.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } From 15ec06ff6a064912aa48fb59b2c7be5651bf032d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:54:12 -0700 Subject: [PATCH 02/17] cl++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f7763715..20a9db7b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1812](https://github.com/umee-network/umee/pull/1812) MaxCollateralShare now works during partial oracle outages when certain conditions are safe. - [1736](https://github.com/umee-network/umee/pull/1736) Blacklisted tokens no longer add themselves back to the oracle accept list. - [1807](https://github.com/umee-network/umee/pull/1807) Fixes BNB ibc denom in 4.1 migration +- [1821](https://github.com/umee-network/umee/pull/1821) Allow safe leverage operations during partial oracle outages. ## [v4.0.1](https://github.com/umee-network/umee/releases/tag/v4.0.1) - 2023-02-10 From 2f3ec1166c38b6560963d94ca889319ddded13de Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:05:24 -0700 Subject: [PATCH 03/17] typo --- x/leverage/keeper/iter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/iter.go b/x/leverage/keeper/iter.go index 80679240a3..01b450bec5 100644 --- a/x/leverage/keeper/iter.go +++ b/x/leverage/keeper/iter.go @@ -172,7 +172,7 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress borrowValue := k.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) // compute liquidation threshold from enabled collateral - // in this case, we can't reasonable skil missing prices but can move on + // in this case, we can't reasonably skip missing prices but can move on // to the next borrower instead of stopping the entire query liquidationLimit, err := k.CalculateLiquidationThreshold(ctx, collateral) if err == nil && liquidationLimit.LT(borrowValue) { From 3f9d4f272811a0c675901ffd96fb8a3b5b763227 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:09:52 -0700 Subject: [PATCH 04/17] typo --- x/leverage/keeper/msg_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index b441952c97..c5c780eba0 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -343,7 +343,7 @@ func (s msgServer) MaxBorrow( } // If a price is missing for the borrower's collateral, - // but not this uToken or any of their borrows, error + // but not this token or any of their borrows, error // will be nil and the resulting value will be what // can safely be withdrawn even with missing prices. maxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom) From a5d9bd88d349e59a1cc7b5207c4908d1be1f286c Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:10:28 -0700 Subject: [PATCH 05/17] typo --- x/leverage/keeper/msg_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index c5c780eba0..6db4c15509 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -345,7 +345,7 @@ func (s msgServer) MaxBorrow( // If a price is missing for the borrower's collateral, // but not this token or any of their borrows, error // will be nil and the resulting value will be what - // can safely be withdrawn even with missing prices. + // can safely be borrowed even with missing prices. maxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom) if err != nil { return nil, err From e40ef37172cfc052d56369767abf74fcf8f76095 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:45:36 -0700 Subject: [PATCH 06/17] improve error checking --- x/leverage/keeper/borrows.go | 9 ++++---- x/leverage/keeper/collateral.go | 16 +++++--------- x/leverage/keeper/errors.go | 29 ++++++++++++++++++++++++ x/leverage/keeper/errors_test.go | 38 ++++++++++++++++++++++++++++++++ x/leverage/keeper/grpc_query.go | 20 +++++++++++++---- x/leverage/keeper/iter.go | 9 +++++++- x/leverage/keeper/liquidate.go | 5 ++++- x/leverage/keeper/oracle.go | 7 ++++-- 8 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 x/leverage/keeper/errors.go create mode 100644 x/leverage/keeper/errors_test.go diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index d7e43817b7..f980d403b3 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -24,7 +24,6 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres } limit, err := k.VisibleBorrowLimit(ctx, collateral) if err != nil { - // This error is not a missing price return err } if value.GT(limit) { @@ -156,11 +155,13 @@ func (k Keeper) VisibleBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk.D // get USD value of base assets using the chosen price mode v, err := k.TokenValue(ctx, baseAsset, types.PriceModeLow) if err == nil { - // if both spot and historic (if requeired) prices exist, - // add each collateral coin's weighted value to borrow limit + // if both spot and historic (if required) prices exist, + // add collateral coin's weighted value to borrow limit limit = limit.Add(v.Mul(ts.CollateralWeight)) } - + if nonOracleError(err) { + return sdk.ZeroDec(), err + } } } diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index efe78e335a..abb6d5fe1c 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -94,17 +94,13 @@ func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (s // get USD value of base assets v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot) - if err != nil { - k.Logger(ctx).Info( - "collateral value skipped", - "uToken", coin.String(), - "error", err.Error(), - ) - continue + if err == nil { + // for coins that did not error, add their value to the total + total = total.Add(v) + } + if nonOracleError(err) { + return sdk.ZeroDec(), err } - - // for coins that did not error, add their value to the total - total = total.Add(v) } return total, nil diff --git a/x/leverage/keeper/errors.go b/x/leverage/keeper/errors.go new file mode 100644 index 0000000000..aca964b5f0 --- /dev/null +++ b/x/leverage/keeper/errors.go @@ -0,0 +1,29 @@ +package keeper + +import ( + "strings" + + "cosmossdk.io/errors" + + "github.com/umee-network/umee/v4/util/decmath" + leveragetypes "github.com/umee-network/umee/v4/x/leverage/types" + oracletypes "github.com/umee-network/umee/v4/x/oracle/types" +) + +// nonOracleError returns true if an error is non-nil +// and also not one of ErrEmptyList, ErrUnknownDenom, or ErrNoHistoricMedians +// which are errors which can result from missing prices +func nonOracleError(err error) bool { + if err == nil { + return false + } + // check typed errors + if errors.IsOf(err, leveragetypes.ErrNoHistoricMedians, oracletypes.ErrUnknownDenom) { + return false + } + // this error needs to be checked by string comparison + if strings.Contains(err.Error(), decmath.ErrEmptyList.Error()) { + return false + } + return true +} diff --git a/x/leverage/keeper/errors_test.go b/x/leverage/keeper/errors_test.go new file mode 100644 index 0000000000..be77f6c82c --- /dev/null +++ b/x/leverage/keeper/errors_test.go @@ -0,0 +1,38 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/errors" + + "github.com/umee-network/umee/v4/util/decmath" + leveragetypes "github.com/umee-network/umee/v4/x/leverage/types" + oracletypes "github.com/umee-network/umee/v4/x/oracle/types" +) + +func TestErrorMatching(t *testing.T) { + // oracle errors + err1 := errors.Wrap(decmath.ErrEmptyList, "denom: UMEE") + err2 := oracletypes.ErrUnknownDenom.Wrap("UMEE") + err3 := leveragetypes.ErrNoHistoricMedians.Wrapf( + "requested %d, got %d", + 16, + 12, + ) + // not oracle errors + err4 := leveragetypes.ErrBlacklisted + err5 := leveragetypes.ErrUToken + err6 := leveragetypes.ErrNotRegisteredToken + err7 := errors.New("foo", 1, "bar") + + require.Equal(t, false, nonOracleError(nil)) + require.Equal(t, false, nonOracleError(err1)) + require.Equal(t, false, nonOracleError(err2)) + require.Equal(t, false, nonOracleError(err3)) + require.Equal(t, true, nonOracleError(err4)) + require.Equal(t, true, nonOracleError(err5)) + require.Equal(t, true, nonOracleError(err6)) + require.Equal(t, true, nonOracleError(err7)) +} diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index 272efd0576..24de6ace8c 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -209,17 +209,22 @@ func (q Querier) AccountSummary( borrowed := q.Keeper.GetBorrowerBorrows(ctx, addr) // supplied value always uses spot prices, and skips supplied assets that are missing prices - suppliedValue := q.Keeper.VisibleTokenValue(ctx, supplied, types.PriceModeSpot) + suppliedValue, err := q.Keeper.VisibleTokenValue(ctx, supplied, types.PriceModeSpot) + if err != nil { + return nil, err + } // borrowed value uses spot prices here, but leverage logic instead uses // the higher of spot or historic prices for each borrowed token when comparing it // to borrow limit. This line also skips borrowed assets that are missing prices. - borrowedValue := q.Keeper.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) + borrowedValue, err := q.Keeper.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) + if err != nil { + return nil, err + } // collateral value always uses spot prices, and this line skips assets that are missing prices collateralValue, err := q.Keeper.VisibleCollateralValue(ctx, collateral) if err != nil { - // this error isn't a missing price - it would be non-uToken collateral return nil, err } @@ -228,7 +233,6 @@ func (q Querier) AccountSummary( // skips collateral tokens with missing oracle prices borrowLimit, err := q.Keeper.VisibleBorrowLimit(ctx, collateral) if err != nil { - // this error isn't a missing price - it would be non-uToken collateral return nil, err } @@ -337,6 +341,10 @@ func (q Querier) MaxWithdraw( maxUTokens = maxUTokens.Add(uToken) maxTokens = maxTokens.Add(token) } + // Non-price errors will cause the query itself to fail + if nonOracleError(err) { + return nil, err + } } return &types.QueryMaxWithdrawResponse{ @@ -388,6 +396,10 @@ func (q Querier) MaxBorrow( if err == nil && maxBorrow.IsPositive() { maxTokens = maxTokens.Add(maxBorrow) } + // Non-price errors will cause the query itself to fail + if nonOracleError(err) { + return nil, err + } } return &types.QueryMaxBorrowResponse{ diff --git a/x/leverage/keeper/iter.go b/x/leverage/keeper/iter.go index 01b450bec5..b20fe48ebb 100644 --- a/x/leverage/keeper/iter.go +++ b/x/leverage/keeper/iter.go @@ -169,7 +169,10 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress // use oracle helper functions to find total borrowed value in USD // skips denoms without prices - borrowValue := k.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) + borrowValue, err := k.VisibleTokenValue(ctx, borrowed, types.PriceModeSpot) + if err != nil { + return err + } // compute liquidation threshold from enabled collateral // in this case, we can't reasonably skip missing prices but can move on @@ -180,6 +183,10 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress // address is eligible for liquidation. liquidationTargets = append(liquidationTargets, addr) } + // Non-price errors will cause the query itself to fail + if nonOracleError(err) { + return err + } return nil } diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 41ec1c2d9c..712af4d07f 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -29,7 +29,10 @@ func (k Keeper) getLiquidationAmounts( // calculate borrower health in USD values, using spot prices only (no historic) // borrowed value will skip borrowed tokens with unknown oracle prices, treating them as zero value - borrowedValue := k.VisibleTokenValue(ctx, totalBorrowed, types.PriceModeSpot) + borrowedValue, err := k.VisibleTokenValue(ctx, totalBorrowed, types.PriceModeSpot) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } collateralValue, err := k.CalculateCollateralValue(ctx, borrowerCollateral) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index ae9f31d2c8..492140b91d 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -121,7 +121,7 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.Pri // VisibleTokenValue functions like TotalTokenValue, but interprets missing oracle prices // as zero value instead of returning an error. -func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) sdk.Dec { +func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.PriceMode) (sdk.Dec, error) { total := sdk.ZeroDec() accepted := k.filterAcceptedCoins(ctx, coins) @@ -131,9 +131,12 @@ func (k Keeper) VisibleTokenValue(ctx sdk.Context, coins sdk.Coins, mode types.P if err == nil { total = total.Add(v) } + if nonOracleError(err) { + return sdk.ZeroDec(), err + } } - return total + return total, nil } // TokenWithValue creates a token of a given denom with an given USD value. From 4f335f9c2121600f6f3a8e97f7b7537bd881598d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:58:44 -0700 Subject: [PATCH 07/17] improve error mimic or mock oracle --- x/leverage/keeper/oracle_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index e24a2ab148..c80f8e9368 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "fmt" "strings" sdk "github.com/cosmos/cosmos-sdk/types" @@ -9,6 +8,7 @@ import ( appparams "github.com/umee-network/umee/v4/app/params" "github.com/umee-network/umee/v4/util/coin" "github.com/umee-network/umee/v4/x/leverage/types" + oracletypes "github.com/umee-network/umee/v4/x/oracle/types" ) type mockOracleKeeper struct { @@ -32,16 +32,22 @@ func (m *mockOracleKeeper) MedianOfHistoricMedians(ctx sdk.Context, denom string ) (sdk.Dec, uint32, error) { p, ok := m.historicExchangeRates[denom] if !ok { - return sdk.ZeroDec(), 0, fmt.Errorf("invalid denom: %s", denom) + // This error matches oracle behavior on zero historic medians + return sdk.ZeroDec(), 0, types.ErrNoHistoricMedians.Wrapf( + "requested %d, got %d", + numStamps, + 0, + ) } - return p, 24, nil + return p, uint32(numStamps), nil } func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec, error) { p, ok := m.symbolExchangeRates[denom] if !ok { - return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom) + // This error matches oracle behavior on missing asset price + return sdk.ZeroDec(), oracletypes.ErrUnknownDenom.Wrap(denom) } return p, nil From c17acb61091baed11fd7172991af1077a8a921c9 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 08:18:28 -0700 Subject: [PATCH 08/17] initial test cases - some failing --- x/leverage/keeper/outage_test.go | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 x/leverage/keeper/outage_test.go diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go new file mode 100644 index 0000000000..ed5f4b2742 --- /dev/null +++ b/x/leverage/keeper/outage_test.go @@ -0,0 +1,150 @@ +package keeper_test + +import ( + "github.com/umee-network/umee/v4/util/coin" + "github.com/umee-network/umee/v4/x/leverage/types" +) + +// TestBorrowedPriceOutage tests price outage scenarios where a borrowed token +// has unknown price +func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { + ctx, srv, require := s.ctx, s.msgSrvr, s.Require() + + // create an ATOM supplier + atomSupplier := s.newAccount(coin.New(atomDenom, 100_000000)) + s.supply(atomSupplier, coin.New(atomDenom, 100_000000)) + + // create a supplier to supply 150 UMEE, and collateralize 100 UMEE + umeeSupplier := s.newAccount(coin.New(umeeDenom, 200_000000)) + s.supply(umeeSupplier, coin.New(umeeDenom, 150_000000)) + s.collateralize(umeeSupplier, coin.New("u/"+umeeDenom, 100_000000)) + // additionally borrow 0.000001 ATOM and 0.000001 UMEE with the UMEE supplier + s.borrow(umeeSupplier, coin.New(atomDenom, 1)) + s.borrow(umeeSupplier, coin.New(umeeDenom, 1)) + + // Create an ATOM price outage + s.mockOracle.Clear("ATOM") + + // UMEE can still be supplied + msg1 := &types.MsgSupply{ + Supplier: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err := srv.Supply(ctx, msg1) + require.NoError(err, "supply umee") + + // Non-collateral UMEE can still be withdrawn + msg2 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Withdraw(ctx, msg2) + require.NoError(err, "withdraw non-collateral umee") + + // Non-collateral UMEE can still be withdrawn using max withdraw + msg3 := &types.MsgMaxWithdraw{ + Supplier: umeeSupplier.String(), + Denom: "u/" + umeeDenom, + } + _, err = srv.MaxWithdraw(ctx, msg3) + require.NoError(err, "max withdraw non-collateral umee") + + // Collateral UMEE cannot be withdrawn since borrowed ATOM value is unknown + msg4 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 1), + } + _, err = srv.Withdraw(ctx, msg4) + require.ErrorIs(err, types.ErrUndercollaterized, "withdraw collateral umee") + + // UMEE can still be collateralized + s.supply(umeeSupplier, coin.New(umeeDenom, 50_000000)) + msg5 := &types.MsgCollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Collateralize(ctx, msg5) + require.NoError(err, "collateralize umee") + + // SupplyCollateral still works for UMEE + msg6 := &types.MsgSupplyCollateral{ + Supplier: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err = srv.SupplyCollateral(ctx, msg6) + require.NoError(err, "supply+collateralize umee") + + // Collateral UMEE cannot be decollateralized since borrowed ATOM value is unknown + msg7 := &types.MsgDecollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 1), + } + _, err = srv.Decollateralize(ctx, msg7) + require.ErrorIs(err, types.ErrUndercollaterized, "decollateralize collateral umee") + + // UMEE cannot be borrowed since ATOM borrowed value is unknown + msg8 := &types.MsgBorrow{ + Borrower: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 1), + } + _, err = srv.Borrow(ctx, msg8) + require.ErrorIs(err, types.ErrUndercollaterized, "borrow umee") + + // UMEE max-borrow succeeds with amount = zero since UMEE cannot be borrowed + msg9 := &types.MsgMaxBorrow{ + Borrower: umeeSupplier.String(), + Denom: umeeDenom, + } + resp9, err := srv.MaxBorrow(ctx, msg9) + require.NoError(err, "max-borrow umee") + require.Equal(int64(0), resp9.Borrowed.Amount.Int64(), "max borrow umee") + + // UMEE repay succeeds + msg10 := &types.MsgRepay{ + Borrower: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 1), + } + _, err = srv.Repay(ctx, msg10) + require.NoError(err, "repay umee") + + // Liquidation is ineligible because known borrowed value does not exceed borrow limit + msg11 := &types.MsgLiquidate{ + Liquidator: umeeSupplier.String(), + Borrower: umeeSupplier.String(), + Repayment: coin.New(umeeDenom, 1), + RewardDenom: umeeDenom, + } + _, err = srv.Liquidate(ctx, msg11) + require.ErrorIs(err, types.ErrLiquidationIneligible, "liquidate umee") + + // ATOM repay succeeds + msg12 := &types.MsgRepay{ + Borrower: umeeSupplier.String(), + Asset: coin.New(atomDenom, 1), + } + _, err = srv.Repay(ctx, msg12) + require.NoError(err, "repay atom") +} + +// TestCollateralPriceOutage tests price outage scenarios where a collateral token +// has unknown price +func (s *IntegrationTestSuite) TestCollateralPriceOutage() { + // ctx, srv, require := s.ctx, s.msgSrvr, s.Require() + + // create a supplier + umeeSupplier := s.newAccount(coin.New(umeeDenom, 200_000000)) + s.supply(umeeSupplier, coin.New(umeeDenom, 200_000000)) + + // create an ATOM supplier and borrow 0.000001 UMEE + atomSupplier := s.newAccount(coin.New(atomDenom, 100_000000)) + s.supply(atomSupplier, coin.New(atomDenom, 75_000000)) + s.collateralize(atomSupplier, coin.New("u/"+atomDenom, 50_000000)) + s.borrow(atomSupplier, coin.New(umeeDenom, 1)) + + // Create an ATOM price outage + s.mockOracle.Clear("ATOM") + + // TODO: Test every message type +} + +// TODO: Test complex (3-asset) scenarios From 1370b4f1bbd5056bd2cd2d73e22a52aa6b31e3f5 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 08:29:13 -0700 Subject: [PATCH 09/17] when withdrawing non-collateral only, skip borrower health check --- x/leverage/keeper/keeper.go | 28 +++++++++++++++++----------- x/leverage/keeper/msg_server.go | 20 ++++++++++++-------- x/leverage/keeper/outage_test.go | 2 +- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 1a91920917..ef6090e795 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -111,21 +111,24 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co // the amount requested, returns an error. Returns the amount of base tokens received. // This function does NOT check that a borrower remains under their borrow limit or that // collateral liquidity remains healthy - those assertions have been moved to MsgServer. -func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, error) { +// Returns a boolean which is true if some or all of the withdrawn uTokens were from collateral. +func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, bool, error) { + isFromCollateral := false + if err := validateUToken(uToken); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } // calculate base asset amount to withdraw token, err := k.ExchangeUToken(ctx, uToken) if err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } // Ensure module account has sufficient unreserved tokens to withdraw availableAmount := k.AvailableLiquidity(ctx, token.Denom) if token.Amount.GT(availableAmount) { - return sdk.Coin{}, types.ErrLendingPoolInsufficient.Wrap(token.String()) + return sdk.Coin{}, isFromCollateral, types.ErrLendingPoolInsufficient.Wrap(token.String()) } // Withdraw will first attempt to use any uTokens in the supplier's wallet @@ -134,11 +137,14 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd amountFromCollateral := uToken.Amount.Sub(amountFromWallet) if amountFromCollateral.IsPositive() { + // This indicates that borrower health check cannot be skipped after MsgWithdraw + isFromCollateral = true + // Check for sufficient collateral collateral := k.GetBorrowerCollateral(ctx, supplierAddr) collateralAmount := collateral.AmountOf(uToken.Denom) if collateral.AmountOf(uToken.Denom).LT(amountFromCollateral) { - return sdk.Coin{}, types.ErrInsufficientBalance.Wrapf( + return sdk.Coin{}, isFromCollateral, types.ErrInsufficientBalance.Wrapf( "%s uToken balance + %s from collateral is less than %s to withdraw", amountFromWallet, collateralAmount, uToken) } @@ -146,31 +152,31 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sd // reduce the supplier's collateral by amountFromCollateral newCollateral := sdk.NewCoin(uToken.Denom, collateralAmount.Sub(amountFromCollateral)) if err = k.setCollateral(ctx, supplierAddr, newCollateral); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } } // transfer amountFromWallet uTokens to the module account uTokens := sdk.NewCoins(sdk.NewCoin(uToken.Denom, amountFromWallet)) if err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, supplierAddr, types.ModuleName, uTokens); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } // send the base assets to supplier tokens := sdk.NewCoins(token) if err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, supplierAddr, tokens); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } // burn the uTokens and set the new total uToken supply if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(uToken)); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, uToken.Denom).Sub(uToken)); err != nil { - return sdk.Coin{}, err + return sdk.Coin{}, isFromCollateral, err } - return token, nil + return token, isFromCollateral, nil } // Borrow attempts to borrow tokens from the leverage module account using diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 8a3c53d837..a05af590e0 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -68,16 +68,18 @@ func (s msgServer) Withdraw( if err != nil { return nil, err } - received, err := s.keeper.Withdraw(ctx, supplierAddr, msg.Asset) + received, isFromCollateral, err := s.keeper.Withdraw(ctx, supplierAddr, msg.Asset) if err != nil { return nil, err } // Fail here if supplier ends up over their borrow limit under current or historic prices // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows - err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) - if err != nil { - return nil, err + if isFromCollateral { + err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) + if err != nil { + return nil, err + } } // Ensure MinCollateralLiquidity is still satisfied after the transaction @@ -122,16 +124,18 @@ func (s msgServer) MaxWithdraw( return &types.MsgMaxWithdrawResponse{Withdrawn: uToken, Received: zeroCoin}, nil } - received, err := s.keeper.Withdraw(ctx, supplierAddr, uToken) + received, isFromCollateral, err := s.keeper.Withdraw(ctx, supplierAddr, uToken) if err != nil { return nil, err } // Fail here if supplier ends up over their borrow limit under current or historic prices // Tolerates missing collateral prices if the rest of the borrower's collateral can cover all borrows - err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) - if err != nil { - return nil, err + if isFromCollateral { + err = s.keeper.assertBorrowerHealth(ctx, supplierAddr) + if err != nil { + return nil, err + } } // Ensure MinCollateralLiquidity is still satisfied after the transaction diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index ed5f4b2742..7734a35677 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -44,7 +44,7 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { // Non-collateral UMEE can still be withdrawn using max withdraw msg3 := &types.MsgMaxWithdraw{ Supplier: umeeSupplier.String(), - Denom: "u/" + umeeDenom, + Denom: umeeDenom, } _, err = srv.MaxWithdraw(ctx, msg3) require.NoError(err, "max withdraw non-collateral umee") From 1ea883f8f535fb5c62c5df7f8cdad982ca42877d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:04:21 -0700 Subject: [PATCH 10/17] on unknown borrow price, still allow withdrawal of non-collateral uTokens --- x/leverage/keeper/limits.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index 7ed73cf2fe..b72e30cf29 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -25,9 +25,16 @@ func (k *Keeper) maxWithdraw(ctx sdk.Context, addr sdk.AccAddress, denom string) // calculate borrowed value for the account, using the higher of spot or historic prices for each token borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, types.PriceModeHigh) - if err != nil { + if nonOracleError(err) { + // for errors besides a missing price, the whole transaction fails return sdk.Coin{}, err } + if err != nil { + // for missing prices on borrowed assets, we can't withdraw any collateral + // but can withdraw non-collateral uTokens + withdrawAmount := sdk.MinInt(walletUtokens, availableUTokens.Amount) + return sdk.NewCoin(uDenom, withdrawAmount), nil + } // if no non-blacklisted tokens are borrowed, withdraw the maximum available amount if borrowedValue.IsZero() { From e06778c5c628eadf651a6b653f628f43ecc64783 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:40:29 -0700 Subject: [PATCH 11/17] fix some error types in tests --- x/leverage/keeper/outage_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index 7734a35677..a58cc4434d 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "github.com/umee-network/umee/v4/util/coin" "github.com/umee-network/umee/v4/x/leverage/types" + oracletypes "github.com/umee-network/umee/v4/x/oracle/types" ) // TestBorrowedPriceOutage tests price outage scenarios where a borrowed token @@ -55,7 +56,7 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { Asset: coin.New("u/"+umeeDenom, 1), } _, err = srv.Withdraw(ctx, msg4) - require.ErrorIs(err, types.ErrUndercollaterized, "withdraw collateral umee") + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "withdraw collateral umee") // UMEE can still be collateralized s.supply(umeeSupplier, coin.New(umeeDenom, 50_000000)) @@ -80,7 +81,7 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { Asset: coin.New("u/"+umeeDenom, 1), } _, err = srv.Decollateralize(ctx, msg7) - require.ErrorIs(err, types.ErrUndercollaterized, "decollateralize collateral umee") + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "decollateralize collateral umee") // UMEE cannot be borrowed since ATOM borrowed value is unknown msg8 := &types.MsgBorrow{ @@ -88,7 +89,7 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { Asset: coin.New(umeeDenom, 1), } _, err = srv.Borrow(ctx, msg8) - require.ErrorIs(err, types.ErrUndercollaterized, "borrow umee") + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "borrow umee") // UMEE max-borrow succeeds with amount = zero since UMEE cannot be borrowed msg9 := &types.MsgMaxBorrow{ From 5c477f9c874b813fb0dcb857565d96a41ed1a1ba Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 10:01:13 -0700 Subject: [PATCH 12/17] maxBorrow now correctly returns zero on missing prices --- x/leverage/keeper/limits.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/limits.go b/x/leverage/keeper/limits.go index b72e30cf29..96f2799978 100644 --- a/x/leverage/keeper/limits.go +++ b/x/leverage/keeper/limits.go @@ -107,9 +107,14 @@ func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) ( // calculate borrowed value for the account, using the higher of spot or historic prices borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed, types.PriceModeHigh) - if err != nil { + if nonOracleError(err) { + // non-oracle errors fail the transaction (or query) return sdk.Coin{}, err } + if err != nil { + // oracle errors cause max borrow to be zero + return sdk.NewCoin(denom, sdk.ZeroInt()), nil + } // calculate borrow limit for the account, using only collateral whose price is known borrowLimit, err := k.VisibleBorrowLimit(ctx, totalCollateral) @@ -126,9 +131,14 @@ func (k *Keeper) maxBorrow(ctx sdk.Context, addr sdk.AccAddress, denom string) ( // determine max borrow, using the higher of spot or historic prices for the token to borrow maxBorrow, err := k.TokenWithValue(ctx, denom, unusedBorrowLimit, types.PriceModeHigh) - if err != nil { + if nonOracleError(err) { + // non-oracle errors fail the transaction (or query) return sdk.Coin{}, err } + if err != nil { + // oracle errors cause max borrow to be zero + return sdk.NewCoin(denom, sdk.ZeroInt()), nil + } // also cap borrow amount at available liquidity maxBorrow.Amount = sdk.MinInt(maxBorrow.Amount, availableTokens) From 3a1538a4cfeca4483037d710a242f68c14b1d639 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 11:44:11 -0700 Subject: [PATCH 13/17] tests where collateral price is unknown --- x/leverage/keeper/outage_test.go | 113 ++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index a58cc4434d..5b33b46779 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -130,22 +130,113 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { // TestCollateralPriceOutage tests price outage scenarios where a collateral token // has unknown price func (s *IntegrationTestSuite) TestCollateralPriceOutage() { - // ctx, srv, require := s.ctx, s.msgSrvr, s.Require() + ctx, srv, require := s.ctx, s.msgSrvr, s.Require() + + // create an ATOM supplier + atomSupplier := s.newAccount(coin.New(atomDenom, 100_000000)) + s.supply(atomSupplier, coin.New(atomDenom, 100_000000)) - // create a supplier + // create a supplier to supply 150 UMEE, and collateralize 100 UMEE umeeSupplier := s.newAccount(coin.New(umeeDenom, 200_000000)) - s.supply(umeeSupplier, coin.New(umeeDenom, 200_000000)) + s.supply(umeeSupplier, coin.New(umeeDenom, 150_000000)) + s.collateralize(umeeSupplier, coin.New("u/"+umeeDenom, 100_000000)) + // additionally borrow 0.000001 ATOM + s.borrow(umeeSupplier, coin.New(atomDenom, 1)) - // create an ATOM supplier and borrow 0.000001 UMEE - atomSupplier := s.newAccount(coin.New(atomDenom, 100_000000)) - s.supply(atomSupplier, coin.New(atomDenom, 75_000000)) - s.collateralize(atomSupplier, coin.New("u/"+atomDenom, 50_000000)) - s.borrow(atomSupplier, coin.New(umeeDenom, 1)) + // Create an UMEE price outage + s.mockOracle.Clear("UMEE") - // Create an ATOM price outage - s.mockOracle.Clear("ATOM") + // UMEE can still be supplied + msg1 := &types.MsgSupply{ + Supplier: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err := srv.Supply(ctx, msg1) + require.NoError(err, "supply umee") + + // Non-collateral UMEE can still be withdrawn + msg2 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Withdraw(ctx, msg2) + require.NoError(err, "withdraw non-collateral umee") + + // Non-collateral UMEE can still be withdrawn using max withdraw + msg3 := &types.MsgMaxWithdraw{ + Supplier: umeeSupplier.String(), + Denom: umeeDenom, + } + _, err = srv.MaxWithdraw(ctx, msg3) + require.NoError(err, "max withdraw non-collateral umee") + + // Collateral UMEE cannot be withdrawn since collateral UMEE value is unknown + msg4 := &types.MsgWithdraw{ + Supplier: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 1), + } + _, err = srv.Withdraw(ctx, msg4) + require.ErrorIs(err, types.ErrUndercollaterized, "withdraw collateral umee") - // TODO: Test every message type + // UMEE can still be collateralized + s.supply(umeeSupplier, coin.New(umeeDenom, 50_000000)) + msg5 := &types.MsgCollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Collateralize(ctx, msg5) + require.NoError(err, "collateralize umee") + + // SupplyCollateral still works for UMEE + msg6 := &types.MsgSupplyCollateral{ + Supplier: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err = srv.SupplyCollateral(ctx, msg6) + require.NoError(err, "supply+collateralize umee") + + // Collateral UMEE cannot be decollateralized since collateral UMEE value is unknown + msg7 := &types.MsgDecollateralize{ + Borrower: umeeSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 1), + } + _, err = srv.Decollateralize(ctx, msg7) + require.ErrorIs(err, types.ErrUndercollaterized, "decollateralize collateral umee") + + // UMEE cannot be borrowed since UMEE value is unknown + msg8 := &types.MsgBorrow{ + Borrower: umeeSupplier.String(), + Asset: coin.New(umeeDenom, 1), + } + _, err = srv.Borrow(ctx, msg8) + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "borrow umee") + + // UMEE max-borrow succeeds with amount = zero since UMEE cannot be borrowed + msg9 := &types.MsgMaxBorrow{ + Borrower: umeeSupplier.String(), + Denom: umeeDenom, + } + resp9, err := srv.MaxBorrow(ctx, msg9) + require.NoError(err, "max-borrow umee") + require.Equal(int64(0), resp9.Borrowed.Amount.Int64(), "max borrow umee") + + // Liquidation fails because collateral value cannot be calculated + msg11 := &types.MsgLiquidate{ + Liquidator: umeeSupplier.String(), + Borrower: umeeSupplier.String(), + Repayment: coin.New(umeeDenom, 1), + RewardDenom: umeeDenom, + } + _, err = srv.Liquidate(ctx, msg11) + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "liquidate umee") + + // ATOM repay succeeds + msg12 := &types.MsgRepay{ + Borrower: umeeSupplier.String(), + Asset: coin.New(atomDenom, 1), + } + _, err = srv.Repay(ctx, msg12) + require.NoError(err, "repay atom") } // TODO: Test complex (3-asset) scenarios From 2b6c162d6d57a2a608956b8eb95571cb55e881f4 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 11:44:58 -0700 Subject: [PATCH 14/17] reset --- x/leverage/keeper/outage_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index 5b33b46779..2677a585a8 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -125,6 +125,8 @@ func (s *IntegrationTestSuite) TestBorrowedPriceOutage() { } _, err = srv.Repay(ctx, msg12) require.NoError(err, "repay atom") + + s.mockOracle.Reset() } // TestCollateralPriceOutage tests price outage scenarios where a collateral token @@ -237,6 +239,8 @@ func (s *IntegrationTestSuite) TestCollateralPriceOutage() { } _, err = srv.Repay(ctx, msg12) require.NoError(err, "repay atom") + + s.mockOracle.Reset() } // TODO: Test complex (3-asset) scenarios From 193ef2add4942fbf6cebbdebdd8eae054ad7a474 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 12:10:49 -0700 Subject: [PATCH 15/17] tests where only some of a user's collateral has unknown value --- x/leverage/keeper/outage_test.go | 184 ++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index 2677a585a8..b04840399f 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -243,4 +243,186 @@ func (s *IntegrationTestSuite) TestCollateralPriceOutage() { s.mockOracle.Reset() } -// TODO: Test complex (3-asset) scenarios +// TestPartialCollateralPriceOutage tests price outage scenarios where two collateral +// tokens are used and only one collateral token has unknown price +func (s *IntegrationTestSuite) TestCollateralPartialPriceOutage() { + app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require() + + // create an ATOM supplier + atomSupplier := s.newAccount(coin.New(atomDenom, 100_000000)) + s.supply(atomSupplier, coin.New(atomDenom, 100_000000)) + + // create a supplier to supply 150 UMEE, and collateralize 100 UMEE + // plus the same amounts of ATOM + bothSupplier := s.newAccount(coin.New(umeeDenom, 200_000000), coin.New(atomDenom, 200_000000)) + s.supply(bothSupplier, coin.New(umeeDenom, 150_000000)) + s.collateralize(bothSupplier, coin.New("u/"+umeeDenom, 100_000000)) + s.supply(bothSupplier, coin.New(atomDenom, 150_000000)) + s.collateralize(bothSupplier, coin.New("u/"+atomDenom, 100_000000)) + // additionally borrow 0.000001 ATOM + s.borrow(bothSupplier, coin.New(atomDenom, 1)) + + // Create an UMEE price outage + s.mockOracle.Clear("UMEE") + + // UMEE can still be supplied + msg1 := &types.MsgSupply{ + Supplier: bothSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err := srv.Supply(ctx, msg1) + require.NoError(err, "supply umee") + + // ATOM can still be supplied + msg2 := &types.MsgSupply{ + Supplier: bothSupplier.String(), + Asset: coin.New(atomDenom, 50_000000), + } + _, err = srv.Supply(ctx, msg2) + require.NoError(err, "supply atom") + + // Non-collateral UMEE can still be withdrawn + msg3 := &types.MsgWithdraw{ + Supplier: bothSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Withdraw(ctx, msg3) + require.NoError(err, "withdraw non-collateral umee") + + // Non-collateral ATOM can still be withdrawn + msg4 := &types.MsgWithdraw{ + Supplier: bothSupplier.String(), + Asset: coin.New("u/"+atomDenom, 50_000000), + } + _, err = srv.Withdraw(ctx, msg4) + require.NoError(err, "withdraw non-collateral atom") + + // Non-collateral and Collateral UMEE can still be withdrawn using max withdraw + msg5 := &types.MsgMaxWithdraw{ + Supplier: bothSupplier.String(), + Denom: umeeDenom, + } + _, err = srv.MaxWithdraw(ctx, msg5) + require.NoError(err, "max withdraw umee") + supplied, err := app.LeverageKeeper.GetSupplied(ctx, bothSupplier, umeeDenom) + require.NoError(err, "measure umee supplied") + require.Equal(int64(0), supplied.Amount.Int64(), "all umee withdrawn by maxwithdraw") + + // Non-collateral ATOM and some collateral ATOM can still be withdrawn using max withdraw + msg6 := &types.MsgMaxWithdraw{ + Supplier: bothSupplier.String(), + Denom: atomDenom, + } + _, err = srv.MaxWithdraw(ctx, msg6) + require.NoError(err, "max withdraw non-collateral atom") + supplied, err = app.LeverageKeeper.GetSupplied(ctx, bothSupplier, atomDenom) + require.NoError(err, "measure atom supplied") + require.Equal(int64(4), supplied.Amount.Int64(), "some atom collateral withdrawn by maxwithdraw") + + // UMEE can still be collateralized + s.supply(bothSupplier, coin.New(umeeDenom, 50_000000)) + msg7 := &types.MsgCollateralize{ + Borrower: bothSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 50_000000), + } + _, err = srv.Collateralize(ctx, msg7) + require.NoError(err, "collateralize umee") + + // ATOM can still be collateralized + s.supply(bothSupplier, coin.New(atomDenom, 50_000000)) + msg8 := &types.MsgCollateralize{ + Borrower: bothSupplier.String(), + Asset: coin.New("u/"+atomDenom, 50_000000), + } + _, err = srv.Collateralize(ctx, msg8) + require.NoError(err, "collateralize atom") + + // SupplyCollateral still works for UMEE + msg9 := &types.MsgSupplyCollateral{ + Supplier: bothSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err = srv.SupplyCollateral(ctx, msg9) + require.NoError(err, "supply+collateralize umee") + + // SupplyCollateral still works for ATOM + msg10 := &types.MsgSupplyCollateral{ + Supplier: bothSupplier.String(), + Asset: coin.New(umeeDenom, 50_000000), + } + _, err = srv.SupplyCollateral(ctx, msg10) + require.NoError(err, "supply+collateralize atom") + + // Collateral UMEE can still decollateralized even though collateral UMEE value is unknown + // because ATOM collateral is sufficient to cover borrows + msg11 := &types.MsgDecollateralize{ + Borrower: bothSupplier.String(), + Asset: coin.New("u/"+umeeDenom, 1), + } + _, err = srv.Decollateralize(ctx, msg11) + require.NoError(err, "decollateralize collateral umee") + + // Collateral ATOM can still decollateralized even though collateral UMEE value is unknown + // because remaining ATOM collateral is sufficient to cover borrows + msg12 := &types.MsgDecollateralize{ + Borrower: bothSupplier.String(), + Asset: coin.New("u/"+atomDenom, 1), + } + _, err = srv.Decollateralize(ctx, msg12) + require.NoError(err, "decollateralize collateral atom") + + // UMEE cannot be borrowed since UMEE value is unknown + msg13 := &types.MsgBorrow{ + Borrower: bothSupplier.String(), + Asset: coin.New(umeeDenom, 1), + } + _, err = srv.Borrow(ctx, msg13) + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "borrow umee") + + // ATOM can be borrowed even though UMEE collateral value is unknown + // because because ATOM collateral is sufficient to cover borrows + msg14 := &types.MsgBorrow{ + Borrower: bothSupplier.String(), + Asset: coin.New(atomDenom, 1), + } + _, err = srv.Borrow(ctx, msg14) + require.NoError(err, "borrow atom") + + // UMEE max-borrow succeeds with amount = zero since UMEE cannot be borrowed + msg15 := &types.MsgMaxBorrow{ + Borrower: bothSupplier.String(), + Denom: umeeDenom, + } + resp15, err := srv.MaxBorrow(ctx, msg15) + require.NoError(err, "max-borrow umee") + require.Equal(int64(0), resp15.Borrowed.Amount.Int64(), "max borrow umee") + + // ATOM max-borrow succeeds with nonzero amountsince ATOM can still borrowed + msg16 := &types.MsgMaxBorrow{ + Borrower: bothSupplier.String(), + Denom: umeeDenom, + } + resp16, err := srv.MaxBorrow(ctx, msg16) + require.NoError(err, "max-borrow atom") + require.Greater(resp16.Borrowed.Amount.Int64(), int64(0), "max borrow atom") + + // Liquidation is ineligible because known collateral covers all borrows + msg17 := &types.MsgLiquidate{ + Liquidator: bothSupplier.String(), + Borrower: bothSupplier.String(), + Repayment: coin.New(atomDenom, 1), + RewardDenom: atomDenom, + } + _, err = srv.Liquidate(ctx, msg17) + require.ErrorIs(err, types.ErrLiquidationIneligible, "liquidate atom") + + // ATOM repay succeeds + msg18 := &types.MsgRepay{ + Borrower: bothSupplier.String(), + Asset: coin.New(atomDenom, 1), + } + _, err = srv.Repay(ctx, msg18) + require.NoError(err, "repay atom") + + s.mockOracle.Reset() +} From 21ff1ba14bd0b9d104cff825f5b0de94e46418c5 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 12:24:18 -0700 Subject: [PATCH 16/17] praise kek --- x/leverage/keeper/outage_test.go | 40 +++++++++++++------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/x/leverage/keeper/outage_test.go b/x/leverage/keeper/outage_test.go index b04840399f..56b4fa7e13 100644 --- a/x/leverage/keeper/outage_test.go +++ b/x/leverage/keeper/outage_test.go @@ -371,57 +371,49 @@ func (s *IntegrationTestSuite) TestCollateralPartialPriceOutage() { _, err = srv.Decollateralize(ctx, msg12) require.NoError(err, "decollateralize collateral atom") - // UMEE cannot be borrowed since UMEE value is unknown - msg13 := &types.MsgBorrow{ - Borrower: bothSupplier.String(), - Asset: coin.New(umeeDenom, 1), - } - _, err = srv.Borrow(ctx, msg13) - require.ErrorIs(err, oracletypes.ErrUnknownDenom, "borrow umee") - // ATOM can be borrowed even though UMEE collateral value is unknown // because because ATOM collateral is sufficient to cover borrows - msg14 := &types.MsgBorrow{ + msg13 := &types.MsgBorrow{ Borrower: bothSupplier.String(), Asset: coin.New(atomDenom, 1), } - _, err = srv.Borrow(ctx, msg14) + _, err = srv.Borrow(ctx, msg13) require.NoError(err, "borrow atom") // UMEE max-borrow succeeds with amount = zero since UMEE cannot be borrowed - msg15 := &types.MsgMaxBorrow{ + msg14 := &types.MsgMaxBorrow{ Borrower: bothSupplier.String(), Denom: umeeDenom, } - resp15, err := srv.MaxBorrow(ctx, msg15) + resp14, err := srv.MaxBorrow(ctx, msg14) require.NoError(err, "max-borrow umee") - require.Equal(int64(0), resp15.Borrowed.Amount.Int64(), "max borrow umee") + require.Equal(int64(0), resp14.Borrowed.Amount.Int64(), "max borrow umee") - // ATOM max-borrow succeeds with nonzero amountsince ATOM can still borrowed - msg16 := &types.MsgMaxBorrow{ + // ATOM max-borrow succeeds with NONZERO amount since ATOM can still borrowed + msg15 := &types.MsgMaxBorrow{ Borrower: bothSupplier.String(), - Denom: umeeDenom, + Denom: atomDenom, } - resp16, err := srv.MaxBorrow(ctx, msg16) + resp15, err := srv.MaxBorrow(ctx, msg15) require.NoError(err, "max-borrow atom") - require.Greater(resp16.Borrowed.Amount.Int64(), int64(0), "max borrow atom") + require.Greater(resp15.Borrowed.Amount.Int64(), int64(0), "max borrow atom") - // Liquidation is ineligible because known collateral covers all borrows - msg17 := &types.MsgLiquidate{ + // Liquidation fails because UMEE collateral value cannot be calculated + msg16 := &types.MsgLiquidate{ Liquidator: bothSupplier.String(), Borrower: bothSupplier.String(), Repayment: coin.New(atomDenom, 1), RewardDenom: atomDenom, } - _, err = srv.Liquidate(ctx, msg17) - require.ErrorIs(err, types.ErrLiquidationIneligible, "liquidate atom") + _, err = srv.Liquidate(ctx, msg16) + require.ErrorIs(err, oracletypes.ErrUnknownDenom, "liquidate atom") // ATOM repay succeeds - msg18 := &types.MsgRepay{ + msg17 := &types.MsgRepay{ Borrower: bothSupplier.String(), Asset: coin.New(atomDenom, 1), } - _, err = srv.Repay(ctx, msg18) + _, err = srv.Repay(ctx, msg17) require.NoError(err, "repay atom") s.mockOracle.Reset() From 4b02569c30481764e803caad20b397c933f8d008 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:14:58 -0700 Subject: [PATCH 17/17] add ErrInvalidOraclePrice to oracle errors list --- x/leverage/keeper/errors.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x/leverage/keeper/errors.go b/x/leverage/keeper/errors.go index aca964b5f0..d7fec3bae6 100644 --- a/x/leverage/keeper/errors.go +++ b/x/leverage/keeper/errors.go @@ -18,7 +18,11 @@ func nonOracleError(err error) bool { return false } // check typed errors - if errors.IsOf(err, leveragetypes.ErrNoHistoricMedians, oracletypes.ErrUnknownDenom) { + if errors.IsOf(err, + leveragetypes.ErrInvalidOraclePrice, + leveragetypes.ErrNoHistoricMedians, + oracletypes.ErrUnknownDenom, + ) { return false } // this error needs to be checked by string comparison