Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement special asset pairs #2164

Closed
wants to merge 78 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
2f1b8bd
feat: add special asset pairs
toteki Jul 17, 2023
cb484f9
Update proto/umee/leverage/v1/leverage.proto
toteki Jul 17, 2023
02b5edf
Merge branch 'main' into adam/special
toteki Jul 18, 2023
07acee1
Merge branch 'main' into adam/special
toteki Jul 20, 2023
9f0f9fe
Merge branch 'main' into adam/special
toteki Jul 21, 2023
286fa0e
line length lint
toteki Jul 21, 2023
e05b997
Merge branch 'main' into adam/special
toteki Jul 21, 2023
5eb3d88
Merge branch 'main' into adam/special
toteki Jul 21, 2023
ea72c67
Update x/leverage/keeper/token.go
toteki Jul 21, 2023
545ee88
Update x/leverage/keeper/token.go
toteki Jul 21, 2023
b3270a8
Update proto/umee/leverage/v1/leverage.proto
toteki Jul 21, 2023
d1819fa
Update proto/umee/leverage/v1/leverage.proto
toteki Jul 21, 2023
73c4b30
Merge branch 'main' into adam/special
toteki Jul 21, 2023
5b7b02d
make proto-all
toteki Jul 21, 2023
55f8153
make proto-all
toteki Jul 21, 2023
97efc7f
fix
toteki Jul 21, 2023
2923478
assertBorrowerHealth changes
toteki Jul 21, 2023
0a7b2b1
pairs may not reduce cw
toteki Jul 21, 2023
48a9900
Merge branch 'main' into adam/special
toteki Jul 25, 2023
c9c0cdb
remove title + description
toteki Jul 25, 2023
5817edd
group legacy msg interface
toteki Jul 25, 2023
153037e
proposal.go
toteki Jul 25, 2023
c3d39b2
legacy++
toteki Jul 25, 2023
9a22f72
special asset sets in gov message
toteki Jul 26, 2023
11ffd99
pair deletion logic
toteki Jul 26, 2023
5e08485
can query for specific asset
toteki Jul 26, 2023
004a5d5
Update proto/umee/leverage/v1/tx.proto
toteki Jul 26, 2023
0c3440d
make proto-all
toteki Jul 26, 2023
f8cb27f
changelog
toteki Jul 26, 2023
19d5a06
Merge branch 'main' into adam/special
toteki Jul 27, 2023
206c769
lint
toteki Jul 27, 2023
1c55762
Merge branch 'adam/special' into adam/special3
toteki Jul 27, 2023
e9b7ddf
progress
toteki Jul 28, 2023
d9f5578
progress
toteki Jul 28, 2023
b9b522c
Update proto/umee/leverage/v1/tx.proto
toteki Jul 28, 2023
0075b58
Update proto/umee/leverage/v1/tx.proto
toteki Jul 28, 2023
53b7e4f
proto changes
toteki Jul 28, 2023
7944aae
collateral weight zero deletes sets ad pairs
toteki Jul 28, 2023
fcda085
fix CLI
toteki Jul 28, 2023
ef36cda
test++
toteki Jul 28, 2023
0e46e1a
combine validate functions in genesis
toteki Jul 28, 2023
6f75ab6
Merge branch 'main' into adam/special
toteki Jul 28, 2023
daa5e0a
fix
toteki Jul 28, 2023
f83ab08
fix
toteki Jul 28, 2023
78cd2e7
proto changes
toteki Jul 30, 2023
e48b35c
fix names
toteki Jul 30, 2023
8804ed4
fix names
toteki Jul 30, 2023
31402c5
validation allows empty pairs if sets present instead
toteki Jul 30, 2023
03dcd74
optional arg doc
toteki Jul 30, 2023
cbd2178
Add liquidation threshold to pairs
toteki Jul 30, 2023
39a9994
revert proposal.go
toteki Jul 30, 2023
5e2e948
change stringer
toteki Jul 30, 2023
5a43519
Merge branch 'adam/special' into adam/special3
toteki Jul 30, 2023
ddc290b
readme++
toteki Jul 31, 2023
f4bb3fc
Rearrange types
toteki Jul 31, 2023
6934348
additional cached values
toteki Jul 31, 2023
978c3d1
move position struct to types and isolate for keeper, ctx
toteki Jul 31, 2023
e90da2b
sorting of position into special pairs
toteki Jul 31, 2023
5b5d084
use position struct for AssertBorrowerHealth
toteki Jul 31, 2023
cb0a532
lint
toteki Jul 31, 2023
3b64064
fix
toteki Jul 31, 2023
f3c2ff6
use position borrow limit for account summary query
toteki Jul 31, 2023
15c75c1
comments++
toteki Jul 31, 2023
3f0332c
Merge branch 'main' into adam/special3
toteki Aug 1, 2023
f29ed9c
change liquidation threshold logic
toteki Aug 1, 2023
a9162aa
Merge branch 'main' into adam/special3
toteki Aug 1, 2023
8df29fb
create empty position.MaxBorrow and position.MaxWithdraw - break tests
toteki Aug 1, 2023
0f88490
comment++
toteki Aug 1, 2023
55f8847
comment++
toteki Aug 1, 2023
10cb160
fix sorting
toteki Aug 2, 2023
dea5bac
move asset pairing to initialization, away from limit function
toteki Aug 2, 2023
64b3f68
move soon-to-be-removed functions to todo file
toteki Aug 2, 2023
6ed253a
lint
toteki Aug 2, 2023
4b1058d
Export fields
toteki Aug 2, 2023
162efcf
Revert "Export fields"
toteki Aug 2, 2023
1443ce4
export GetAccountPosition
toteki Aug 2, 2023
93a0ed5
typo
toteki Aug 2, 2023
48b5d88
Merge branch 'main' into adam/special3
toteki Aug 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion util/coin/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func Zero(denom string) sdk.Coin {
return sdk.NewInt64Coin(denom, 0)
}

