Skip to content

Commit

Permalink
feat!: liquidation directly rewards base assets (#1118)
Browse files Browse the repository at this point in the history
## Description

Significant Change:
- Instead of receiving uToken rewards on `MsgLiquidate`, liquidator receives equivalent base assets.

Reasoning:

We're about to add `MaxCollateralUtilization` which restricts `MsgWithdraw` at critical times, so `AvailableSupply` can be used for liquidation.

But if liquidators receive their rewards as uTokens, then the `MsgWithdraw` restriction will apply to them just like any other user, and they will be unable to receive their base tokens.

Switching to base assets received eliminates makes `MaxCollateralUtilization` work as intended, and also simplifies the `MsgLiquidate -> MsgWithdraw -> IBC (to sell reward)` liquidation workflow to just `MsgLiquidate -> IBC`

---

API Breaking:
- Reverts `MsgLiquidate`'s reward `sdk.Coin` field to reward_denom `string`

Reasoning:

That field was for rare cases where liquidators do not trust the umee `x/oracle` and want to put their own price floors. Such cases should be handled off-chain (just query prices) rather than adding a computation and required field for all users. See #828

---

closes: #898 
closes: #828

relevant to #831 - because rounding up repayment and reward eliminates most "dust" liquidation remainders

---

### 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))
- [x] 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)
- [x] updated the relevant documentation or specification
- [x] reviewed "Files changed" and left comments if necessary
- [x] 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)
  • Loading branch information
toteki committed Aug 4, 2022
1 parent e1a6f1f commit af827ba
Show file tree
Hide file tree
Showing 23 changed files with 818 additions and 524 deletions.
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;
}

// 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];
}
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]",
Args: cobra.ExactArgs(4),
Short: "Liquidate a specified amount of a borrower's debt for a chosen reward denomination",
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",
},
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))
}
}

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

0 comments on commit af827ba

Please sign in to comment.