From af827ba29514cbbfa5988bdf10e484b49aac9467 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 4 Aug 2022 08:56:39 -0700 Subject: [PATCH] feat!: liquidation directly rewards base assets (#1118) ## 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) --- CHANGELOG.md | 2 + proto/umee/leverage/v1/tx.proto | 33 ++- x/leverage/client/cli/tx.go | 21 +- x/leverage/client/tests/tests.go | 59 +++-- x/leverage/keeper/borrows.go | 36 ++-- x/leverage/keeper/filter.go | 26 +++ x/leverage/keeper/keeper.go | 263 ++++++----------------- x/leverage/keeper/keeper_test.go | 89 +------- x/leverage/keeper/liquidate.go | 216 +++++++++++++++++++ x/leverage/keeper/liquidate_test.go | 200 +++++++++++++++++ x/leverage/keeper/msg_server.go | 34 +-- x/leverage/keeper/oracle.go | 45 ++-- x/leverage/keeper/oracle_test.go | 18 +- x/leverage/keeper/token.go | 13 -- x/leverage/keeper/validate.go | 33 ++- x/leverage/simulation/operations.go | 2 +- x/leverage/simulation/operations_test.go | 2 +- x/leverage/spec/04_messages.md | 18 +- x/leverage/types/errors.go | 9 +- x/leverage/types/events.go | 2 + x/leverage/types/token.go | 12 +- x/leverage/types/tx.go | 16 +- x/leverage/types/tx.pb.go | 193 +++++++++++------ 23 files changed, 818 insertions(+), 524 deletions(-) create mode 100644 x/leverage/keeper/filter.go create mode 100644 x/leverage/keeper/liquidate.go create mode 100644 x/leverage/keeper/liquidate_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 66cd660f1f..355cb89a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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. diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index 53239e9938..95554057f0 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -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); } @@ -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. @@ -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]; } diff --git a/x/leverage/client/cli/tx.go b/x/leverage/client/cli/tx.go index 41fd56d304..8ca4507354 100644 --- a/x/leverage/client/cli/tx.go +++ b/x/leverage/client/cli/tx.go @@ -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 @@ -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) }, } diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index e5e4268623..f7bedbd573 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -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, } @@ -186,7 +176,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { cli.GetCmdRepay(), []string{ val.Address.String(), - "51uumee", + "50uumee", }, nil, } @@ -196,7 +186,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { cli.GetCmdDecollateralize(), []string{ val.Address.String(), - "1000u/uumee", + "950u/uumee", }, nil, } @@ -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(), @@ -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(), @@ -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"), }, }, } @@ -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...) } diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index bcfffd08db..975c46462e 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -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 @@ -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 diff --git a/x/leverage/keeper/filter.go b/x/leverage/keeper/filter.go new file mode 100644 index 0000000000..864c958273 --- /dev/null +++ b/x/leverage/keeper/filter.go @@ -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 + }, + ) +} diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 373840cdd7..fac6330095 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -346,226 +346,83 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, co return nil } -// LiquidateBorrow attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange -// for a selected denomination of uToken collateral, specified by its associated token denom. The liquidator -// may also specify a minimum reward amount, again in base token denom that will be adjusted by uToken exchange -// rate, they would accept for the specified repayment. If the borrower is not over their liquidation limit, or -// the repayment or reward denominations are invalid, an error is returned. If the attempted repayment -// is greater than the amount owed or the maximum that can be repaid due to parameters (close factor) -// then a partial liquidation, equal to the maximum valid amount, is performed. The same occurs if the -// value of collateral in the selected reward denomination cannot cover the proposed repayment. -// Because partial liquidation is possible and exchange rates vary, LiquidateBorrow returns the actual -// amount of tokens repaid and uTokens rewarded (in that order). -func (k Keeper) LiquidateBorrow( - ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, desiredRepayment, desiredReward sdk.Coin, -) (sdk.Int, sdk.Int, error) { - if !desiredRepayment.IsValid() { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrInvalidAsset.Wrap(desiredRepayment.String()) - } - if err := k.AssertNotBlacklisted(ctx, desiredRepayment.Denom); err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - if !k.IsAcceptedToken(ctx, desiredReward.Denom) { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrInvalidAsset.Wrap(desiredReward.String()) - } - - collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) - borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr) - - borrowValue, err := k.TotalTokenValue(ctx, borrowed) // total borrowed value in USD - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, collateral) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - // confirm borrower's eligibility for liquidation - if liquidationThreshold.GTE(borrowValue) { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrLiquidationIneligible.Wrapf( - "%s borrowed value is below the liquidation threshold %s", borrowerAddr, liquidationThreshold) - } - - // get reward-specific incentive and dynamic close factor - baseRewardDenom := desiredReward.Denom - liquidationIncentive, closeFactor, err := k.LiquidationParams(ctx, baseRewardDenom, borrowValue, liquidationThreshold) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - // actual repayment starts at desiredRepayment but can be lower due to limiting factors - repayment := desiredRepayment - - // get liquidator's available balance of base asset to repay - liquidatorBalance := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(repayment.Denom) - - // repayment cannot exceed liquidator's available balance - repayment.Amount = sdk.MinInt(repayment.Amount, liquidatorBalance) - - // repayment cannot exceed borrower's borrowed amount of selected denom - repayment.Amount = sdk.MinInt(repayment.Amount, borrowed.AmountOf(repayment.Denom)) - - // repayment cannot exceed borrowed value * close factor - maxRepayValue := borrowValue.Mul(closeFactor) - repayValue, err := k.TokenValue(ctx, repayment) +// Liquidate attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange +// for a the base token equivalent of selected denomination of the borrower's uToken collateral. If the +// borrower is not over their liquidation limit, or the repayment or reward denominations are invalid, +// an error is returned. If the attempted repayment is greater than the amount owed or the maximum that +// can be repaid due to parameters or available balances, then a partial liquidation, equal to the maximum +// valid amount, is performed. Because partial liquidation is possible and exchange rates vary, Liquidate +// returns the actual amount of tokens repaid, uTokens consumed, and base tokens rewarded. +func (k Keeper) Liquidate( + ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, maxRepay sdk.Coin, rewardDenom string, +) (baseRepay sdk.Coin, collateralReward sdk.Coin, baseReward sdk.Coin, err error) { + if err := k.validateAcceptedAsset(ctx, maxRepay); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + if err := k.validateAcceptedDenom(ctx, rewardDenom); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + + // calculate borrowed Token repay, uToken collateral, and Token reward amounts allowed by liquidation rules and available balances + baseRepay, collateralReward, baseReward, err = k.getLiquidationAmounts( + ctx, + liquidatorAddr, + borrowerAddr, + maxRepay, + rewardDenom, + ) if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - - if repayValue.GT(maxRepayValue) { - // repayment *= (maxRepayValue / repayValue) - repayment.Amount = repayment.Amount.ToDec().Mul(maxRepayValue).Quo(repayValue).TruncateInt() + if baseRepay.IsZero() { + // Zero repay amount returned from liquidation computation means the target was eligible for liquidation + // but the proposed reward and repayment would have zero effect. + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationInvalid } - // Given repay denom and amount, use oracle to find equivalent amount of rewardDenom. - baseReward, err := k.EquivalentTokenValue(ctx, repayment, baseRewardDenom) + // send repayment from liquidator to leverage module account + err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, liquidatorAddr, types.ModuleName, sdk.NewCoins(baseRepay)) if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - - // convert reward tokens back to uTokens - reward, err := k.ExchangeToken(ctx, baseReward) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + // update borrower's remaining borrowed amount + newBorrow := k.GetBorrow(ctx, borrowerAddr, baseRepay.Denom).Amount.Sub(baseRepay.Amount) + if err = k.setBorrow(ctx, borrowerAddr, sdk.NewCoin(baseRepay.Denom, newBorrow)); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // apply liquidation incentive - reward.Amount = reward.Amount.ToDec().Mul(sdk.OneDec().Add(liquidationIncentive)).TruncateInt() - - maxReward := collateral.AmountOf(reward.Denom) - if maxReward.IsZero() { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrInvalidAsset.Wrapf( - "borrower doesn't have %s as a collateral", desiredReward.Denom) + // reduce borrower's collateral by collateral reward amount + oldCollateral := k.GetCollateralAmount(ctx, borrowerAddr, collateralReward.Denom) + newCollateral := sdk.NewCoin(collateralReward.Denom, oldCollateral.Amount.Sub(collateralReward.Amount)) + if err = k.setCollateralAmount(ctx, borrowerAddr, newCollateral); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - - // reward amount cannot exceed available collateral - if reward.Amount.GT(maxReward) { - // reduce repayment.Amount to the maximum value permitted by the available collateral reward - repayment.Amount = repayment.Amount.Mul(maxReward).Quo(reward.Amount) - reward.Amount = maxReward + // burn the collateral reward uTokens and set the new total uToken supply + if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(collateralReward)); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - - // final check for invalid liquidation (negative/zero value after reductions above) - if !repayment.Amount.IsPositive() { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrInvalidAsset.Wrap(repayment.String()) + if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, collateralReward.Denom).Sub(collateralReward)); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - if desiredReward.Amount.IsPositive() { - // user-controlled minimum ratio of reward to repayment, expressed in collateral base assets (not uTokens) - rewardTokenEquivalent, err := k.ExchangeUToken(ctx, reward) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - minimumRewardRatio := sdk.NewDecFromInt(desiredReward.Amount).QuoInt(desiredRepayment.Amount) - actualRewardRatio := sdk.NewDecFromInt(rewardTokenEquivalent.Amount).QuoInt(repayment.Amount) - if actualRewardRatio.LT(minimumRewardRatio) { - return sdk.ZeroInt(), sdk.ZeroInt(), types.ErrLiquidationRewardRatio - } - } - - // send repayment to leverage module account - if err = k.bankKeeper.SendCoinsFromAccountToModule( - ctx, liquidatorAddr, - types.ModuleName, - sdk.NewCoins(repayment), - ); err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + // send base rewards from module to liquidator's account + err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, liquidatorAddr, sdk.NewCoins(baseReward)) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // update the remaining borrowed amount - owed := borrowed.AmountOf(repayment.Denom).Sub(repayment.Amount) - if err = k.setBorrow(ctx, borrowerAddr, sdk.NewCoin(repayment.Denom, owed)); err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } + // get remaining collateral, ignoring blacklisted + remainingCollateral := k.filterAcceptedCoins(ctx, k.GetBorrowerCollateral(ctx, borrowerAddr)) - // Reduce borrower collateral by reward amount - newBorrowerCollateral := sdk.NewCoin(reward.Denom, maxReward.Sub(reward.Amount)) - if err = k.setCollateralAmount(ctx, borrowerAddr, newBorrowerCollateral); err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - // Send rewards to liquidator's account. - err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, liquidatorAddr, sdk.NewCoins(reward)) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } - - // Detect bad debt (collateral == 0 after reward) for repayment by protocol reserves - if collateral.Sub(sdk.NewCoins(reward)).IsZero() { - for _, coin := range borrowed { - // Mark repayment denom as bad debt only if some debt remains after - // this liquidation. All other borrowed denoms were definitely not - // repaid in this liquidation so they are always marked as bad debt. - if coin.Denom != repayment.Denom || owed.IsPositive() { - if err := k.setBadDebtAddress(ctx, borrowerAddr, coin.Denom, true); err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err - } + // detect bad debt if collateral is completely exhausted + if remainingCollateral.IsZero() { + for _, coin := range k.GetBorrowerBorrows(ctx, borrowerAddr) { + // set a bad debt flag for each borrowed denom + if err := k.setBadDebtAddress(ctx, borrowerAddr, coin.Denom, true); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } } } - return repayment.Amount, reward.Amount, nil -} - -// LiquidationParams computes dynamic liquidation parameters based on collateral denomination, -// borrowed value, and liquidation threshold. Returns liquidationIncentive (the ratio of bonus collateral -// awarded during Liquidate transactions, and closeFactor (the fraction of a borrower's total -// borrowed value that can be repaid by a liquidator in a single liquidation event.) -func (k Keeper) LiquidationParams( - ctx sdk.Context, - rewardDenom string, - borrowed sdk.Dec, - limit sdk.Dec, -) (sdk.Dec, sdk.Dec, error) { - if borrowed.IsNegative() { - return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, borrowed.String()) - } - if limit.IsNegative() { - return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, limit.String()) - } - - ts, err := k.GetTokenSettings(ctx, rewardDenom) - if err != nil { - return sdk.ZeroDec(), sdk.ZeroDec(), err - } - - // special case: If liquidation threshold is zero, close factor is always 1 - if limit.IsZero() { - return ts.LiquidationIncentive, sdk.OneDec(), nil - } - - params := k.GetParams(ctx) - - // special case: If borrowed value is less than small liquidation size, - // close factor is always 1 - if borrowed.LTE(params.SmallLiquidationSize) { - return ts.LiquidationIncentive, sdk.OneDec(), nil - } - - // special case: If complete liquidation threshold is zero, close factor is always 1 - if params.CompleteLiquidationThreshold.IsZero() { - return ts.LiquidationIncentive, sdk.OneDec(), nil - } - - // outside of special cases, close factor scales linearly between MinimumCloseFactor and 1.0, - // reaching max value when (borrowed / threshold) = 1 + CompleteLiquidationThreshold - var closeFactor sdk.Dec - closeFactor = Interpolate( - borrowed.Quo(limit).Sub(sdk.OneDec()), // x - sdk.ZeroDec(), // xMin - params.MinimumCloseFactor, // yMin - params.CompleteLiquidationThreshold, // xMax - sdk.OneDec(), // yMax - ) - if closeFactor.GTE(sdk.OneDec()) { - closeFactor = sdk.OneDec() - } - if closeFactor.IsNegative() { - closeFactor = sdk.ZeroDec() - } - - return ts.LiquidationIncentive, closeFactor, nil + return baseRepay, collateralReward, baseReward, nil } diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index d418eb6ac3..56e4976ed5 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -264,7 +264,7 @@ func (s *IntegrationTestSuite) TestGetToken() { s.Require().NoError(t.AssertBorrowEnabled()) s.Require().NoError(t.AssertSupplyEnabled()) - s.Require().NoError(s.app.LeverageKeeper.AssertNotBlacklisted(s.ctx, "uabc")) + s.Require().NoError(t.AssertNotBlacklisted()) } // initialize the common starting scenario from which borrow and repay tests stem: @@ -559,93 +559,6 @@ func (s *IntegrationTestSuite) TestRepay_Overpay() { s.Require().Error(err) } -func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { - addr, _ := s.initBorrowScenario() - app, ctx := s.app, s.ctx - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - - // user borrows 90 umee - err := s.app.LeverageKeeper.Borrow(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 90000000)) - s.Require().NoError(err) - - // create an account and address which will represent a liquidator - liquidatorAddr := sdk.AccAddress([]byte("addr______________03")) - liquidatorAcc := app.AccountKeeper.NewAccountWithAddress(ctx, liquidatorAddr) - app.AccountKeeper.SetAccount(ctx, liquidatorAcc) - - // mint and send 10k umee to liquiator - s.Require().NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee - )) - s.Require().NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, liquidatorAddr, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee, - )) - - // liquidator attempts to liquidate user, but user is ineligible (not over borrow limit) - // liquidator does not specify a minimum reward (hence 0 u/umee) - repayment := sdk.NewInt64Coin(umeeapp.BondDenom, 30000000) // 30 umee - rewardDenom := s.app.LeverageKeeper.FromTokenToUTokenDenom(ctx, umeeapp.BondDenom) - unrestrictedReward := sdk.NewInt64Coin(umeeapp.BondDenom, 0) // 0 umee (rewardDenom = u/umee) - _, _, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, unrestrictedReward) - s.Require().Error(err) - - // Note: Setting umee collateral weight to 0.0 to allow liquidation - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // liquidator attempts to liquidate user, but specifies too high of a minimum reward - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 10000000) // 10 umee - excessiveReward := sdk.NewInt64Coin(umeeapp.BondDenom, 20000000) // 20 umee (rewardDenom = u/umee) - _, _, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, excessiveReward) - s.Require().Error(err) - - // liquidator partially liquidates user, receiving some collateral - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 10000000) // 10 umee - repaid, reward, err := s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, unrestrictedReward) - s.Require().NoError(err) - s.Require().Equal(repayment.Amount, repaid) - s.Require().Equal(sdk.NewInt(11000000), reward) - - // verify user's new loan amount is 80 umee (still over borrow limit) - loanBalance := s.app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance.String(), sdk.NewInt64Coin(umeeapp.BondDenom, 80000000).String()) - - // verify liquidator's new u/umee balance = 11 = (10 + liquidation incentive) - uTokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 11000000)) - - // verify liquidator's new umee balance (10k - 10) = 9990 umee - tokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9990000000)) - - // liquidator fully liquidates user, receiving more collateral and reducing borrowed amount to zero - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 300000000) // 300 umee - repaid, reward, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, unrestrictedReward) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt(80000000), repaid) - s.Require().Equal(sdk.NewInt(88000000), reward) - - // verify that repayment has not been modified - s.Require().Equal(sdk.NewInt(300000000), repayment.Amount) - - // verify liquidator's new u/umee balance = 99 = (90 + liquidation incentive) - uTokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 99000000)) - - // verify user's new loan amount is zero - loanBalance = s.app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 0)) - - // verify liquidator's new umee balance (10k - 90) = 9910 umee - tokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9910000000)) -} - func (s *IntegrationTestSuite) TestRepayBadDebt() { // Creating a supplier so module account has some uumee _ = s.setupAccount(umeeDenom, 200000000, 200000000, 0, false) // 200 umee diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go new file mode 100644 index 0000000000..26813f5ffc --- /dev/null +++ b/x/leverage/keeper/liquidate.go @@ -0,0 +1,216 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v2/x/leverage/types" +) + +// getLiquidationAmounts takes a repayment and reward denom proposed by a liquidator and calculates +// the actual repayment amount a target address is eligible for, and the corresponding collateral +// to burn and rewards to return to the liquidator. +func (k Keeper) getLiquidationAmounts( + ctx sdk.Context, + liquidatorAddr sdk.AccAddress, + targetAddr sdk.AccAddress, + maxRepay sdk.Coin, + rewardDenom string, +) (tokenRepay sdk.Coin, collateralBurn sdk.Coin, tokenReward sdk.Coin, err error) { + repayDenom := maxRepay.Denom + collateralDenom := k.FromTokenToUTokenDenom(ctx, rewardDenom) + + // get relevant liquidator, borrower, and module balances + borrowerCollateral := k.GetBorrowerCollateral(ctx, targetAddr) + totalBorrowed := k.GetBorrowerBorrows(ctx, targetAddr) + availableRepay := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(repayDenom) + + // calculate borrower health in USD values + borrowedValue, err := k.TotalTokenValue(ctx, totalBorrowed) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, borrowerCollateral) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + if liquidationThreshold.GTE(borrowedValue) { + // borrower is healthy and cannot be liquidated + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationIneligible + } + + // get liquidation incentive + ts, err := k.GetTokenSettings(ctx, rewardDenom) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + + // get dynamic close factor + params := k.GetParams(ctx) + closeFactor := ComputeCloseFactor( + borrowedValue, + liquidationThreshold, + params.SmallLiquidationSize, + params.MinimumCloseFactor, + params.CompleteLiquidationThreshold, + ) + + // get oracle prices for the reward and repay denoms + repayTokenPrice, err := k.TokenPrice(ctx, repayDenom) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + rewardTokenPrice, err := k.TokenPrice(ctx, rewardDenom) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + + // get collateral uToken exchange rate + exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) + + // compute final liquidation amounts + repay, burn, reward := ComputeLiquidation( + sdk.MinInt(sdk.MinInt(availableRepay, maxRepay.Amount), totalBorrowed.AmountOf(repayDenom)), + borrowerCollateral.AmountOf(collateralDenom), + k.ModuleBalance(ctx, rewardDenom).Sub(k.GetReserveAmount(ctx, rewardDenom)), + repayTokenPrice, + rewardTokenPrice, + exchangeRate, + ts.LiquidationIncentive, + closeFactor, + borrowedValue, + ) + + return sdk.NewCoin(repayDenom, repay), sdk.NewCoin(collateralDenom, burn), sdk.NewCoin(rewardDenom, reward), nil +} + +// ComputeLiquidation takes the conditions preceding a liquidation and outputs the amounts +// of base token that should be repaid, collateral uToken burned, and reward token allocated +// as a result of the transaction, after accounting for limiting factors with as little +// rounding as possible. Inputs are as follows: +// - availableRepay: The lowest (in repay denom) of either liquidator balance, max repayment, or borrowed amount. +// - availableCollateral: The amount of the reward uToken denom which borrower has as collateral +// - availableReward: The amount of unreserved reward tokens in the module balance +// - repayTokenPrice: The oracle price of the base repayment denom +// - rewardTokenPrice: The oracle price of the base reward denom +// - uTokenExchangeRate: The uToken exchange rate from collateral uToken denom to reward base denom +// - liquidationIncentive: The liquidation incentive of the token reward denomination +// - closeFactor: The dynamic close factor computed from the borrower's borrowed value and liquidation threshold +// - borrowedValue: The borrower's borrowed value in USD +func ComputeLiquidation( + availableRepay, + availableCollateral, + availableReward sdk.Int, + repayTokenPrice, + rewardTokenPrice, + uTokenExchangeRate, + liquidationIncentive, + closeFactor, + borrowedValue sdk.Dec, +) (tokenRepay sdk.Int, collateralBurn sdk.Int, tokenReward sdk.Int) { + // Prevent division by zero + if uTokenExchangeRate.IsZero() || rewardTokenPrice.IsZero() || repayTokenPrice.IsZero() { + return sdk.ZeroInt(), sdk.ZeroInt(), sdk.ZeroInt() + } + + // Start with the maximum possible repayment amount, as a decimal + maxRepay := availableRepay.ToDec() + // Determine the base maxReward amount that would result from maximum repayment + maxReward := maxRepay.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) + // Determine the maxCollateral burn amount that corresponds to base reward amount + maxCollateral := maxReward.Quo(uTokenExchangeRate) + + // Catch no-ops early + if maxRepay.IsZero() || maxReward.IsZero() || maxCollateral.IsZero() || closeFactor.IsZero() || borrowedValue.IsZero() { + return sdk.ZeroInt(), sdk.ZeroInt(), sdk.ZeroInt() + } + + // We will track limiting factors by the ratio by which the max repayment would need to be reduced to comply + ratio := sdk.OneDec() + // Repaid value cannot exceed borrowed value times close factor + ratio = sdk.MinDec(ratio, + borrowedValue.Mul(closeFactor).Quo(maxRepay.Mul(repayTokenPrice)), + ) + // Collateral burned cannot exceed borrower's collateral + ratio = sdk.MinDec(ratio, + availableCollateral.ToDec().Quo(maxCollateral), + ) + // Base token reward cannot exceed available unreserved module balance + ratio = sdk.MinDec(ratio, + availableReward.ToDec().Quo(maxReward), + ) + // Catch edge cases + ratio = sdk.MaxDec(ratio, sdk.ZeroDec()) + + // Reduce repay and collateral limits by the most severe limiting factor encountered + maxRepay = maxRepay.Mul(ratio) + maxCollateral = maxCollateral.Mul(ratio) + + // No rounding has occurred yet, but both values are now within the + // limits defined by available balances and module parameters. + + // First, the amount of borrowed token the liquidator must repay is rounded up. + // This is a slight disadvantage to the liquidator in favor of the borrower and + // the module. It also ensures borrow dust is always eliminated when encountered. + tokenRepay = maxRepay.Ceil().RoundInt() + + // Next, the amount of collateral uToken the borrower will lose is rounded down. + // This is favors the borrower over the liquidator, and also protects the module. + collateralBurn = maxCollateral.TruncateInt() + + // One danger to rounding collateral burn down is that of collateral dust. This + // can be considered in two scenarios: + // 1) If collateral was the limiting factor above, then it will have already been + // an integer amount and truncating is a no-op. + // 2) If collateral was not the limiting factor, then there will be a non-dust + // quantity left over anyway. + + // Finally, the base token reward amount is derived directly from the collateral + // to burn. This will round down identically to MsgWithdraw, favoring the module + // over the liquidator. + tokenReward = collateralBurn.ToDec().Mul(uTokenExchangeRate).TruncateInt() + + return tokenRepay, collateralBurn, tokenReward +} + +// ComputeCloseFactor derives the maximum portion of a borrower's current +// borrowed value can currently be repaid in a single liquidate transaction. +func ComputeCloseFactor( + borrowedValue sdk.Dec, + liquidationThreshold sdk.Dec, + smallLiquidationSize sdk.Dec, + minimumCloseFactor sdk.Dec, + completeLiquidationThreshold sdk.Dec, +) (closeFactor sdk.Dec) { + if !liquidationThreshold.IsPositive() || borrowedValue.LTE(liquidationThreshold) { + // Not eligible for liquidation + return sdk.ZeroDec() + } + + if borrowedValue.LTE(smallLiquidationSize) { + // Small enough borrows should be liquidated completely to reduce dust + return sdk.OneDec() + } + + if completeLiquidationThreshold.IsZero() { + // If close factor is set to unlimited by global params + return sdk.OneDec() + } + + // outside of special cases, close factor scales linearly between MinimumCloseFactor and 1.0, + // reaching max value when (borrowed / threshold) = 1 + CompleteLiquidationThreshold + closeFactor = Interpolate( + borrowedValue.Quo(liquidationThreshold).Sub(sdk.OneDec()), // x + sdk.ZeroDec(), // xMin + minimumCloseFactor, // yMin + completeLiquidationThreshold, // xMax + sdk.OneDec(), // yMax + ) + if closeFactor.GTE(sdk.OneDec()) { + closeFactor = sdk.OneDec() + } + if closeFactor.IsNegative() { + closeFactor = sdk.ZeroDec() + } + + return closeFactor +} diff --git a/x/leverage/keeper/liquidate_test.go b/x/leverage/keeper/liquidate_test.go new file mode 100644 index 0000000000..2ff8553ea2 --- /dev/null +++ b/x/leverage/keeper/liquidate_test.go @@ -0,0 +1,200 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/umee-network/umee/v2/x/leverage/keeper" +) + +func TestComputeLiquidation(t *testing.T) { + type testCase struct { + availableRepay sdk.Int + availableCollateral sdk.Int + availableReward sdk.Int + repayTokenPrice sdk.Dec + rewardTokenPrice sdk.Dec + uTokenExchangeRate sdk.Dec + liquidationIncentive sdk.Dec + closeFactor sdk.Dec + borrowedValue sdk.Dec + } + + baseCase := func() testCase { + return testCase{ + sdk.NewInt(1000), // 1000 Token A to repay + sdk.NewInt(5000), // 5000 uToken B collateral + sdk.NewInt(5000), // 5000 Token B liquidity + sdk.OneDec(), // price(A) = $1 + sdk.OneDec(), // price(B) = $1 + sdk.OneDec(), // utoken exchange rate 1 u/B => 1 B + sdk.MustNewDecFromStr("0.1"), // reward value is 110% repay value + sdk.OneDec(), // unlimited close factor + sdk.MustNewDecFromStr("10000"), // $10000 borrowed value + } + } + + runTestCase := func(tc testCase, expectedRepay, expectedCollateral, expectedReward int64, msg string) { + repay, collateral, reward := keeper.ComputeLiquidation( + tc.availableRepay, + tc.availableCollateral, + tc.availableReward, + tc.repayTokenPrice, + tc.rewardTokenPrice, + tc.uTokenExchangeRate, + tc.liquidationIncentive, + tc.closeFactor, + tc.borrowedValue, + ) + + require.Equal(t, sdk.NewInt(expectedRepay), repay, msg+" (repay)") + require.Equal(t, sdk.NewInt(expectedCollateral), collateral, msg+" (collateral)") + require.Equal(t, sdk.NewInt(expectedReward), reward, msg+" (reward)") + } + + // basic liquidation of 1000 borrowed tokens with plenty of available rewards and collateral + runTestCase(baseCase(), 1000, 1100, 1100, "base case") + + // borrower is healthy (as implied by a close factor of zero) so liquidation cannot occur + healthyCase := baseCase() + healthyCase.closeFactor = sdk.ZeroDec() + runTestCase(healthyCase, 0, 0, 0, "healthy borrower") + + // limiting factor is available repay + repayLimited := baseCase() + repayLimited.availableRepay = sdk.NewInt(100) + runTestCase(repayLimited, 100, 110, 110, "repay limited") + + // limiting factor is available collateral + collateralLimited := baseCase() + collateralLimited.availableCollateral = sdk.NewInt(220) + runTestCase(collateralLimited, 200, 220, 220, "collateral limited") + + // limiting factor is available reward + rewardLimited := baseCase() + rewardLimited.availableReward = sdk.NewInt(330) + runTestCase(rewardLimited, 300, 330, 330, "reward limited") + + // repay token is worth more + expensiveRepay := baseCase() + expensiveRepay.repayTokenPrice = sdk.MustNewDecFromStr("2") + runTestCase(expensiveRepay, 1000, 2200, 2200, "expensive repay") + + // reward token is worth more + expensiveReward := baseCase() + expensiveReward.rewardTokenPrice = sdk.MustNewDecFromStr("2") + runTestCase(expensiveReward, 1000, 550, 550, "expensive reward") + + // high collateral uToken exchange rate + exchangeRate := baseCase() + exchangeRate.uTokenExchangeRate = sdk.MustNewDecFromStr("2") + runTestCase(exchangeRate, 1000, 550, 1100, "high uToken exchange rate") + + // high liquidation incentive + highIncentive := baseCase() + highIncentive.liquidationIncentive = sdk.MustNewDecFromStr("1.5") + runTestCase(highIncentive, 1000, 2500, 2500, "high liquidation incentive") + + // no liquidation incentive + noIncentive := baseCase() + noIncentive.liquidationIncentive = sdk.ZeroDec() + runTestCase(noIncentive, 1000, 1000, 1000, "no liquidation incentive") + + // partial close factor + partialClose := baseCase() + partialClose.closeFactor = sdk.MustNewDecFromStr("0.03") + runTestCase(partialClose, 300, 330, 330, "close factor") + + // lowered borrowed value + lowValue := baseCase() + lowValue.borrowedValue = sdk.MustNewDecFromStr("700") + runTestCase(lowValue, 700, 770, 770, "lowered borrowed value") + + // complex case, limited by available repay, with various nontrivial values + complexCase := baseCase() + complexCase.availableRepay = sdk.NewInt(300) + complexCase.uTokenExchangeRate = sdk.MustNewDecFromStr("2.5") + complexCase.liquidationIncentive = sdk.MustNewDecFromStr("0.5") + complexCase.repayTokenPrice = sdk.MustNewDecFromStr("6") + complexCase.rewardTokenPrice = sdk.MustNewDecFromStr("12") + // repay = 300 (limiting factor) + // collateral = 300 * 1.5 * (6/12) / 2.5 = 0.3 * 300 = 90 + // reward = 300 * 1.5 * (6/12) = 225 + runTestCase(complexCase, 300, 90, 225, "complex case") + + // borrow dust case, with high borrowed token value and no rounding + expensiveBorrowDust := baseCase() + expensiveBorrowDust.availableRepay = sdk.NewInt(1) + expensiveBorrowDust.repayTokenPrice = sdk.MustNewDecFromStr("40") + expensiveBorrowDust.rewardTokenPrice = sdk.MustNewDecFromStr("2") + expensiveBorrowDust.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveBorrowDust, 1, 20, 20, "expensive borrow dust") + + // borrow dust case, with high borrowed token value rounds reward down + expensiveBorrowDustDown := baseCase() + expensiveBorrowDustDown.availableRepay = sdk.NewInt(1) + expensiveBorrowDustDown.repayTokenPrice = sdk.MustNewDecFromStr("39.9") + expensiveBorrowDustDown.rewardTokenPrice = sdk.MustNewDecFromStr("2") + expensiveBorrowDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveBorrowDustDown, 1, 19, 19, "expensive borrow dust with price down") + + // borrow dust case, with high borrowed token value rounds collateral burn up + expensiveBorrowDustUp := baseCase() + expensiveBorrowDustUp.availableRepay = sdk.NewInt(1) + expensiveBorrowDustUp.repayTokenPrice = sdk.MustNewDecFromStr("40.1") + expensiveBorrowDustUp.rewardTokenPrice = sdk.MustNewDecFromStr("2") + expensiveBorrowDustUp.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveBorrowDustUp, 1, 20, 20, "expensive borrow dust with price up") + + // borrow dust case, with low borrowed token value rounds collateral burn and reward to zero + cheapBorrowDust := baseCase() + cheapBorrowDust.availableRepay = sdk.NewInt(1) + cheapBorrowDust.repayTokenPrice = sdk.MustNewDecFromStr("2") + cheapBorrowDust.rewardTokenPrice = sdk.MustNewDecFromStr("40") + cheapBorrowDust.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(cheapBorrowDust, 1, 0, 0, "cheap borrow dust") + + // collateral dust case, with high collateral token value and no rounding + expensiveCollateralDust := baseCase() + expensiveCollateralDust.availableCollateral = sdk.NewInt(1) + expensiveCollateralDust.repayTokenPrice = sdk.MustNewDecFromStr("2") + expensiveCollateralDust.rewardTokenPrice = sdk.MustNewDecFromStr("40") + expensiveCollateralDust.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveCollateralDust, 20, 1, 1, "expensive collateral dust") + + // collateral dust case, with high collateral token value rounds required repayment up + expensiveCollateralDustUp := baseCase() + expensiveCollateralDustUp.availableCollateral = sdk.NewInt(1) + expensiveCollateralDustUp.repayTokenPrice = sdk.MustNewDecFromStr("2") + expensiveCollateralDustUp.rewardTokenPrice = sdk.MustNewDecFromStr("40.1") + expensiveCollateralDustUp.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveCollateralDustUp, 21, 1, 1, "expensive collateral dust with price up") + + // collateral dust case, with high collateral token value rounds required repayment up + expensiveCollateralDustDown := baseCase() + expensiveCollateralDustDown.availableCollateral = sdk.NewInt(1) + expensiveCollateralDustDown.repayTokenPrice = sdk.MustNewDecFromStr("2") + expensiveCollateralDustDown.rewardTokenPrice = sdk.MustNewDecFromStr("39.9") + expensiveCollateralDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(expensiveCollateralDustDown, 20, 1, 1, "expensive collateral dust with price down") + + // collateral dust case, with low collateral token value rounds required repayment up + cheapCollateralDust := baseCase() + cheapCollateralDust.availableCollateral = sdk.NewInt(1) + cheapCollateralDust.repayTokenPrice = sdk.MustNewDecFromStr("40") + cheapCollateralDust.rewardTokenPrice = sdk.MustNewDecFromStr("2") + cheapCollateralDust.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(cheapCollateralDust, 1, 1, 1, "cheap collateral dust") + + // exotic case with cheap collateral base tokens but a very high uToken exchange rate + // rounds required repayment up and base reward down + uDust := baseCase() + uDust.availableCollateral = sdk.NewInt(1) + uDust.repayTokenPrice = sdk.MustNewDecFromStr("40") + uDust.rewardTokenPrice = sdk.MustNewDecFromStr("2") + uDust.uTokenExchangeRate = sdk.MustNewDecFromStr("29.5") + uDust.liquidationIncentive = sdk.MustNewDecFromStr("0") + runTestCase(uDust, 2, 1, 29, "high exchange rate collateral dust") +} diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index fbbee45c7d..de788a4728 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -255,51 +255,51 @@ func (s msgServer) Liquidate( ) (*types.MsgLiquidateResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - liquidatorAddr, err := sdk.AccAddressFromBech32(msg.Liquidator) + liquidator, err := sdk.AccAddressFromBech32(msg.Liquidator) if err != nil { return nil, err } - borrowerAddr, err := sdk.AccAddressFromBech32(msg.Borrower) + borrower, err := sdk.AccAddressFromBech32(msg.Borrower) if err != nil { return nil, err } - repaid, reward, err := s.keeper.LiquidateBorrow(ctx, liquidatorAddr, borrowerAddr, msg.Repayment, msg.Reward) + repaid, collateral, reward, err := s.keeper.Liquidate(ctx, liquidator, borrower, msg.Repayment, msg.RewardDenom) if err != nil { return nil, err } - repaidCoin := sdk.NewCoin(msg.Repayment.Denom, repaid) - rewardCoin := sdk.NewCoin(msg.Reward.Denom, reward) - s.keeper.Logger(ctx).Debug( "borrowed assets repaid by liquidator", - "liquidator", liquidatorAddr.String(), - "borrower", borrowerAddr.String(), - "amount", repaidCoin.String(), - "reward", rewardCoin.String(), + "liquidator", liquidator.String(), + "borrower", borrower.String(), "attempted", msg.Repayment.String(), + "repaid", repaid.String(), + "collateral", collateral.String(), + "reward", reward.String(), ) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeLiquidate, - sdk.NewAttribute(types.EventAttrLiquidator, liquidatorAddr.String()), - sdk.NewAttribute(types.EventAttrBorrower, borrowerAddr.String()), - sdk.NewAttribute(sdk.AttributeKeyAmount, repaidCoin.String()), - sdk.NewAttribute(types.EventAttrReward, rewardCoin.String()), + sdk.NewAttribute(types.EventAttrLiquidator, liquidator.String()), + sdk.NewAttribute(types.EventAttrBorrower, borrower.String()), sdk.NewAttribute(types.EventAttrAttempted, msg.Repayment.String()), + sdk.NewAttribute(types.EventAttrRepaid, reward.String()), + sdk.NewAttribute(types.EventAttrCollateral, collateral.String()), + sdk.NewAttribute(types.EventAttrReward, reward.String()), ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.EventAttrModule), - sdk.NewAttribute(sdk.AttributeKeySender, liquidatorAddr.String()), + sdk.NewAttribute(sdk.AttributeKeySender, liquidator.String()), ), }) return &types.MsgLiquidateResponse{ - Repaid: repaidCoin, - Reward: rewardCoin, + Repaid: repaid, + Collateral: collateral, + Reward: reward, }, nil } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 28598f652a..6d4df27e31 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -13,9 +13,7 @@ import ( // must be the base denomination, e.g. uumee. The x/oracle module must know of // the base and display/symbol denominations for each exchange pair. E.g. it must // know about the UMEE/USD exchange rate along with the uumee base denomination -// and the exponent. -// This function will only return positive exchange rates or errors, unless a -// token is blacklisted, in which case it will return zero. +// and the exponent. When error is nil, price is guaranteed to be positive. func (k Keeper) TokenPrice(ctx sdk.Context, denom string) (sdk.Dec, error) { t, err := k.GetTokenSettings(ctx, denom) if err != nil { @@ -23,7 +21,7 @@ func (k Keeper) TokenPrice(ctx sdk.Context, denom string) (sdk.Dec, error) { } if t.Blacklist { - return sdk.ZeroDec(), nil + return sdk.ZeroDec(), types.ErrBlacklisted } price, err := k.oracleKeeper.GetExchangeRateBase(ctx, denom) @@ -45,16 +43,18 @@ func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { if err != nil { return sdk.ZeroDec(), err } - return p.Mul(coin.Amount.ToDec()), nil } // TotalTokenValue returns the total value of all supplied tokens. It is -// equivalent to calling GetTokenValue on each coin individually. +// equivalent to the sum of TokenValue on each coin individually, except it +// ignores unregistered and blacklisted tokens instead of returning an error. func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, error) { total := sdk.ZeroDec() - for _, c := range coins { + accepted := k.filterAcceptedCoins(ctx, coins) + + for _, c := range accepted { v, err := k.TokenValue(ctx, c) if err != nil { return sdk.ZeroDec(), err @@ -66,34 +66,19 @@ func (k Keeper) TotalTokenValue(ctx sdk.Context, coins sdk.Coins) (sdk.Dec, erro return total, nil } -// EquivalentValue returns the amount of a selected denom which would have equal -// USD value to a provided sdk.Coin -func (k Keeper) EquivalentTokenValue(ctx sdk.Context, fromCoin sdk.Coin, toDenom string) (sdk.Coin, error) { - // get USD price of input (fromCoin) denomination - p1, err := k.TokenPrice(ctx, fromCoin.Denom) +// PriceRatio computed the ratio of the USD prices of two tokens, as sdk.Dec(fromPrice/toPrice). +// Will return an error if either token price is not positive, and guarantees a positive output. +func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, error) { + p1, err := k.TokenPrice(ctx, fromDenom) if err != nil { - return sdk.Coin{}, err - } - - // return immediately on zero input value - if p1.IsZero() { - return sdk.NewCoin(toDenom, sdk.ZeroInt()), nil + return sdk.ZeroDec(), err } - - // get USD price of output denomination p2, err := k.TokenPrice(ctx, toDenom) if err != nil { - return sdk.Coin{}, err - } - if !p2.IsPositive() { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrBadValue, p2.String()) + return sdk.ZeroDec(), err } - - // then return the amount corrected by the price ratio - return sdk.NewCoin( - toDenom, - fromCoin.Amount.ToDec().Mul(p1).Quo(p2).TruncateInt(), - ), nil + // Price ratio > 1 if fromDenom is worth more than toDenom. + return p1.Quo(p2), nil } // FundOracle transfers requested coins to the oracle module account, as diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index c8dc3effb3..e23ba1854e 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -84,6 +84,7 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { s.Require().NoError(err) s.Require().Equal(sdk.MustNewDecFromStr("195.19"), v) + // same result, as unregistered token is ignored v, err = s.app.LeverageKeeper.TotalTokenValue( s.ctx, sdk.NewCoins( @@ -92,20 +93,19 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { sdk.NewInt64Coin("foo", 4700000), ), ) - s.Require().Error(err) - s.Require().Equal(sdk.ZeroDec(), v) + s.Require().NoError(err) + s.Require().Equal(sdk.MustNewDecFromStr("195.19"), v) } -func (s *IntegrationTestSuite) TestOracle_EquivalentTokenValue() { - c, err := s.app.LeverageKeeper.EquivalentTokenValue(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 2400000), atomIBCDenom) +func (s *IntegrationTestSuite) TestOracle_PriceRatio() { + r, err := s.app.LeverageKeeper.PriceRatio(s.ctx, umeeapp.BondDenom, atomIBCDenom) s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(atomIBCDenom, 256576), c) + // $4.21 / $39.38 + s.Require().Equal(sdk.MustNewDecFromStr("0.106907059421025901"), r) - c, err = s.app.LeverageKeeper.EquivalentTokenValue(s.ctx, sdk.NewInt64Coin("foo", 2400000), atomIBCDenom) + _, err = s.app.LeverageKeeper.PriceRatio(s.ctx, "foo", atomIBCDenom) s.Require().Error(err) - s.Require().Equal(sdk.Coin{}, c) - c, err = s.app.LeverageKeeper.EquivalentTokenValue(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 2400000), "foo") + _, err = s.app.LeverageKeeper.PriceRatio(s.ctx, umeeapp.BondDenom, "foo") s.Require().Error(err) - s.Require().Equal(sdk.Coin{}, c) } diff --git a/x/leverage/keeper/token.go b/x/leverage/keeper/token.go index 33185ad13b..688516fd73 100644 --- a/x/leverage/keeper/token.go +++ b/x/leverage/keeper/token.go @@ -81,16 +81,3 @@ func (k Keeper) GetTokenSettings(ctx sdk.Context, denom string) (types.Token, er err := k.cdc.Unmarshal(bz, &token) return token, err } - -// AssertNotBlacklisted returns an error if a token does not exist or is blacklisted. -func (k Keeper) AssertNotBlacklisted(ctx sdk.Context, denom string) error { - token, err := k.GetTokenSettings(ctx, denom) - if err != nil { - return err - } - if token.Blacklist { - return sdkerrors.Wrap(types.ErrBlacklisted, denom) - } - - return nil -} diff --git a/x/leverage/keeper/validate.go b/x/leverage/keeper/validate.go index 32ecea8fc0..ff5c4b3d13 100644 --- a/x/leverage/keeper/validate.go +++ b/x/leverage/keeper/validate.go @@ -6,10 +6,29 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) +// validateAcceptedDenom validates an sdk.Coin and ensures it is a registered Token +// with Blacklisted == false +func (k Keeper) validateAcceptedDenom(ctx sdk.Context, denom string) error { + token, err := k.GetTokenSettings(ctx, denom) + if err != nil { + return err + } + return token.AssertNotBlacklisted() +} + +// validateAcceptedAsset validates an sdk.Coin and ensures it is a registered Token +// with Blacklisted == false +func (k Keeper) validateAcceptedAsset(ctx sdk.Context, coin sdk.Coin) error { + if err := coin.Validate(); err != nil { + return err + } + return k.validateAcceptedDenom(ctx, coin.Denom) +} + // validateSupply validates an sdk.Coin and ensures its Denom is a Token with EnableMsgSupply func (k Keeper) validateSupply(ctx sdk.Context, loan sdk.Coin) error { - if !loan.IsValid() { - return types.ErrInvalidAsset.Wrap(loan.String()) + if err := loan.Validate(); err != nil { + return err } token, err := k.GetTokenSettings(ctx, loan.Denom) if err != nil { @@ -20,10 +39,9 @@ func (k Keeper) validateSupply(ctx sdk.Context, loan sdk.Coin) error { // validateBorrow validates an sdk.Coin and ensures its Denom is a Token with EnableMsgBorrow func (k Keeper) validateBorrow(ctx sdk.Context, borrow sdk.Coin) error { - if !borrow.IsValid() { - return types.ErrInvalidAsset.Wrap(borrow.String()) + if err := borrow.Validate(); err != nil { + return err } - token, err := k.GetTokenSettings(ctx, borrow.Denom) if err != nil { return err @@ -34,10 +52,9 @@ func (k Keeper) validateBorrow(ctx sdk.Context, borrow sdk.Coin) error { // validateCollateralAsset validates an sdk.Coin and ensures its Denom is a Token with EnableMsgSupply // and CollateralWeight > 0 func (k Keeper) validateCollateralAsset(ctx sdk.Context, collateral sdk.Coin) error { - if !collateral.IsValid() { - return types.ErrInvalidAsset.Wrap(collateral.String()) + if err := collateral.Validate(); err != nil { + return err } - tokenDenom := k.FromUTokenToTokenDenom(ctx, collateral.Denom) token, err := k.GetTokenSettings(ctx, tokenDenom) if err != nil { diff --git a/x/leverage/simulation/operations.go b/x/leverage/simulation/operations.go index e6b09bc5cf..e71bc180bc 100644 --- a/x/leverage/simulation/operations.go +++ b/x/leverage/simulation/operations.go @@ -331,7 +331,7 @@ func SimulateMsgLiquidate(ak simulation.AccountKeeper, bk types.BankKeeper, lk k return simtypes.NoOpMsg(types.ModuleName, types.EventTypeLiquidate, "skip all transfers"), nil, nil } - msg := types.NewMsgLiquidate(liquidator.Address, borrower.Address, repaymentToken, sdk.NewInt64Coin(rewardDenom, 0)) + msg := types.NewMsgLiquidate(liquidator.Address, borrower.Address, repaymentToken, rewardDenom) txCtx := simulation.OperationInput{ R: r, diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index f97c8bb445..092155f2c4 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -330,7 +330,7 @@ func (s *SimTestSuite) TestSimulateMsgLiquidate() { op := simulation.SimulateMsgLiquidate(s.app.AccountKeeper, s.app.BankKeeper, s.app.LeverageKeeper) operationMsg, futureOperations, err := op(r, s.app.BaseApp, s.ctx, accs, "") s.Require().EqualError(err, - "failed to execute message; message index: 0: umee1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7wrm6ea borrowed value is below the liquidation threshold 0.005000000000000000: borrower not eligible for liquidation", + "failed to execute message; message index: 0: borrower not eligible for liquidation", ) var msg types.MsgLiquidate diff --git a/x/leverage/spec/04_messages.md b/x/leverage/spec/04_messages.md index 3a3b5251e0..80fe6cd6b0 100644 --- a/x/leverage/spec/04_messages.md +++ b/x/leverage/spec/04_messages.md @@ -98,27 +98,23 @@ The message will fail under the following conditions: ## MsgLiquidate -A user liquidates all or part of an undercollateralized borrower's borrow positions in exchange for an equivalent value of the borrower's collateral, plus liquidation incentive. If the requested repayment amount would overpay or is limited by available collateral rewards or the dynamic `CloseFactor`, the repayment amount will be reduced to the maximum acceptable value before liquidation is attempted. - -The user specifies a minimum reward amount (in a base token denom) that they would accept for the full repayment amount. This is used to compute a ratio of actual repayment (which could be lower than intended) to token equivalent of actual uToken reward. Transactions that would result in a reward:repayment amount lower than the minimum will fail instead. - -A minimum reward amount of zero ignores this check and trusts oracle prices. +A user liquidates all or part of an undercollateralized borrower's borrow positions in exchange for an equivalent value of the borrower's collateral, plus liquidation incentive. If the requested repayment amount would overpay or is limited by available balances or the dynamic `CloseFactor`, the repayment amount will be reduced to the maximum acceptable value before liquidation is attempted. ```protobuf message MsgLiquidate { string liquidator = 1; string borrower = 2; cosmos.base.v1beta1.Coin repayment = 3; - cosmos.base.v1beta1.Coin reward = 4; + string reward_denom = 4; } ``` The message will fail under the following conditions: - `repayment` is not a valid amount of an accepted base asset -- `reward` is not a valid amount of an accepted base asset +- `reward_denom` is not an accepted base asset - `borrower` has not borrowed any of the specified asset to repay -- `borrower` has no collateral of the requested reward denom -- `borrower`'s total borrowed value does not exceed their `LiquidationThreshold` -- `liquidator` balance is insufficient -- the message's ratio of `reward` to `repayment` is higher than the ratio that would result from liquidation at the current oracle prices and liquidation incentives +- `borrower` has no collateral of the requested reward's uToken denom +- `borrower`'s `BorrowedValue` does not exceed their `LiquidationThreshold` +- `liquidator` balance of the repayment denom is zero +- `x/leverage` unreserved module balance or the reward denom is zero - Borrowed value or `LiquidationThreshold` cannot be computed due to a missing `x/oracle` price \ No newline at end of file diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 5641e2b03f..8dbed3cac6 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -9,7 +9,7 @@ import ( var ( ErrInvalidAsset = sdkerrors.Register(ModuleName, 1100, "invalid asset") ErrInsufficientBalance = sdkerrors.Register(ModuleName, 1101, "insufficient balance") - ErrUndercollaterized = sdkerrors.Register(ModuleName, 1102, "Borrow positions are undercollaterized") + ErrUndercollaterized = sdkerrors.Register(ModuleName, 1102, "borrow positions are undercollaterized") ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 1103, "lending pool insufficient") ErrInvalidRepayment = sdkerrors.Register(ModuleName, 1104, "invalid repayment") ErrInvalidAddress = sdkerrors.Register(ModuleName, 1105, "invalid address") @@ -30,11 +30,12 @@ var ( ErrBorrowNotAllowed = sdkerrors.Register(ModuleName, 1120, "borrowing of asset disabled") ErrBlacklisted = sdkerrors.Register(ModuleName, 1121, "base denom blacklisted") ErrCollateralWeightZero = sdkerrors.Register(ModuleName, 1122, "token collateral weight is zero") - ErrMaxSupplyUtilization = sdkerrors.Register(ModuleName, 1123, "market would exceed MaxSupplyUtilization") - ErrMinCollateralLiquidity = sdkerrors.Register(ModuleName, 1124, "market would fall below MinCollateralLiquidity") + ErrLiquidationInvalid = sdkerrors.Register(ModuleName, 1123, "liquidation invalid") + ErrMaxSupplyUtilization = sdkerrors.Register(ModuleName, 1124, "market would exceed MaxSupplyUtilization") + ErrMinCollateralLiquidity = sdkerrors.Register(ModuleName, 1125, "market would fall below MinCollateralLiquidity") ErrMaxCollateralShare = sdkerrors.Register( ModuleName, - 1125, + 1126, "market total collateral would exceed MaxCollateralShare", ) ) diff --git a/x/leverage/types/events.go b/x/leverage/types/events.go index 85da16f74e..d065526404 100644 --- a/x/leverage/types/events.go +++ b/x/leverage/types/events.go @@ -21,6 +21,8 @@ const ( EventAttrDenom = "denom" EventAttrEnable = "enabled" EventAttrAttempted = "attempted" + EventAttrRepaid = "repaid" + EventAttrCollateral = "collateral" EventAttrReward = "reward" EventAttrInterest = "total_interest" EventAttrBlockHeight = "block_height" diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index ea945d3713..7bee142c1f 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -99,7 +99,7 @@ func (t Token) Validate() error { return nil } -// AssertSupplyEnabled returns an error if a token does not exist or cannot be supplied. +// AssertSupplyEnabled returns an error if a Token cannot be supplied. func (t Token) AssertSupplyEnabled() error { if !t.EnableMsgSupply { return sdkerrors.Wrap(ErrSupplyNotAllowed, t.BaseDenom) @@ -107,10 +107,18 @@ func (t Token) AssertSupplyEnabled() error { return nil } -// AssertBorrowEnabled returns an error if a token does not exist or cannot be borrowed. +// AssertBorrowEnabled returns an error if a Token cannot be borrowed. func (t Token) AssertBorrowEnabled() error { if !t.EnableMsgBorrow { return sdkerrors.Wrap(ErrBorrowNotAllowed, t.BaseDenom) } return nil } + +// AssertNotBlacklisted returns an error if a Token is blacklisted. +func (t Token) AssertNotBlacklisted() error { + if t.Blacklist { + return sdkerrors.Wrap(ErrBlacklisted, t.BaseDenom) + } + return nil +} diff --git a/x/leverage/types/tx.go b/x/leverage/types/tx.go index f87b82d71f..a0ea980782 100644 --- a/x/leverage/types/tx.go +++ b/x/leverage/types/tx.go @@ -151,12 +151,12 @@ func (msg *MsgRepay) GetSignBytes() []byte { return sdk.MustSortJSON(bz) } -func NewMsgLiquidate(liquidator, borrower sdk.AccAddress, repayment, reward sdk.Coin) *MsgLiquidate { +func NewMsgLiquidate(liquidator, borrower sdk.AccAddress, repayment sdk.Coin, rewardDenom string) *MsgLiquidate { return &MsgLiquidate{ - Liquidator: liquidator.String(), - Borrower: borrower.String(), - Repayment: repayment, - Reward: reward, + Liquidator: liquidator.String(), + Borrower: borrower.String(), + Repayment: repayment, + RewardDenom: rewardDenom, } } @@ -167,7 +167,11 @@ func (msg *MsgLiquidate) ValidateBasic() error { if err := validateSenderAndAsset(msg.Borrower, &msg.Repayment); err != nil { return err } - return validateSenderAndAsset(msg.Liquidator, &msg.Reward) + if err := sdk.ValidateDenom(msg.RewardDenom); err != nil { + return err + } + _, err := sdk.AccAddressFromBech32(msg.Liquidator) + return err } func (msg *MsgLiquidate) GetSigners() []sdk.AccAddress { diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index d42fe04cc7..9a3c7fda1f 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -276,15 +276,21 @@ func (m *MsgRepay) XXX_DiscardUnknown() { var xxx_messageInfo_MsgRepay proto.InternalMessageInfo -// 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. type MsgLiquidate struct { // Liquidator is the account address performing a liquidation and the signer // of the message. - Liquidator string `protobuf:"bytes,1,opt,name=liquidator,proto3" json:"liquidator,omitempty"` - Borrower string `protobuf:"bytes,2,opt,name=borrower,proto3" json:"borrower,omitempty"` - Repayment types.Coin `protobuf:"bytes,3,opt,name=repayment,proto3" json:"repayment"` - Reward types.Coin `protobuf:"bytes,4,opt,name=reward,proto3" json:"reward"` + Liquidator string `protobuf:"bytes,1,opt,name=liquidator,proto3" json:"liquidator,omitempty"` + // Borrower is the account whose borrow is being repaid, and collateral consumed, + // by the liquidation. It does not sign the message. + Borrower string `protobuf:"bytes,2,opt,name=borrower,proto3" json:"borrower,omitempty"` + // Repayment is the maximum amount of base tokens that the liquidator is willing + // to repay. + Repayment types.Coin `protobuf:"bytes,3,opt,name=repayment,proto3" json:"repayment"` + // 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. + RewardDenom string `protobuf:"bytes,4,opt,name=reward_denom,json=rewardDenom,proto3" json:"reward_denom,omitempty"` } func (m *MsgLiquidate) Reset() { *m = MsgLiquidate{} } @@ -507,6 +513,8 @@ var xxx_messageInfo_MsgBorrowResponse proto.InternalMessageInfo // MsgRepayResponse defines the Msg/Repay response type. type MsgRepayResponse struct { + // Repaid is the amount of debt, in base tokens, that was repaid to the + // module by the borrower. Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` } @@ -545,8 +553,15 @@ var xxx_messageInfo_MsgRepayResponse proto.InternalMessageInfo // MsgLiquidateResponse defines the Msg/Liquidate response type. type MsgLiquidateResponse struct { + // Repaid is the amount of debt, in base tokens, that liquidator repaid + // to the module on behalf of the borrower. Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` - Reward types.Coin `protobuf:"bytes,2,opt,name=reward,proto3" json:"reward"` + // Collateral is the amount of the borrower's uToken collateral that + // was converted to the reward tokens as a result of liquidation. + Collateral types.Coin `protobuf:"bytes,2,opt,name=collateral,proto3" json:"collateral"` + // Reward is the amount of base tokens that the liquidator received from + // the module as reward for the liquidation. + Reward types.Coin `protobuf:"bytes,3,opt,name=reward,proto3" json:"reward"` } func (m *MsgLiquidateResponse) Reset() { *m = MsgLiquidateResponse{} } @@ -602,45 +617,46 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 595 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcb, 0x6e, 0xd3, 0x40, - 0x14, 0x8d, 0xd3, 0x34, 0x4a, 0x6e, 0x41, 0x14, 0xb7, 0x48, 0xe9, 0x00, 0xa6, 0x32, 0x0f, 0x55, - 0x08, 0x6c, 0x92, 0x0a, 0xb1, 0x62, 0x93, 0x22, 0x21, 0x15, 0x8c, 0x50, 0xba, 0x40, 0x42, 0xe2, - 0x31, 0x49, 0x46, 0x53, 0x0b, 0x27, 0x63, 0x66, 0x26, 0x49, 0xc3, 0x17, 0xb0, 0xe4, 0x9b, 0x58, - 0x65, 0xd9, 0x0d, 0x12, 0x2b, 0x04, 0xc9, 0x8f, 0x20, 0xbf, 0x26, 0x0f, 0xdc, 0x60, 0xa8, 0xb2, - 0x9b, 0xb9, 0xe7, 0xdc, 0x7b, 0x66, 0xee, 0x5c, 0x1f, 0xc3, 0x4e, 0xaf, 0x43, 0x88, 0xed, 0x91, - 0x3e, 0xe1, 0x98, 0x12, 0xbb, 0x5f, 0xb5, 0xe5, 0x89, 0xe5, 0x73, 0x26, 0x99, 0xbe, 0x19, 0x40, - 0x56, 0x02, 0x59, 0xfd, 0x2a, 0x32, 0x5a, 0x4c, 0x74, 0x98, 0xb0, 0x9b, 0x58, 0x04, 0xd4, 0x26, - 0x91, 0xb8, 0x6a, 0xb7, 0x98, 0xdb, 0x8d, 0x32, 0xd0, 0x36, 0x65, 0x94, 0x85, 0x4b, 0x3b, 0x58, - 0x45, 0x51, 0xf3, 0x2d, 0x94, 0x1d, 0x41, 0x8f, 0x7a, 0xbe, 0xef, 0x0d, 0x75, 0x04, 0x25, 0x11, - 0xac, 0x5c, 0xc2, 0x2b, 0xda, 0xae, 0xb6, 0x57, 0x6e, 0xa8, 0xbd, 0xfe, 0x10, 0xd6, 0xb1, 0x10, - 0x44, 0x56, 0xf2, 0xbb, 0xda, 0xde, 0x46, 0x6d, 0xc7, 0x8a, 0xe4, 0xac, 0x40, 0xce, 0x8a, 0xe5, - 0xac, 0x03, 0xe6, 0x76, 0xeb, 0x85, 0xd1, 0x8f, 0x1b, 0xb9, 0x46, 0xc4, 0x36, 0xdf, 0xc3, 0x86, - 0x23, 0xe8, 0x2b, 0x57, 0x1e, 0xb7, 0x39, 0x1e, 0xac, 0x42, 0xa1, 0x05, 0x9b, 0x8e, 0xa0, 0x07, - 0xcc, 0xf3, 0xb0, 0x24, 0x1c, 0x7b, 0xee, 0x27, 0x12, 0xc8, 0x34, 0x19, 0xe7, 0x6c, 0x30, 0x95, - 0x49, 0xf6, 0xfa, 0x3e, 0x14, 0x82, 0xae, 0x64, 0x55, 0x09, 0xc9, 0x26, 0x01, 0xdd, 0x11, 0xf4, - 0x09, 0x69, 0xad, 0x56, 0x26, 0x7a, 0x8d, 0x7a, 0x58, 0x63, 0x69, 0xf5, 0xff, 0xec, 0xd5, 0x1b, - 0x28, 0x39, 0x82, 0x36, 0x88, 0x8f, 0x87, 0xab, 0x28, 0xff, 0x55, 0x83, 0x0b, 0x8e, 0xa0, 0xcf, - 0xdd, 0x8f, 0x3d, 0xb7, 0x8d, 0x25, 0xd1, 0x0d, 0x00, 0x2f, 0xde, 0xb0, 0x44, 0x65, 0x26, 0x32, - 0x77, 0x86, 0xfc, 0xc2, 0x19, 0x1e, 0x43, 0x99, 0x07, 0x07, 0xed, 0x90, 0xae, 0xac, 0xac, 0x65, - 0x3b, 0xc7, 0x34, 0x43, 0x7f, 0x04, 0x45, 0x4e, 0x06, 0x98, 0xb7, 0x2b, 0x85, 0x6c, 0xb9, 0x31, - 0xdd, 0xdc, 0x82, 0xcb, 0xea, 0x8b, 0x68, 0x10, 0xe1, 0xb3, 0xae, 0x20, 0xe6, 0x15, 0xd8, 0x9a, - 0x19, 0x63, 0x15, 0x46, 0x50, 0x59, 0x9c, 0x3d, 0x85, 0x5d, 0x03, 0xf4, 0xe7, 0xc8, 0x28, 0x34, - 0x52, 0x89, 0x5e, 0x5a, 0x05, 0x9f, 0x85, 0xa3, 0x1c, 0x3e, 0x4f, 0x12, 0x8b, 0xee, 0xe1, 0x63, - 0xb7, 0x1d, 0xb6, 0x2f, 0xdb, 0x3d, 0x02, 0xba, 0xf9, 0x59, 0x83, 0xed, 0xd9, 0xc7, 0x38, 0x77, - 0xc5, 0x99, 0x96, 0xe6, 0xff, 0xa9, 0xa5, 0xb5, 0x6f, 0x05, 0x58, 0x73, 0x04, 0xd5, 0x0f, 0xa1, - 0x18, 0x3b, 0xcd, 0x55, 0x6b, 0xd1, 0xbf, 0x2c, 0xd5, 0x74, 0x74, 0x73, 0x09, 0xa8, 0x6e, 0xf1, - 0x12, 0x4a, 0xca, 0x55, 0xae, 0xa7, 0x26, 0x24, 0x30, 0xba, 0xbd, 0x14, 0x56, 0x15, 0xdf, 0xc1, - 0xc5, 0x79, 0x17, 0x31, 0x53, 0xf3, 0xe6, 0x38, 0xe8, 0xee, 0xdf, 0x39, 0x4a, 0x80, 0xc0, 0xa5, - 0x45, 0x07, 0xb9, 0x95, 0x9a, 0xbe, 0xc0, 0x42, 0xf7, 0xb2, 0xb0, 0x94, 0xcc, 0x21, 0x14, 0x63, - 0x07, 0x49, 0xef, 0x72, 0x04, 0x9e, 0xd1, 0xe5, 0xf9, 0x89, 0xd4, 0x9f, 0xc2, 0x7a, 0xec, 0x16, - 0xa9, 0xec, 0x10, 0x43, 0xe6, 0xd9, 0x98, 0x2a, 0x74, 0x04, 0xe5, 0x19, 0x5b, 0x48, 0x4d, 0x50, - 0x38, 0xba, 0xb3, 0x1c, 0x4f, 0x8a, 0xd6, 0x5f, 0x8c, 0x7e, 0x19, 0xb9, 0xd1, 0xd8, 0xd0, 0x4e, - 0xc7, 0x86, 0xf6, 0x73, 0x6c, 0x68, 0x5f, 0x26, 0x46, 0xee, 0x74, 0x62, 0xe4, 0xbe, 0x4f, 0x8c, - 0xdc, 0xeb, 0x07, 0xd4, 0x95, 0xc7, 0xbd, 0xa6, 0xd5, 0x62, 0x1d, 0x3b, 0xa8, 0x77, 0xbf, 0x4b, - 0xe4, 0x80, 0xf1, 0x0f, 0xe1, 0xc6, 0xee, 0xd7, 0xec, 0x93, 0xe9, 0xaf, 0x55, 0x0e, 0x7d, 0x22, - 0x9a, 0xc5, 0xf0, 0x9f, 0xb8, 0xff, 0x3b, 0x00, 0x00, 0xff, 0xff, 0x9c, 0x7e, 0x5e, 0x14, 0x78, - 0x07, 0x00, 0x00, + // 617 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdb, 0x6e, 0xd3, 0x40, + 0x10, 0x8d, 0xdb, 0x34, 0x4a, 0x26, 0x45, 0x14, 0xb7, 0x48, 0xa9, 0x01, 0x53, 0xcc, 0x45, 0x15, + 0x02, 0x9b, 0xa4, 0x42, 0x3c, 0x21, 0xa4, 0xb4, 0x12, 0x52, 0xc1, 0x08, 0xa5, 0x0f, 0x48, 0x48, + 0x50, 0x9c, 0x64, 0xe4, 0x5a, 0x38, 0x5e, 0xe3, 0xdd, 0x24, 0x0d, 0x5f, 0xc1, 0x87, 0xf0, 0x21, + 0xe1, 0xad, 0x2f, 0x48, 0x3c, 0x21, 0x48, 0x7e, 0x04, 0xad, 0x2f, 0x9b, 0x0b, 0x6e, 0xb0, 0xa8, + 0xf2, 0xb6, 0x3b, 0xe7, 0xcc, 0x39, 0xeb, 0x99, 0xd1, 0x18, 0xb6, 0xbb, 0x1d, 0x44, 0xc3, 0xc5, + 0x1e, 0x06, 0x96, 0x8d, 0x46, 0xaf, 0x6a, 0xb0, 0x53, 0xdd, 0x0f, 0x08, 0x23, 0xf2, 0x06, 0x87, + 0xf4, 0x04, 0xd2, 0x7b, 0x55, 0x45, 0x6d, 0x11, 0xda, 0x21, 0xd4, 0x68, 0x5a, 0x94, 0x53, 0x9b, + 0xc8, 0xac, 0xaa, 0xd1, 0x22, 0x8e, 0x17, 0x65, 0x28, 0x5b, 0x36, 0xb1, 0x49, 0x78, 0x34, 0xf8, + 0x29, 0x8a, 0x6a, 0xef, 0xa1, 0x64, 0x52, 0xfb, 0xa8, 0xeb, 0xfb, 0xee, 0x40, 0x56, 0xa0, 0x48, + 0xf9, 0xc9, 0xc1, 0xa0, 0x22, 0xed, 0x48, 0xbb, 0xa5, 0x86, 0xb8, 0xcb, 0x8f, 0x61, 0xcd, 0xa2, + 0x14, 0x59, 0x65, 0x65, 0x47, 0xda, 0x2d, 0xd7, 0xb6, 0xf5, 0xc8, 0x4e, 0xe7, 0x76, 0x7a, 0x6c, + 0xa7, 0xef, 0x13, 0xc7, 0xab, 0xe7, 0x87, 0x3f, 0x6f, 0xe6, 0x1a, 0x11, 0x5b, 0xfb, 0x00, 0x65, + 0x93, 0xda, 0x6f, 0x1c, 0x76, 0xd2, 0x0e, 0xac, 0xfe, 0x32, 0x1c, 0x5a, 0xb0, 0x61, 0x52, 0x7b, + 0x9f, 0xb8, 0xae, 0xc5, 0x30, 0xb0, 0x5c, 0xe7, 0x33, 0x72, 0x9b, 0x26, 0x09, 0x02, 0xd2, 0x9f, + 0xd8, 0x24, 0x77, 0x79, 0x0f, 0xf2, 0xbc, 0x2a, 0x59, 0x5d, 0x42, 0xb2, 0x86, 0x20, 0x9b, 0xd4, + 0x3e, 0xc0, 0xd6, 0x72, 0x6d, 0xa2, 0x6e, 0xd4, 0x43, 0x8d, 0x85, 0xea, 0xff, 0x59, 0xab, 0x77, + 0x50, 0x34, 0xa9, 0xdd, 0x40, 0xdf, 0x1a, 0x2c, 0x43, 0xfe, 0xab, 0x04, 0xeb, 0x26, 0xb5, 0x5f, + 0x3a, 0x9f, 0xba, 0x4e, 0xdb, 0x62, 0x28, 0xab, 0x00, 0x6e, 0x7c, 0x21, 0x89, 0xcb, 0x54, 0x64, + 0xe6, 0x0d, 0x2b, 0x73, 0x6f, 0x78, 0x0a, 0xa5, 0x80, 0x3f, 0xb4, 0x83, 0x1e, 0xab, 0xac, 0x66, + 0x7b, 0xc7, 0x24, 0x43, 0xbe, 0x05, 0xeb, 0x01, 0xf6, 0xad, 0xa0, 0x7d, 0xdc, 0x46, 0x8f, 0x74, + 0x2a, 0xf9, 0x50, 0xbe, 0x1c, 0xc5, 0x0e, 0x78, 0x48, 0xdb, 0x84, 0x2b, 0x62, 0xf6, 0x1b, 0x48, + 0x7d, 0xe2, 0x51, 0xd4, 0xae, 0xc2, 0xe6, 0xd4, 0xc0, 0x8a, 0xb0, 0x02, 0x95, 0xf9, 0x29, 0x13, + 0xd8, 0x75, 0x50, 0xfe, 0x1e, 0x0e, 0x81, 0x46, 0x2e, 0x51, 0x4f, 0x45, 0xf0, 0x45, 0x38, 0xb4, + 0x61, 0x23, 0x92, 0x98, 0xfc, 0x04, 0x0a, 0xfc, 0xf9, 0x4e, 0x3b, 0x2c, 0x54, 0x86, 0xaf, 0x8d, + 0xe9, 0xda, 0x37, 0x09, 0xb6, 0xa6, 0xcb, 0x7e, 0x61, 0x45, 0xf9, 0x19, 0xc0, 0xe4, 0x63, 0xb2, + 0x0e, 0xc1, 0x54, 0x4a, 0xe4, 0xcc, 0x2b, 0x9d, 0xb5, 0x73, 0x31, 0xbd, 0xf6, 0x3d, 0x0f, 0xab, + 0x26, 0xb5, 0xe5, 0x43, 0x28, 0xc4, 0x4b, 0xe9, 0x9a, 0x3e, 0xbf, 0xea, 0x74, 0xd1, 0x35, 0xe5, + 0xf6, 0x02, 0x50, 0x94, 0xe1, 0x35, 0x14, 0xc5, 0x02, 0xba, 0x91, 0x9a, 0x90, 0xc0, 0xca, 0xdd, + 0x85, 0xb0, 0x50, 0x3c, 0x86, 0x4b, 0xb3, 0x0b, 0x47, 0x4b, 0xcd, 0x9b, 0xe1, 0x28, 0xf7, 0xff, + 0xcd, 0x11, 0x06, 0x08, 0x97, 0xe7, 0x97, 0xcd, 0x9d, 0xd4, 0xf4, 0x39, 0x96, 0xf2, 0x20, 0x0b, + 0x4b, 0xd8, 0x1c, 0x42, 0x21, 0x5e, 0x36, 0xe9, 0x55, 0x8e, 0xc0, 0x73, 0xaa, 0x3c, 0x3b, 0xd2, + 0xf2, 0x73, 0x58, 0x8b, 0x17, 0x4b, 0x2a, 0x3b, 0xc4, 0x14, 0xed, 0x7c, 0x4c, 0x08, 0x1d, 0x41, + 0x69, 0x6a, 0x83, 0xa4, 0x26, 0x08, 0x5c, 0xb9, 0xb7, 0x18, 0x4f, 0x44, 0xeb, 0xaf, 0x86, 0xbf, + 0xd5, 0xdc, 0x70, 0xa4, 0x4a, 0x67, 0x23, 0x55, 0xfa, 0x35, 0x52, 0xa5, 0x2f, 0x63, 0x35, 0x77, + 0x36, 0x56, 0x73, 0x3f, 0xc6, 0x6a, 0xee, 0xed, 0x23, 0xdb, 0x61, 0x27, 0xdd, 0xa6, 0xde, 0x22, + 0x1d, 0x83, 0xeb, 0x3d, 0xf4, 0x90, 0xf5, 0x49, 0xf0, 0x31, 0xbc, 0x18, 0xbd, 0x9a, 0x71, 0x3a, + 0xf9, 0x0b, 0xb3, 0x81, 0x8f, 0xb4, 0x59, 0x08, 0x7f, 0x9f, 0x7b, 0x7f, 0x02, 0x00, 0x00, 0xff, + 0xff, 0xf4, 0x30, 0x1e, 0x2f, 0xa3, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -671,7 +687,7 @@ type MsgClient interface { // Repay allows a user to repay previously borrowed tokens and interest. Repay(ctx context.Context, in *MsgRepay, opts ...grpc.CallOption) (*MsgRepayResponse, error) // Liquidate allows a user to repay a different user's borrowed coins in exchange for some - // of their collateral. + // of the target's collateral. Liquidate(ctx context.Context, in *MsgLiquidate, opts ...grpc.CallOption) (*MsgLiquidateResponse, error) } @@ -764,7 +780,7 @@ type MsgServer interface { // Repay allows a user to repay previously borrowed tokens and interest. Repay(context.Context, *MsgRepay) (*MsgRepayResponse, error) // Liquidate allows a user to repay a different user's borrowed coins in exchange for some - // of their collateral. + // of the target's collateral. Liquidate(context.Context, *MsgLiquidate) (*MsgLiquidateResponse, error) } @@ -1221,16 +1237,13 @@ func (m *MsgLiquidate) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - { - size, err := m.Reward.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintTx(dAtA, i, uint64(size)) + if len(m.RewardDenom) > 0 { + i -= len(m.RewardDenom) + copy(dAtA[i:], m.RewardDenom) + i = encodeVarintTx(dAtA, i, uint64(len(m.RewardDenom))) + i-- + dAtA[i] = 0x22 } - i-- - dAtA[i] = 0x22 { size, err := m.Repayment.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -1435,6 +1448,16 @@ func (m *MsgLiquidateResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i = encodeVarintTx(dAtA, i, uint64(size)) } i-- + dAtA[i] = 0x1a + { + size, err := m.Collateral.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- dAtA[i] = 0x12 { size, err := m.Repaid.MarshalToSizedBuffer(dAtA[:i]) @@ -1566,8 +1589,10 @@ func (m *MsgLiquidate) Size() (n int) { } l = m.Repayment.Size() n += 1 + l + sovTx(uint64(l)) - l = m.Reward.Size() - n += 1 + l + sovTx(uint64(l)) + l = len(m.RewardDenom) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } return n } @@ -1635,6 +1660,8 @@ func (m *MsgLiquidateResponse) Size() (n int) { _ = l l = m.Repaid.Size() n += 1 + l + sovTx(uint64(l)) + l = m.Collateral.Size() + n += 1 + l + sovTx(uint64(l)) l = m.Reward.Size() n += 1 + l + sovTx(uint64(l)) return n @@ -2464,9 +2491,9 @@ func (m *MsgLiquidate) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 4: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Reward", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field RewardDenom", wireType) } - var msglen int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowTx @@ -2476,24 +2503,23 @@ func (m *MsgLiquidate) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - msglen |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - if msglen < 0 { + intStringLen := int(stringLen) + if intStringLen < 0 { return ErrInvalidLengthTx } - postIndex := iNdEx + msglen + postIndex := iNdEx + intStringLen if postIndex < 0 { return ErrInvalidLengthTx } if postIndex > l { return io.ErrUnexpectedEOF } - if err := m.Reward.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } + m.RewardDenom = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex @@ -2912,6 +2938,39 @@ func (m *MsgLiquidateResponse) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Collateral", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Collateral.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Reward", wireType) }