Skip to content

Commit 87b9ed4

Browse files
authored
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)
1 parent c500338 commit 87b9ed4

File tree

19 files changed

+850
-141
lines changed

19 files changed

+850
-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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,31 +222,40 @@ message QueryAccountSummary {
222222
// QueryAccountSummaryResponse defines the response structure for the AccountSummary gRPC service handler.
223223
message QueryAccountSummaryResponse {
224224
// Supplied Value is the sum of the USD value of all tokens the account has supplied, including interest earned.
225+
// Computation skips assets which are missing oracle prices, potentially resulting in a lower supplied
226+
// value than if prices were all available.
225227
string supplied_value = 1 [
226228
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
227229
(gogoproto.nullable) = false
228230
];
229231
// Collateral Value is the sum of the USD value of all uTokens the account has collateralized.
232+
// Computation skips collateral which is missing an oracle price, potentially resulting in a lower collateral
233+
// value than if prices were all available.
230234
string collateral_value = 2 [
231235
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
232236
(gogoproto.nullable) = false
233237
];
234238
// Borrowed Value is the sum of the USD value of all tokens the account has borrowed, including interest owed.
235239
// It always uses spot prices.
240+
// Computation skips borrows which are missing oracle prices, potentially resulting in a lower borrowed
241+
// value than if prices were all available.
236242
string borrowed_value = 3 [
237243
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
238244
(gogoproto.nullable) = false
239245
];
240246
// Borrow Limit is the maximum Borrowed Value the account is allowed to reach through direct borrowing.
241247
// The lower of spot or historic price for each collateral token is used when calculating borrow limits.
248+
// Computation skips collateral which is missing an oracle price, potentially resulting in a lower borrow
249+
// limit than if prices were all available.
242250
string borrow_limit = 4 [
243251
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
244252
(gogoproto.nullable) = false
245253
];
246254
// Liquidation Threshold is the Borrowed Value at which the account becomes eligible for liquidation.
255+
// Will be null if an oracle price required for computation is missing.
247256
string liquidation_threshold = 5 [
248257
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
249-
(gogoproto.nullable) = false
258+
(gogoproto.nullable) = true
250259
];
251260
}
252261

swagger/swagger.yaml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,33 @@ paths:
132132
description: >-
133133
Supplied Value is the sum of the USD value of all tokens the
134134
account has supplied, including interest earned.
135+
136+
Computation skips assets which are missing oracle prices,
137+
potentially resulting in a lower supplied
138+
139+
value than if prices were all available.
135140
collateral_value:
136141
type: string
137142
description: >-
138143
Collateral Value is the sum of the USD value of all uTokens
139144
the account has collateralized.
145+
146+
Computation skips collateral which is missing an oracle price,
147+
potentially resulting in a lower collateral
148+
149+
value than if prices were all available.
140150
borrowed_value:
141151
type: string
142152
description: >-
143153
Borrowed Value is the sum of the USD value of all tokens the
144154
account has borrowed, including interest owed.
145155
146156
It always uses spot prices.
157+
158+
Computation skips borrows which are missing oracle prices,
159+
potentially resulting in a lower borrowed
160+
161+
value than if prices were all available.
147162
borrow_limit:
148163
type: string
149164
description: >-
@@ -152,11 +167,19 @@ paths:
152167
153168
The lower of spot or historic price for each collateral token
154169
is used when calculating borrow limits.
170+
171+
Computation skips collateral which is missing an oracle price,
172+
potentially resulting in a lower borrow
173+
174+
limit than if prices were all available.
155175
liquidation_threshold:
156176
type: string
157177
description: >-
158178
Liquidation Threshold is the Borrowed Value at which the
159179
account becomes eligible for liquidation.
180+
181+
Will be null if an oracle price required for computation is
182+
missing.
160183
description: >-
161184
QueryAccountSummaryResponse defines the response structure for the
162185
AccountSummary gRPC service handler.
@@ -2023,18 +2046,33 @@ definitions:
20232046
description: >-
20242047
Supplied Value is the sum of the USD value of all tokens the account
20252048
has supplied, including interest earned.
2049+
2050+
Computation skips assets which are missing oracle prices, potentially
2051+
resulting in a lower supplied
2052+
2053+
value than if prices were all available.
20262054
collateral_value:
20272055
type: string
20282056
description: >-
20292057
Collateral Value is the sum of the USD value of all uTokens the
20302058
account has collateralized.
2059+
2060+
Computation skips collateral which is missing an oracle price,
2061+
potentially resulting in a lower collateral
2062+
2063+
value than if prices were all available.
20312064
borrowed_value:
20322065
type: string
20332066
description: >-
20342067
Borrowed Value is the sum of the USD value of all tokens the account
20352068
has borrowed, including interest owed.
20362069
20372070
It always uses spot prices.
2071+
2072+
Computation skips borrows which are missing oracle prices, potentially
2073+
resulting in a lower borrowed
2074+
2075+
value than if prices were all available.
20382076
borrow_limit:
20392077
type: string
20402078
description: >-
@@ -2043,11 +2081,18 @@ definitions:
20432081
20442082
The lower of spot or historic price for each collateral token is used
20452083
when calculating borrow limits.
2084+
2085+
Computation skips collateral which is missing an oracle price,
2086+
potentially resulting in a lower borrow
2087+
2088+
limit than if prices were all available.
20462089
liquidation_threshold:
20472090
type: string
20482091
description: >-
20492092
Liquidation Threshold is the Borrowed Value at which the account
20502093
becomes eligible for liquidation.
2094+
2095+
Will be null if an oracle price required for computation is missing.
20512096
description: >-
20522097
QueryAccountSummaryResponse defines the response structure for the
20532098
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)