// Zero returns new coin with zero amount
// ZeroDec returns new decCoin with zero amount
func ZeroDec(denom string) sdk.DecCoin {
return sdk.NewInt64DecCoin(denom, 0)
}
Expand Down
2 changes: 1 addition & 1 deletion util/coin/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func UmeeDec(amount string) sdk.DecCoin {
return Dec(appparams.BondDenom, amount)
}

// Utoken creates a uToken DecCoin.
// Utoken creates a uToken Coin.
func Utoken(denom string, amount int64) sdk.Coin {
return New(leveragetypes.ToUTokenDenom(denom), amount)
}
Expand Down
6 changes: 4 additions & 2 deletions x/leverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,14 @@ For example, an account using a single collateral token with `CollateralWeight 0

The leverage module can define pairs of assets which are advantaged when one is used as collateral to borrow the other.

They are defined in the form `[Asset A, Asset B, Special Collateral Weight]`. In effect, this means that
They are defined in the form `[Asset A, Asset B, Special Collateral Weight, Special Liquidation Threshold]`. In effect, this means that
Copy link
Member Author

@toteki toteki Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary change, it turns out, to prevent special asset pairs allowing users to instantly become eligible for liquidation.

Added to proto and struct in #2150, but to readme here.


> When a user has collateral of `Asset A` and borrows `Asset B`, or vice versa, the `CollateralWeight` of both `Asset A` and `Asset B` are replaced by `Special Collateral Weight`.
> When a user has collateral of `Asset A` and borrows `Asset B`, or vice versa, the `CollateralWeight` of both `Asset A` and `Asset B` are replaced by `Special Collateral Weight`. The `LiquidationThreshold` of the assets is also replaced by that of the special pair.

#### Special Asset Pair Examples

> Consider a scenario where assets `A,B,C,D` all have collateral weight `0.75`. There is also a special asset pair `[A,B,0.9]` which privileges borrows between those two assets.
> (Note: Liquidation threshold has been omitted from the special pair in this example.)
>
> A user with `Collateral: $10A, Borrowed: $7A` is unaffected by any special asset pairs. The maximum `A` it could borrow is `$7.50`
>
Expand Down Expand Up @@ -320,6 +321,7 @@ In practice, the following calculation (which reduces to the logic above in simp
This utilizes the borrow limit, which has already been computed with special asset pairs, and the token parameters of borrower's collateral:

- The average (weighted by collateral value) collateral weights and liquidation thresholds of the borrower's collateral assets are collected.
- For collateral assets being counted in special asset pairs, the collateral weight and liquidation threshold of the pair is used instead of that of the asset.
- The distances from average collateral weight to average liquidation threshold and 1 are compared. (For example when `CW = 0.6` and `LT = 0.7`, then liquidation threshold is `25%` of the way from `CW` to `1`.)
- Then the borrower's liquidation threshold behaves the same as the average parameters (e.g. it will be `25%` of the way between `borrow limit` and `collateral value`).

Expand Down
147 changes: 9 additions & 138 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,28 @@ import (
)

// assertBorrowerHealth returns an error if a borrower is currently above their borrow limit,
// under either recent (historic median) or current prices. Checks using borrow limit based
// on collateral weight, then check separately for borrow limit using borrow factor. Error if
// borrowed asset prices cannot be calculated, but will try to treat collateral whose prices are
// under either recent (historic median) or current prices. Checks using rules for collateral
// weight, borrow factor, and special asset pairs to determine borrow limit. Error if
// borrowed asset prices cannot be calculated, but will 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.
// by borrow limits, i.e. Borrow, Decollateralize, Withdraw, MaxWithdraw, LeverageLiquidate.
// MaxUsage sets the maximum percent of a user's borrow limit that can be in use: set to 1
// to allow up to 100% borrow limit, or a lower value (e.g. 0.9) if a transaction should fail
// if a safety margin is desired (e.g. <90% borrow limit).
func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress, maxUsage sdk.Dec) error {
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)
collateral := k.GetBorrowerCollateral(ctx, borrowerAddr)

// check health using collateral weight
borrowValue, err := k.TotalTokenValue(ctx, borrowed, types.PriceModeHigh)
position, err := k.GetAccountPosition(ctx, borrowerAddr, false)
if err != nil {
return err
}
borrowLimit, err := k.VisibleBorrowLimit(ctx, collateral)
if err != nil {
return err
}
if borrowValue.GT(borrowLimit.Mul(maxUsage)) {
return types.ErrUndercollaterized.Wrapf(
"borrowed: %s, limit: %s, max usage %s", borrowValue, borrowLimit, maxUsage)
}

// check health using borrow factor
weightedBorrowValue, err := k.ValueWithBorrowFactor(ctx, borrowed, types.PriceModeHigh)
if err != nil {
return err
}
collateralValue, err := k.VisibleUTokensValue(ctx, collateral, types.PriceModeLow)
if err != nil {
return err
}
if weightedBorrowValue.GT(collateralValue.Mul(maxUsage)) {
borrowedValue := position.BorrowedValue()
borrowLimit := position.Limit()
if borrowedValue.GT(borrowLimit.Mul(maxUsage)) {
return types.ErrUndercollaterized.Wrapf(
"weighted borrow: %s, collateral value: %s, max usage %s", weightedBorrowValue, collateralValue, maxUsage)
"borrowed: %s, limit: %s, max usage %s", borrowedValue, borrowLimit, maxUsage)
}

return nil
}

Expand Down Expand Up @@ -125,115 +105,6 @@ func (k Keeper) SupplyUtilization(ctx sdk.Context, denom string) sdk.Dec {
return totalBorrowed.Quo(tokenSupply)
}

// CalculateBorrowLimit 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 or if value calculation fails.
func (k Keeper) CalculateBorrowLimit(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 {
return sdk.ZeroDec(), err
}
// add each collateral coin's weighted value to borrow limit
limit = limit.Add(v.Mul(ts.CollateralWeight))
}
}

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

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.
// An error is returned if any input coins are not uTokens or if value
// calculation fails. Always uses spot prices.
func (k Keeper) CalculateLiquidationThreshold(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
totalThreshold := 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
v, err := k.TokenValue(ctx, baseAsset, types.PriceModeSpot)
if err != nil {
return sdk.ZeroDec(), err
}

// add each collateral coin's weighted value to liquidation threshold
totalThreshold = totalThreshold.Add(v.Mul(ts.LiquidationThreshold))
}
}

return totalThreshold, nil
}

// checkSupplyUtilization returns the appropriate error if a token denom's
// supply utilization has exceeded MaxSupplyUtilization
func (k Keeper) checkSupplyUtilization(ctx sdk.Context, denom string) error {
Expand Down
2 changes: 2 additions & 0 deletions x/leverage/keeper/borrows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func (s *IntegrationTestSuite) TestDeriveBorrowUtilization() {
require.Equal(sdk.MustNewDecFromStr("1.0"), utilization)
}

/*
func (s *IntegrationTestSuite) TestCalculateBorrowLimit() {
app, ctx, require := s.app, s.ctx, s.Require()

Expand Down Expand Up @@ -251,3 +252,4 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() {
require.NoError(err)
require.Equal(expectedCombinedLimit, borrowLimit)
}
*/
8 changes: 5 additions & 3 deletions x/leverage/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,11 @@ func (q Querier) AccountSummary(
// borrow limit shown here as it is used in leverage logic:
// using the lower of spot or historic prices for each collateral token
// skips collateral tokens with missing oracle prices
borrowLimit, err := q.Keeper.VisibleBorrowLimit(ctx, collateral)
ap, err := q.Keeper.GetAccountPosition(ctx, addr, false)
if err != nil {
return nil, err
}
borrowLimit := ap.Limit()

resp := &types.QueryAccountSummaryResponse{
SuppliedValue: suppliedValue,
Expand All @@ -273,9 +274,10 @@ func (q Querier) AccountSummary(

// liquidation always uses spot prices. This response field will be null
// if a price is missing
liquidationThreshold, err := q.Keeper.CalculateLiquidationThreshold(ctx, collateral)
ap, err = q.Keeper.GetAccountPosition(ctx, addr, true)
if err == nil {
resp.LiquidationThreshold = &liquidationThreshold
lt := ap.Limit()
resp.LiquidationThreshold = &lt
}

return resp, nil
Expand Down
10 changes: 7 additions & 3 deletions x/leverage/keeper/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,15 @@ func (q Querier) Inspect(
}
checkedAddrs[addr.String()] = struct{}{}

borrowedValue, collateralValue, liquidationThreshold := sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()
position, err := k.GetAccountPosition(ctx, addr, true)
if err == nil {
borrowedValue = position.BorrowedValue()
collateralValue = position.CollateralValue()
liquidationThreshold = position.Limit()
}
borrowed := k.GetBorrowerBorrows(ctx, addr)
borrowedValue, _ := k.TotalTokenValue(ctx, borrowed, types.PriceModeSpot)
collateral := k.GetBorrowerCollateral(ctx, addr)
collateralValue, _ := k.CalculateCollateralValue(ctx, collateral, types.PriceModeSpot)
liquidationThreshold, _ := k.CalculateLiquidationThreshold(ctx, collateral)

account := types.InspectAccount{
Address: addr.String(),
Expand Down
47 changes: 23 additions & 24 deletions x/leverage/keeper/iter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"sort"

sdkmath "cosmossdk.io/math"
prefixstore "github.com/cosmos/cosmos-sdk/store/prefix"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
Expand Down Expand Up @@ -45,14 +47,22 @@ func (k Keeper) GetAllRegisteredTokens(ctx sdk.Context) []types.Token {
// GetAllSpecialAssetPairs returns all the special asset pairs from the x/leverage
// module's KVStore.
func (k Keeper) GetAllSpecialAssetPairs(ctx sdk.Context) []types.SpecialAssetPair {
return store.MustLoadAll[*types.SpecialAssetPair](ctx.KVStore(k.storeKey), types.KeyPrefixSpecialAssetPair)
pairs := store.MustLoadAll[*types.SpecialAssetPair](ctx.KVStore(k.storeKey), types.KeyPrefixSpecialAssetPair)
sort.SliceStable(pairs, func(i, j int) bool {
return pairs[i].CollateralWeight.LT(pairs[j].CollateralWeight)
})
return pairs
}

// GetSpecialAssetPairs returns all the special asset pairs from the x/leverage
// module's KVStore which match a single asset.
func (k Keeper) GetSpecialAssetPairs(ctx sdk.Context, denom string) []types.SpecialAssetPair {
prefix := types.KeySpecialAssetPairOneDenom(denom)
toteki marked this conversation as resolved.
Show resolved Hide resolved
return store.MustLoadAll[*types.SpecialAssetPair](ctx.KVStore(k.storeKey), prefix)
pairs := store.MustLoadAll[*types.SpecialAssetPair](ctx.KVStore(k.storeKey), prefix)
sort.SliceStable(pairs, func(i, j int) bool {
return pairs[i].CollateralWeight.LT(pairs[j].CollateralWeight)
})
return pairs
}

// GetBorrowerBorrows returns an sdk.Coins object containing all open borrows
Expand Down Expand Up @@ -117,29 +127,18 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress
}
checkedAddrs[borrowerAddr.String()] = struct{}{}

// get borrower's total borrowed
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)

// get borrower's total collateral
collateral := k.GetBorrowerCollateral(ctx, borrowerAddr)

// use oracle helper functions to find total borrowed value in USD
// skips denoms without prices
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
// to the next borrower instead of stopping the entire query
liquidationLimit, err := k.CalculateLiquidationThreshold(ctx, collateral)
if err == nil && liquidationLimit.LT(borrowValue) {
// If liquidation limit is smaller than borrowed value then the
// address is eligible for liquidation.
liquidationTargets = append(liquidationTargets, borrowerAddr)
position, err := k.GetAccountPosition(ctx, borrowerAddr, true)
if err == nil {
borrowValue := position.BorrowedValue()
liquidationThreshold := position.Limit()
if liquidationThreshold.LT(borrowValue) {
// If liquidation threshold is smaller than borrowed value then the
// address is eligible for liquidation.
liquidationTargets = append(liquidationTargets, borrowerAddr)
}
}
// Non-price errors will cause the query itself to fail
// Non-price errors will cause the query itself to fail, whereas oracle errors
// simply cause the address to be skipped
if nonOracleError(err) {
return err
}
Expand Down
Loading
Loading