Skip to content

Commit

Permalink
fix: collateral share restriction works during partial price outages (#…
Browse files Browse the repository at this point in the history
…1812)

* fix: collateral share restriction works during partial price outages

* cl++

* add info logs

---------

Co-authored-by: Robert Zaremba <robert@zaremba.ch>
  • Loading branch information
toteki and robert-zaremba authored Feb 14, 2023
1 parent c67510e commit e23e4fc
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Fixes

- [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.

## [v4.0.1](https://github.com/umee-network/umee/releases/tag/v4.0.1) - 2023-02-10
Expand Down
52 changes: 46 additions & 6 deletions x/leverage/keeper/collateral.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ func (k Keeper) CalculateCollateralValue(ctx sdk.Context, collateral sdk.Coins)
return total, nil
}

// VisibleCollateralValue uses the price oracle to determine the value (in USD) provided by
// collateral sdk.Coins, using each token's uToken exchange rate. Always uses spot price.
// Unlike CalculateCollateralValue, this function will not return an error if value calculation
// fails on a token - instead, that token will contribute zero value to the total.
func (k Keeper) VisibleCollateralValue(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
total := 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
}

// 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
}

// for coins that did not error, add their value to the total
total = total.Add(v)
}

return total, nil
}

// GetAllTotalCollateral returns total collateral across all uTokens.
func (k Keeper) GetAllTotalCollateral(ctx sdk.Context) sdk.Coins {
total := sdk.NewCoins()
Expand Down Expand Up @@ -112,14 +144,16 @@ func (k Keeper) CollateralLiquidity(ctx sdk.Context, denom string) sdk.Dec {
return sdk.MinDec(collateralLiquidity, sdk.OneDec())
}

// CollateralShare calculates the portion of overall collateral
// (measured in USD value) that a given uToken denom represents.
func (k *Keeper) CollateralShare(ctx sdk.Context, denom string) (sdk.Dec, error) {
// VisibleCollateralShare calculates the portion of overall collateral (measured in USD value) that a
// given uToken denom represents. If an asset other than the denom requested is missing an oracle
// price, it ignores that asset's contribution to the system's overall collateral, thus potentially
// overestimating the requested denom's collateral share while improving availability.
func (k *Keeper) VisibleCollateralShare(ctx sdk.Context, denom string) (sdk.Dec, error) {
systemCollateral := k.GetAllTotalCollateral(ctx)
thisCollateral := sdk.NewCoins(sdk.NewCoin(denom, systemCollateral.AmountOf(denom)))

// get USD collateral value for all uTokens combined
totalValue, err := k.CalculateCollateralValue(ctx, systemCollateral)
// get USD collateral value for all uTokens combined, except those experiencing price outages
totalValue, err := k.VisibleCollateralValue(ctx, systemCollateral)
if err != nil {
return sdk.ZeroDec(), err
}
Expand Down Expand Up @@ -152,13 +186,19 @@ func (k Keeper) checkCollateralLiquidity(ctx sdk.Context, denom string) error {
}

// checkCollateralShare returns an error if a given uToken is above its collateral share
// as calculated using only tokens whose oracle prices exist
func (k *Keeper) checkCollateralShare(ctx sdk.Context, denom string) error {
token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(denom))
if err != nil {
return err
}

share, err := k.CollateralShare(ctx, denom)
if token.MaxCollateralShare.Equal(sdk.OneDec()) {
// skip computation when collateral share is unrestricted
return nil
}

share, err := k.VisibleCollateralShare(ctx, denom)
if err != nil {
return err
}
Expand Down
43 changes: 34 additions & 9 deletions x/leverage/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,27 +813,32 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() {
supplier,
coin.New(umeeDenom, 80_000000),
types.ErrNotUToken,
}, {
},
{
"no collateral",
supplier,
coin.New("u/"+atomDenom, 40_000000),
types.ErrInsufficientCollateral,
}, {
},
{
"valid decollateralize",
supplier,
coin.New("u/"+umeeDenom, 80_000000),
nil,
}, {
},
{
"additional decollateralize",
supplier,
coin.New("u/"+umeeDenom, 10_000000),
nil,
}, {
},
{
"insufficient collateral",
supplier,
coin.New("u/"+umeeDenom, 40_000000),
types.ErrInsufficientCollateral,
}, {
},
{
"above borrow limit",
borrower,
coin.New("u/"+atomDenom, 100_000000),
Expand All @@ -845,17 +850,20 @@ func (s *IntegrationTestSuite) TestMsgDecollateralize() {
dumpborrower,
coin.New("u/"+pumpDenom, 20_000000),
nil,
}, {
},
{
"above borrow limit (undercollateralized under historic prices but ok with current prices)",
dumpborrower,
coin.New("u/"+pumpDenom, 20_000000),
types.ErrUndercollaterized,
}, {
},
{
"acceptable decollateralize (pump borrower)",
pumpborrower,
coin.New("u/"+dumpDenom, 20_000000),
nil,
}, {
},
{
"above borrow limit (undercollateralized under current prices but ok with historic prices)",
pumpborrower,
coin.New("u/"+dumpDenom, 20_000000),
Expand Down Expand Up @@ -1684,8 +1692,25 @@ func (s *IntegrationTestSuite) TestMaxCollateralShare() {
// so ATOM's collateral share ($46.46 / $467.46) is barely below 10%
s.collateralize(atomSupplier, coin.New("u/"+atomDenom, 1_180000))

// attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM
// kill the oracle's ability to return UMEE price
s.mockOracle.Clear("UMEE")

// now ATOM's (visible) collateral share is 100% and even the smallest collateralize will fail
msg := &types.MsgCollateralize{
Borrower: atomSupplier.String(),
Asset: coin.New("u/"+atomDenom, 1),
}
_, err = srv.Collateralize(ctx, msg)
require.ErrorIs(err, types.ErrMaxCollateralShare)

// return the oracle to normal
s.mockOracle.Reset()

// ensure the previous collateralize would have worked
s.collateralize(atomSupplier, coin.New("u/"+atomDenom, 1))

// attempt to collateralize another 0.01 ATOM, which would result in too much collateral share for ATOM
msg = &types.MsgCollateralize{
Borrower: atomSupplier.String(),
Asset: coin.New("u/"+atomDenom, 10000),
}
Expand Down
19 changes: 5 additions & 14 deletions x/leverage/keeper/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ func (m *mockOracleKeeper) GetExchangeRate(_ sdk.Context, denom string) (sdk.Dec
return p, nil
}

func (m *mockOracleKeeper) GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error) {
p, ok := m.baseExchangeRates[denom]
if !ok {
return sdk.ZeroDec(), fmt.Errorf("invalid denom: %s", denom)
}

return p, nil
// Clear clears a denom from the mock oracle, simulating an outage.
func (m *mockOracleKeeper) Clear(denom string) {
delete(m.symbolExchangeRates, denom)
delete(m.historicExchangeRates, denom)
}

// Reset restores the mock oracle's prices to its default values.
func (m *mockOracleKeeper) Reset() {
m.symbolExchangeRates = map[string]sdk.Dec{
"UMEE": sdk.MustNewDecFromStr("4.21"),
Expand All @@ -64,13 +62,6 @@ func (m *mockOracleKeeper) Reset() {
"DUMP": sdk.MustNewDecFromStr("0.50"), // A token which has recently halved in price
"PUMP": sdk.MustNewDecFromStr("2.00"), // A token which has recently doubled in price
}
m.baseExchangeRates = map[string]sdk.Dec{
appparams.BondDenom: sdk.MustNewDecFromStr("0.00000421"),
atomDenom: sdk.MustNewDecFromStr("0.00003938"),
daiDenom: sdk.MustNewDecFromStr("0.000000000000000001"),
dumpDenom: sdk.MustNewDecFromStr("0.0000005"),
pumpDenom: sdk.MustNewDecFromStr("0.0000020"),
}
m.historicExchangeRates = map[string]sdk.Dec{
"UMEE": sdk.MustNewDecFromStr("4.21"),
"ATOM": sdk.MustNewDecFromStr("39.38"),
Expand Down
6 changes: 5 additions & 1 deletion x/leverage/keeper/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type IntegrationTestSuite struct {
setupAccountCounter sdkmath.Int
addrs []sdk.AccAddress
msgSrvr types.MsgServer

mockOracle *mockOracleKeeper
}

func TestKeeperTestSuite(t *testing.T) {
Expand All @@ -54,14 +56,16 @@ func (s *IntegrationTestSuite) SetupTest() {
Time: time.Unix(0, 0),
})

s.mockOracle = newMockOracleKeeper()

// we only override the Leverage keeper so we can supply a custom mock oracle
k, tk := keeper.NewTestKeeper(
s.Require(),
app.AppCodec(),
app.GetKey(types.ModuleName),
app.GetSubspace(types.ModuleName),
app.BankKeeper,
newMockOracleKeeper(),
s.mockOracle,
true,
)

Expand Down
1 change: 0 additions & 1 deletion x/leverage/types/expected_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,5 @@ type BankKeeper interface {
// OracleKeeper defines the expected x/oracle keeper interface.
type OracleKeeper interface {
GetExchangeRate(ctx sdk.Context, denom string) (sdk.Dec, error)
GetExchangeRateBase(ctx sdk.Context, denom string) (sdk.Dec, error)
MedianOfHistoricMedians(ctx sdk.Context, denom string, numStamps uint64) (sdk.Dec, uint32, error)
}

0 comments on commit e23e4fc

Please sign in to comment.