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!: liquidation directly rewards base assets #1118

Merged
merged 71 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
828d73d
featgit statusiquidation directly rewards base assets
toteki Jul 7, 2022
5721ff2
Merge branch 'main' into adam/liquidate
toteki Jul 7, 2022
025f017
make proto-gen
toteki Jul 7, 2022
51f34dd
changelog++
toteki Jul 7, 2022
b15cc7e
comment++
toteki Jul 7, 2022
e129f3c
spec++
toteki Jul 7, 2022
7513e98
whitespace--
toteki Jul 7, 2022
5c61a56
function rename
toteki Jul 7, 2022
bcb555f
add MsgLiquidate response field for collateral consumed
toteki Jul 7, 2022
1ef4e90
line length--
toteki Jul 7, 2022
19d3f07
Merge branch 'main' into adam/liquidate
toteki Jul 11, 2022
dcb30d8
Merge branch 'main' into adam/liquidate
toteki Jul 11, 2022
e0aa350
update comment and rename return values
toteki Jul 11, 2022
c88119c
Merge branch 'main' into adam/liquidate
toteki Jul 11, 2022
f498ef4
make proto-gen
toteki Jul 11, 2022
73509d8
Merge branch 'main' into adam/liquidate
toteki Jul 13, 2022
035fb53
++
toteki Jul 13, 2022
3145f57
suggestion++
toteki Jul 13, 2022
654e6ed
suggestion++
toteki Jul 13, 2022
a81116c
Merge branch 'main' into adam/liquidate
toteki Jul 13, 2022
64d8e74
Merge branch 'main' into adam/liquidate
toteki Jul 13, 2022
5ee767b
comment++
toteki Jul 13, 2022
e0485c9
Merge branch 'adam/liquidate' of github.com:umee-network/umee into
toteki Jul 13, 2022
59a058f
move an error between functions
toteki Jul 13, 2022
fd21435
fix test
toteki Jul 13, 2022
9ccab78
add descriptive strings to math test cases
toteki Jul 13, 2022
144993e
proto comments
toteki Jul 13, 2022
03d0e08
filter blacklisted collateral before checking for bad debt
toteki Jul 17, 2022
27b9500
Merge branch 'main' into adam/liquidate
toteki Jul 17, 2022
4e021d9
Merge branch 'main' into adam/liquidate
toteki Jul 19, 2022
af2c201
Merge branch 'main' into adam/liquidate
robert-zaremba Jul 19, 2022
93cf56c
separate out liquidation function and reduce rounding
toteki Jul 19, 2022
d27f904
clarify math comments
toteki Jul 19, 2022
9713fff
clarify math comments
toteki Jul 19, 2022
3331a00
more test cases and a rounding TODO
toteki Jul 22, 2022
c007643
++
toteki Jul 22, 2022
8069c57
Merge branch 'main' into adam/liquidate
RafilxTenfen Jul 24, 2022
08f405a
Merge branch 'main' into adam/liquidate
toteki Jul 25, 2022
865c5d3
refactor and add detailed reasoning to liquidate computation
toteki Jul 26, 2022
c2ceae9
final test cases
toteki Jul 26, 2022
47113cd
remove redundant zero-price checks in PriceRatio
toteki Jul 26, 2022
1dea021
--
toteki Jul 26, 2022
ee01003
move liquidate computations back to keeper package
toteki Jul 26, 2022
9003703
move liquidate computations back to keeper package
toteki Jul 26, 2022
32ba636
Merge branch 'main' into adam/liquidate
toteki Jul 27, 2022
1ecaff8
Merge branch 'main' into adam/liquidate
toteki Jul 27, 2022
3f0e9b1
Merge branch 'main' into adam/liquidate
toteki Jul 27, 2022
f0fbba1
Merge branch 'main' into adam/liquidate
toteki Jul 27, 2022
ebab749
Merge branch 'main' into adam/liquidate
toteki Jul 27, 2022
f31b793
Merge branch 'main' into adam/liquidate
toteki Jul 28, 2022
93a24bb
Merge branch 'main' into adam/liquidate
toteki Jul 30, 2022
c4ed80f
Merge branch 'main' into adam/liquidate
toteki Aug 1, 2022
5d425ce
Merge branch 'main' into adam/liquidate
toteki Aug 2, 2022
7696a85
suggestion++
toteki Aug 3, 2022
b4113aa
make proto-gen
toteki Aug 3, 2022
b018703
remove ctx from filterCoins
toteki Aug 3, 2022
891c24b
Merge branch 'main' into adam/liquidate
toteki Aug 3, 2022
3b1a3a0
long example liquidate command
toteki Aug 3, 2022
c6aab01
Merge branch 'adam/liquidate' of github.com:umee-network/umee into ad…
toteki Aug 3, 2022
c0ee072
Merge branch 'main' into adam/liquidate
toteki Aug 4, 2022
ed87cb0
package keeper_test -> keeper for math unit tests
toteki Aug 4, 2022
dacfe9f
suggestion++
toteki Aug 4, 2022
4b1f543
suggestion++
toteki Aug 4, 2022
4b7bd3f
Merge branch 'main' into adam/liquidate
toteki Aug 4, 2022
11432bf
fix test amounts
toteki Aug 4, 2022
50abc33
update CloseFactor comment
toteki Aug 4, 2022
8542b4c
Merge branch 'main' into adam/liquidate
toteki Aug 4, 2022
0ed0234
++
toteki Aug 4, 2022
df91850
++
toteki Aug 4, 2022
28e905c
Merge branch 'main' into adam/liquidate
toteki Aug 4, 2022
3169527
Merge branch 'main' into adam/liquidate
mergify[bot] Aug 4, 2022
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1123](https://github.com/umee-network/umee/pull/1123) Shorten all leverage and oracle query structs by removing the Request suffix.
- [1125](https://github.com/umee-network/umee/pull/1125) Refactor: remove proto getters in x/leverage and x/oracle proto types.
- [1126](https://github.com/umee-network/umee/pull/1126) Update proto json tag from `APY` to `apy`.
- [1118](https://github.com/umee-network/umee/pull/1118) MsgLiquidate now has reward denom instead of full coin
- [1130](https://github.com/umee-network/umee/pull/1130) Update proto json tag to lower case.
- [1140](https://github.com/umee-network/umee/pull/1140) Rename MarketSize query to TotalSuppliedValue, and TokenMarketSize to TotalSupplied.
- [1188](https://github.com/umee-network/umee/pull/1188) Remove all individual queries which duplicate market_summary fields.
Expand All @@ -72,6 +73,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1094](https://github.com/umee-network/umee/pull/1094) Added TotalCollateral query.
- [1099](https://github.com/umee-network/umee/pull/1099) Added TotalBorrowed query.
- [1157](https://github.com/umee-network/umee/pull/1157) Added `PrintOrErr` util function optimizing the CLI code flow.
- [1118](https://github.com/umee-network/umee/pull/1118) MsgLiquidate rewards base assets instead of requiring an addtional MsgWithdraw
- [1159](https://github.com/umee-network/umee/pull/1159) Add `max_supply_utilization` and `min_collateral_liquidity` to the x/leverage token registry.
- [1188](https://github.com/umee-network/umee/pull/1188) Add `liquidity`, `maximum_borrow`, `maximum_collateral`, `minimum_liquidity`, `available_withdraw`, `available_collateralize`, and `utoken_supply` fields to market summary.
- [1203](https://github.com/umee-network/umee/pull/1203) Add Swagger docs.
Expand Down
33 changes: 24 additions & 9 deletions proto/umee/leverage/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ service Msg {
rpc Repay(MsgRepay) returns (MsgRepayResponse);

// Liquidate allows a user to repay a different user's borrowed coins in exchange for some
// of their collateral.
// of the target's collateral.
rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse);
}

Expand Down Expand Up @@ -85,15 +85,21 @@ message MsgRepay {
cosmos.base.v1beta1.Coin asset = 2 [(gogoproto.nullable) = false];
}

// MsgLiquidate represents a liquidator's request to repay a specific borrower's
// borrowed base asset type to the module in exchange for collateral reward.
// MsgLiquidate is the request structure for the Liquidate RPC.
message MsgLiquidate {
// Liquidator is the account address performing a liquidation and the signer
// of the message.
string liquidator = 1;
string borrower = 2;
cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin reward = 4 [(gogoproto.nullable) = false];
string liquidator = 1;
// Borrower is the account whose borrow is being repaid, and collateral consumed,
// by the liquidation. It does not sign the message.
string borrower = 2;
// Repayment is the maximum amount of base tokens that the liquidator is willing
// to repay.
cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false];
// RewardDenom is the base token denom that the liquidator is willing to accept
// as a liquidation reward. The uToken equivalent of any base token rewards
// will be taken from the borrower's collateral.
string reward_denom = 4;
Copy link
Member

Choose a reason for hiding this comment

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

We need to think more about this change and this PR. In particular, I think this should be repeated Coin, see: #1129

Copy link
Member Author

Choose a reason for hiding this comment

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

Considering the level of mathematical complexity that single-denom liquidations involve (see below), I prefer avoiding sdk.Coins in either repayment or reward.

It does increase the number of MsgLiquidate required for complex positions unfortunately, but keeps the edge cases much more understandable.

Copy link
Member

@robert-zaremba robert-zaremba Jul 13, 2022

Choose a reason for hiding this comment

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

In that case the complexity is moved to liquidator... he will need to calculate which denom he can liquidate and how much.

We are already adjusting the repayment. Instead we can go one by one of the collateral list and sum up to the value related to the repayment. We could use denom list to make it simpler.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the liquidator can do that - I had a script doing as much during the testnet, and what it does is proceed in order of preference for repay and reward denoms by offering the maximum amount it is willing to repay.

For most borrowers, even with multiple borrowed and collateral types, the first MsgLiquidate brings them back to health, so the liquidator's initial preference for reward and repay denoms is enough.

For borrowers where multiple transactions are needed, the liquidator only needs to follow their ranked order of preferences, because the first transaction will have either exhausted a repay denom or a reward denom by succeeding.

Note: Exhausted a denom can mean:

  • Repaid a borrow completely in one token denom
  • Reduced liquidator's token balance to zero (or consumed maximum offered amount) for a token denom
  • Reduced borrower's collateral position in a uToken denom to zero
  • Exhausted available supply of a reward token

Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test verifying if the liquidation of a single asset, even if it doesn't bring the account to a healthy state, is possible?

Copy link
Member Author

Choose a reason for hiding this comment

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

Liquidation logic is independent of final state (except for the collateral == 0 check for bad debt flagging) so that isn't a problem.

In particular, the test case labeled repayLimited covers this scenario in liquidate_test.go

}

// MsgSupplyResponse defines the Msg/Supply response type.
Expand All @@ -113,11 +119,20 @@ message MsgBorrowResponse {}

// MsgRepayResponse defines the Msg/Repay response type.
message MsgRepayResponse {
// Repaid is the amount of debt, in base tokens, that was repaid to the
// module by the borrower.
cosmos.base.v1beta1.Coin repaid = 1 [(gogoproto.nullable) = false];
}

// MsgLiquidateResponse defines the Msg/Liquidate response type.
message MsgLiquidateResponse {
cosmos.base.v1beta1.Coin repaid = 1 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin reward = 2 [(gogoproto.nullable) = false];
// Repaid is the amount of debt, in base tokens, that liquidator repaid
// to the module on behalf of the borrower.
cosmos.base.v1beta1.Coin repaid = 1 [(gogoproto.nullable) = false];
// Collateral is the amount of the borrower's uToken collateral that
// was converted to the reward tokens as a result of liquidation.
cosmos.base.v1beta1.Coin collateral = 2 [(gogoproto.nullable) = false];
// Reward is the amount of base tokens that the liquidator received from
// the module as reward for the liquidation.
cosmos.base.v1beta1.Coin reward = 3 [(gogoproto.nullable) = false];
toteki marked this conversation as resolved.
Show resolved Hide resolved
}
21 changes: 16 additions & 5 deletions x/leverage/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,20 @@ func GetCmdRepay() *cobra.Command {
// transaction with a MsgLiquidate message.
func GetCmdLiquidate() *cobra.Command {
cmd := &cobra.Command{
Use: "liquidate [liquidator] [borrower] [amount] [reward]",
Use: "liquidate [liquidator] [borrower] [amount] [reward-denom]",
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
Args: cobra.ExactArgs(4),
Short: "Liquidate a specified amount of a borrower's debt for a chosen reward denomination",
toteki marked this conversation as resolved.
Show resolved Hide resolved
Long: strings.TrimSpace(
fmt.Sprintf(`
Liquidate up to a specified amount of a borrower's debt for a chosen reward denomination.

Example:
$ umeed tx leverage liquidate %s %s 50000000uumee u/uumee --from mykey`,
"umee16jgsjqp7h0mpahlkw3p6vp90vd3jhn5tz6lcex",
"umee1qqy7cst5qm83ldupph2dcq0wypprkfpc9l3jg2",
),
),

RunE: func(cmd *cobra.Command, args []string) error {
if err := cmd.Flags().Set(flags.FlagFrom, args[0]); err != nil {
return err
Expand All @@ -266,13 +277,13 @@ func GetCmdLiquidate() *cobra.Command {
return err
}

reward, err := sdk.ParseCoinNormalized(args[3])
if err != nil {
rewardDenom := args[3]

msg := types.NewMsgLiquidate(clientCtx.GetFromAddress(), borrowerAddr, asset, rewardDenom)
if err = msg.ValidateBasic(); err != nil {
return err
}

msg := types.NewMsgLiquidate(clientCtx.GetFromAddress(), borrowerAddr, asset, reward)

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
Expand Down
59 changes: 24 additions & 35 deletions x/leverage/client/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
val.Address.String(),
val.Address.String(),
"5uumee",
"4uumee",
},
nil,
}

fixCollateral := testTransaction{
"add back collateral received from liquidation",
cli.GetCmdCollateralize(),
[]string{
val.Address.String(),
"4u/uumee",
"uumee",
},
nil,
}
Expand All @@ -186,7 +176,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
cli.GetCmdRepay(),
[]string{
val.Address.String(),
"51uumee",
"50uumee",
Comment on lines -189 to +179
Copy link
Member Author

Choose a reason for hiding this comment

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

Changes in rounding behavior have created a number of simpler decimals in this test file.

},
nil,
}
Expand All @@ -196,7 +186,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
cli.GetCmdDecollateralize(),
[]string{
val.Address.String(),
"1000u/uumee",
"950u/uumee",
},
nil,
}
Expand All @@ -206,14 +196,14 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
cli.GetCmdWithdraw(),
[]string{
val.Address.String(),
"1000u/uumee",
"950u/uumee",
},
nil,
}

nonzeroQueries := []TestCase{
testQuery{
"query account summary",
"query account balances",
cli.GetCmdQueryAccountBalances(),
[]string{
val.Address.String(),
Expand All @@ -222,18 +212,18 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
&types.QueryAccountBalancesResponse{},
&types.QueryAccountBalancesResponse{
Supplied: sdk.NewCoins(
sdk.NewInt64Coin(umeeapp.BondDenom, 1001),
sdk.NewInt64Coin(umeeapp.BondDenom, 1000),
),
Collateral: sdk.NewCoins(
sdk.NewInt64Coin(types.UTokenFromTokenDenom(umeeapp.BondDenom), 1000),
),
Borrowed: sdk.NewCoins(
sdk.NewInt64Coin(umeeapp.BondDenom, 47),
sdk.NewInt64Coin(umeeapp.BondDenom, 51),
),
},
},
testQuery{
"query account health",
"query account summary",
cli.GetCmdQueryAccountSummary(),
[]string{
val.Address.String(),
Expand All @@ -244,16 +234,16 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
// This result is umee's oracle exchange rate from
// app/test_helpers.go/IntegrationTestNetworkConfig
// times the amount of umee, and then times params
// (1001 / 1000000) * 34.21 = 0.03424421
SuppliedValue: sdk.MustNewDecFromStr("0.03424421"),
// (1001 / 1000000) * 34.21 = 0.03424421
CollateralValue: sdk.MustNewDecFromStr("0.03424421"),
// (47 / 1000000) * 34.21 = 0.00160787
BorrowedValue: sdk.MustNewDecFromStr("0.00160787"),
// (1001 / 1000000) * 34.21 * 0.05 = 0.0017122105
BorrowLimit: sdk.MustNewDecFromStr("0.0017122105"),
// (1001 / 1000000) * 0.05 * 34.21 = 0.0017122105
LiquidationThreshold: sdk.MustNewDecFromStr("0.0017122105"),
// (1000 / 1000000) * 34.21 = 0.03421
SuppliedValue: sdk.MustNewDecFromStr("0.03421"),
// (1000 / 1000000) * 34.21 = 0.03421
CollateralValue: sdk.MustNewDecFromStr("0.03421"),
// (51 / 1000000) * 34.21 = 0.00174471
BorrowedValue: sdk.MustNewDecFromStr("0.00174471"),
// (1000 / 1000000) * 34.21 * 0.05 = 0.0017105
BorrowLimit: sdk.MustNewDecFromStr("0.0017105"),
// (1000 / 1000000) * 0.05 * 34.21 = 0.0017105
LiquidationThreshold: sdk.MustNewDecFromStr("0.0017105"),
},
},
}
Expand All @@ -266,17 +256,16 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
supply,
addCollateral,
borrow,
liquidate,
fixCollateral,
)

// These transactions are deferred to run after nonzero queries are finished
defer s.runTestCases(
// These queries run while the supplying and borrowing is active to produce nonzero output
s.runTestCases(nonzeroQueries...)

// These transactions run after nonzero queries are finished
s.runTestCases(
liquidate,
repay,
removeCollateral,
withdraw,
)

// These queries run while the supplying and borrowing is active to produce nonzero output
s.runTestCases(nonzeroQueries...)
}
36 changes: 21 additions & 15 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,21 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk
return sdk.ZeroDec(), err
}

// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset)
if err != nil {
return sdk.ZeroDec(), err
}

ts, err := k.GetTokenSettings(ctx, baseAsset.Denom)
if err != nil {
return sdk.ZeroDec(), err
}

// add each collateral coin's weighted value to borrow limit
limit = limit.Add(v.Mul(ts.CollateralWeight))
// ignore blacklisted tokens
if !ts.Blacklist {
// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset)
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
Expand All @@ -120,18 +122,22 @@ func (k Keeper) CalculateLiquidationThreshold(ctx sdk.Context, collateral sdk.Co
return sdk.ZeroDec(), err
}

// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset)
if err != nil {
return sdk.ZeroDec(), err
}

ts, err := k.GetTokenSettings(ctx, baseAsset.Denom)
if err != nil {
return sdk.ZeroDec(), err
}

totalThreshold = totalThreshold.Add(v.Mul(ts.LiquidationThreshold))
// ignore blacklisted tokens
if !ts.Blacklist {
// get USD value of base assets
v, err := k.TokenValue(ctx, baseAsset)
if err != nil {
return sdk.ZeroDec(), err
}

// add each collateral coin's weighted value to liquidation threshold
totalThreshold = totalThreshold.Add(v.Mul(ts.LiquidationThreshold))
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
}
}

return totalThreshold, nil
Expand Down
26 changes: 26 additions & 0 deletions x/leverage/keeper/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// filterCoins returns the subset of an sdk.Coins that meet a given condition
func (k Keeper) filterCoins(coins sdk.Coins, accept func(sdk.Coin) bool) sdk.Coins {
filtered := sdk.Coins{}
for _, c := range coins {
if accept(c) {
filtered = append(filtered, c)
}
}
return filtered
}

// filterAcceptedCoins returns the subset of an sdk.Coins that are accepted, non-blacklisted tokens
func (k Keeper) filterAcceptedCoins(ctx sdk.Context, coins sdk.Coins) sdk.Coins {
return k.filterCoins(
coins,
func(c sdk.Coin) bool {
return k.validateAcceptedAsset(ctx, c) == nil
},
)
}
Loading