Skip to content

Commit 5372fa4

Browse files
totekimergify[bot]
authored andcommitted
fix!: allow safe leverage operations during partial oracle outages (#1821)
## Description This one does a lot of things: - Allows `account_summary` to work during price outages, treating all unknown prices as zero. - In such cases, supplied value and other fields will appear lower than it really is, since some assets were skipped - Liquidation threshold will be null when it can't be computed, since there's no safe way to do that with missing prices - `borrow_limit` in queries as well as messages is computed using only collateral with known prices - If the portion of your collateral with known prices is enough to cover a borrow, then it still works. - Same for withdraw and decollateralize - `MaxWithdraw` and `MaxBorrow` (both queries and messages) now function with missing collateral prices, respecting the borrow limit policy above. The queries return zero on missing borrow prices, or a missing price for the specific token being asked for. - `liquidation_targets` query skips addresses where liquidation threshold cannot be computed, instead of returning an error for the whole query - `MsgLiquidate` will be able to function with some missing prices on the target's borrowed assets, if their other borrows are high enough to still put them above their liquidation threshold API Breaking: - `liquidation_threshold` field in `account_summary` field can now be null --- ### Author Checklist _All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues._ I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] added `!` to the type prefix if API or client breaking change - [x] added appropriate labels to the PR - [x] targeted the correct branch (see [PR Targeting](https://github.com/umee-network/umee/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [x] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist _All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items._ I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) (cherry picked from commit 87b9ed4) # Conflicts: # proto/umee/leverage/v1/query.proto # swagger/swagger.yaml # x/leverage/types/query.pb.go
1 parent 0682da0 commit 5372fa4

File tree

19 files changed

+866
-141
lines changed

19 files changed

+866
-141
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
5555
- [1812](https://github.com/umee-network/umee/pull/1812) MaxCollateralShare now works during partial oracle outages when certain conditions are safe.
5656
- [1736](https://github.com/umee-network/umee/pull/1736) Blacklisted tokens no longer add themselves back to the oracle accept list.
5757
- [1807](https://github.com/umee-network/umee/pull/1807) Fixes BNB ibc denom in 4.1 migration
58+
- [1821](https://github.com/umee-network/umee/pull/1821) Allow safe leverage operations during partial oracle outages.
5859

5960
## [v4.0.1](https://github.com/umee-network/umee/releases/tag/v4.0.1) - 2023-02-10
6061

proto/umee/leverage/v1/query.proto

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,32 +221,45 @@ message QueryAccountSummary {
221221

222222
// QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler.
223223
message QueryAccountSummaryResponse {
224+
<<<<<<< HEAD
224225
// Supplied Value is the sum of the USD value of all tokens the account has supplied, includng interest earned.
226+
=======
227+
// Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned.
228+
// Computation skips assets which are missing oracle prices, potentially resulting in a lower supplied
229+
// value than if prices were all available.
230+
>>>>>>> 87b9ed4 (fix!: allow safe leverage operations during partial oracle outages (#1821))
225231
string supplied_value = 1 [
226232
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
227233
(gogoproto.nullable) = false
228234
];
229235
// Collateral Value is the sum of the USD value of all uTokens the account has collateralized.
236+
// Computation skips collateral which is missing an oracle price, potentially resulting in a lower collateral
237+
// value than if prices were all available.
230238
string collateral_value = 2 [
231239
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
232240
(gogoproto.nullable) = false
233241
];
234242
// Borrowed Value is the sum of the USD value of all tokens the account has borrowed, including interest owed.
235243
// It always uses spot prices.
244+
// Computation skips borrows which are missing oracle prices, potentially resulting in a lower borrowed
245+
// value than if prices were all available.
236246
string borrowed_value = 3 [
237247
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
238248
(gogoproto.nullable) = false
239249
];
240250
// Borrow Limit is the maximum Borrowed Value the account is allowed to reach through direct borrowing.
241251
// The lower of spot or historic price for each collateral token is used when calculating borrow limits.
252+
// Computation skips collateral which is missing an oracle price, potentially resulting in a lower borrow
253+
// limit than if prices were all available.
242254
string borrow_limit = 4 [
243255
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
244256
(gogoproto.nullable) = false
245257
];
246258
// Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation.
259+
// Will be null if an oracle price required for computation is missing.
247260
string liquidation_threshold = 5 [
248261
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
249-
(gogoproto.nullable) = false
262+
(gogoproto.nullable) = true
250263
];
251264
}
252265

swagger/swagger.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,19 +131,38 @@ paths:
131131
type: string
132132
description: >-
133133
Supplied Value is the sum of the USD value of all tokens the
134+
<<<<<<< HEAD
134135
account has supplied, includng interest earned.
136+
=======
137+
account has supplied, including interest earned.
138+
139+
Computation skips assets which are missing oracle prices,
140+
potentially resulting in a lower supplied
141+
142+
value than if prices were all available.
143+
>>>>>>> 87b9ed4 (fix!: allow safe leverage operations during partial oracle outages (#1821))
135144
collateral_value:
136145
type: string
137146
description: >-
138147
Collateral Value is the sum of the USD value of all uTokens
139148
the account has collateralized.
149+
150+
Computation skips collateral which is missing an oracle price,
151+
potentially resulting in a lower collateral
152+
153+
value than if prices were all available.
140154
borrowed_value:
141155
type: string
142156
description: >-
143157
Borrowed Value is the sum of the USD value of all tokens the
144158
account has borrowed, including interest owed.
145159
146160
It always uses spot prices.
161+
162+
Computation skips borrows which are missing oracle prices,
163+
potentially resulting in a lower borrowed
164+
165+
value than if prices were all available.
147166
borrow_limit:
148167
type: string
149168
description: >-
@@ -152,11 +171,19 @@ paths:
152171
153172
The lower of spot or historic price for each collateral token
154173
is used when calculating borrow limits.
174+
175+
Computation skips collateral which is missing an oracle price,
176+
potentially resulting in a lower borrow
177+
178+
limit than if prices were all available.
155179
liquidation_threshold:
156180
type: string
157181
description: >-
158182
Liquidation Threshold is the Borrowed Value at which the
159183
account becomes eligible for liquidation.
184+
185+
Will be null if an oracle price required for computation is
186+
missing.
160187
description: >-
161188
QueryAccountSummaryResponse defines the response structure for the
162189
AccountSummary gRPC service handler.
@@ -2021,19 +2048,38 @@ definitions:
20212048
type: string
20222049
description: >-
20232050
Supplied Value is the sum of the USD value of all tokens the account
2051+
<<<<<<< HEAD
20242052
has supplied, includng interest earned.
2053+
=======
2054+
has supplied, including interest earned.
2055+
2056+
Computation skips assets which are missing oracle prices, potentially
2057+
resulting in a lower supplied
2058+
2059+
value than if prices were all available.
2060+
>>>>>>> 87b9ed4 (fix!: allow safe leverage operations during partial oracle outages (#1821))
20252061
collateral_value:
20262062
type: string
20272063
description: >-
20282064
Collateral Value is the sum of the USD value of all uTokens the
20292065
account has collateralized.
2066+
2067+
Computation skips collateral which is missing an oracle price,
2068+
potentially resulting in a lower collateral
2069+
2070+
value than if prices were all available.
20302071
borrowed_value:
20312072
type: string
20322073
description: >-
20332074
Borrowed Value is the sum of the USD value of all tokens the account
20342075
has borrowed, including interest owed.
20352076
20362077
It always uses spot prices.
2078+
2079+
Computation skips borrows which are missing oracle prices, potentially
2080+
resulting in a lower borrowed
2081+
2082+
value than if prices were all available.
20372083
borrow_limit:
20382084
type: string
20392085
description: >-
@@ -2042,11 +2088,18 @@ definitions:
20422088
20432089
The lower of spot or historic price for each collateral token is used
20442090
when calculating borrow limits.
2091+
2092+
Computation skips collateral which is missing an oracle price,
2093+
potentially resulting in a lower borrow
2094+
2095+
limit than if prices were all available.
20452096
liquidation_threshold:
20462097
type: string
20472098
description: >-
20482099
Liquidation Threshold is the Borrowed Value at which the account
20492100
becomes eligible for liquidation.
2101+
2102+
Will be null if an oracle price required for computation is missing.
20502103
description: >-
20512104
QueryAccountSummaryResponse defines the response structure for the
20522105
AccountSummary gRPC service handler.

x/leverage/client/tests/tests.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
252252
nil,
253253
}
254254

255+
lt1 := sdk.MustNewDecFromStr("0.0085610525")
256+
255257
nonzeroQueries := []testQuery{
256258
{
257259
"query account balances",
@@ -294,7 +296,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
294296
// (1001 / 1000000) * 34.21 * 0.25 = 0.0085610525
295297
BorrowLimit: sdk.MustNewDecFromStr("0.0085610525"),
296298
// (1001 / 1000000) * 0.25 * 34.21 = 0.0085610525
297-
LiquidationThreshold: sdk.MustNewDecFromStr("0.0085610525"),
299+
LiquidationThreshold: &lt1,
298300
},
299301
},
300302
{

x/leverage/keeper/borrows.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99

1010
// assertBorrowerHealth returns an error if a borrower is currently above their borrow limit,
1111
// under either recent (historic median) or current prices. It returns an error if
12-
// prices cannot be calculated.
12+
// borrowed asset prices cannot be calculated, but will try to treat collateral whose prices are
13+
// unavailable as having zero value. This can still result in a borrow limit being too low,
14+
// unless the remaining collateral is enough to cover all borrows.
1315
// This should be checked in msg_server.go at the end of any transaction which is restricted
1416
// by borrow limits, i.e. Borrow, Decollateralize, Withdraw, MaxWithdraw.
1517
func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error {
@@ -20,7 +22,7 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres
2022
if err != nil {
2123
return err
2224
}
23-
limit, err := k.CalculateBorrowLimit(ctx, collateral)
25+
limit, err := k.VisibleBorrowLimit(ctx, collateral)
2426
if err != nil {
2527
return err
2628
}
@@ -127,6 +129,45 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk
127129
return limit, nil
128130
}
129131

132+
// VisibleBorrowLimit uses the price oracle to determine the borrow limit (in USD) provided by
133+
// collateral sdk.Coins, using each token's uToken exchange rate and collateral weight.
134+
// The lower of spot price or historic price is used for each collateral token.
135+
// An error is returned if any input coins are not uTokens.
136+
// This function skips assets that are missing prices, which will lead to a lower borrow
137+
// limit when prices are down instead of a complete loss of borrowing ability.
138+
func (k Keeper) VisibleBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
139+
limit := sdk.ZeroDec()
140+
141+
for _, coin := range collateral {
142+
// convert uToken collateral to base assets
143+
baseAsset, err := k.ExchangeUToken(ctx, coin)
144+
if err != nil {
145+
return sdk.ZeroDec(), err
146+
}
147+
148+
ts, err := k.GetTokenSettings(ctx, baseAsset.Denom)
149+
if err != nil {
150+
return sdk.ZeroDec(), err
151+
}
152+
153+
// ignore blacklisted tokens
154+
if !ts.Blacklist {
155+
// get USD value of base assets using the chosen price mode
156+
v, err := k.TokenValue(ctx, baseAsset, types.PriceModeLow)
157+
if err == nil {
158+
// if both spot and historic (if required) prices exist,
159+
// add collateral coin's weighted value to borrow limit
160+
limit = limit.Add(v.Mul(ts.CollateralWeight))
161+
}
162+
if nonOracleError(err) {
163+
return sdk.ZeroDec(), err
164+
}
165+
}
166+
}
167+
168+
return limit, nil
169+
}
170+
130171
// CalculateLiquidationThreshold determines the maximum borrowed value (in USD) that a
131172
// borrower with given collateral could reach before being eligible for liquidation, using
132173
// each token's oracle price, uToken exchange rate, and liquidation threshold.

x/leverage/keeper/collateral.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,13 @@ func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (s
9494

9595
// get USD value of base assets
9696
v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot)
97-
if err != nil {
98-
k.Logger(ctx).Info(
99-
"collateral value skipped",
100-
"uToken", coin.String(),
101-
"error", err.Error(),
102-
)
103-
continue
97+
if err == nil {
98+
// for coins that did not error, add their value to the total
99+
total = total.Add(v)
100+
}
101+
if nonOracleError(err) {
102+
return sdk.ZeroDec(), err
104103
}
105-
106-
// for coins that did not error, add their value to the total
107-
total = total.Add(v)
108104
}
109105

110106
return total, nil

x/leverage/keeper/errors.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package keeper
2+
3+
import (
4+
"strings"
5+
6+
"cosmossdk.io/errors"
7+
8+
"github.com/umee-network/umee/v4/util/decmath"
9+
leveragetypes "github.com/umee-network/umee/v4/x/leverage/types"
10+
oracletypes "github.com/umee-network/umee/v4/x/oracle/types"
11+
)
12+
13+
// nonOracleError returns true if an error is non-nil
14+
// and also not one of ErrEmptyList, ErrUnknownDenom, or ErrNoHistoricMedians
15+
// which are errors which can result from missing prices
16+
func nonOracleError(err error) bool {
17+
if err == nil {
18+
return false
19+
}
20+
// check typed errors
21+
if errors.IsOf(err,
22+
leveragetypes.ErrInvalidOraclePrice,
23+
leveragetypes.ErrNoHistoricMedians,
24+
oracletypes.ErrUnknownDenom,
25+
) {
26+
return false
27+
}
28+
// this error needs to be checked by string comparison
29+
if strings.Contains(err.Error(), decmath.ErrEmptyList.Error()) {
30+
return false
31+
}
32+
return true
33+
}

x/leverage/keeper/errors_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package keeper
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"cosmossdk.io/errors"
9+
10+
"github.com/umee-network/umee/v4/util/decmath"
11+
leveragetypes "github.com/umee-network/umee/v4/x/leverage/types"
12+
oracletypes "github.com/umee-network/umee/v4/x/oracle/types"
13+
)
14+
15+
func TestErrorMatching(t *testing.T) {
16+
// oracle errors
17+
err1 := errors.Wrap(decmath.ErrEmptyList, "denom: UMEE")
18+
err2 := oracletypes.ErrUnknownDenom.Wrap("UMEE")
19+
err3 := leveragetypes.ErrNoHistoricMedians.Wrapf(
20+
"requested %d, got %d",
21+
16,
22+
12,
23+
)
24+
// not oracle errors
25+
err4 := leveragetypes.ErrBlacklisted
26+
err5 := leveragetypes.ErrUToken
27+
err6 := leveragetypes.ErrNotRegisteredToken
28+
err7 := errors.New("foo", 1, "bar")
29+
30+
require.Equal(t, false, nonOracleError(nil))
31+
require.Equal(t, false, nonOracleError(err1))
32+
require.Equal(t, false, nonOracleError(err2))
33+
require.Equal(t, false, nonOracleError(err3))
34+
require.Equal(t, true, nonOracleError(err4))
35+
require.Equal(t, true, nonOracleError(err5))
36+
require.Equal(t, true, nonOracleError(err6))
37+
require.Equal(t, true, nonOracleError(err7))
38+
}

0 commit comments

Comments
 (0)