From 828d73de34d25609b50e3a7a76797fcb180a534d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:04:25 -0700 Subject: [PATCH 01/42] featgit statusiquidation directly rewards base assets --- app/test_helpers.go | 1 + proto/umee/leverage/v1/tx.proto | 15 +- x/leverage/client/cli/tx.go | 10 +- x/leverage/client/tests/tests.go | 82 ++++---- x/leverage/keeper/keeper.go | 239 +++++------------------ x/leverage/keeper/keeper_test.go | 43 ++-- x/leverage/keeper/liquidate.go | 167 ++++++++++++++++ x/leverage/keeper/math.go | 26 +++ x/leverage/keeper/math_test.go | 29 +++ x/leverage/keeper/msg_server.go | 19 +- x/leverage/keeper/oracle.go | 31 ++- x/leverage/keeper/oracle_test.go | 13 +- 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/types/errors.go | 5 +- x/leverage/types/events.go | 2 + x/leverage/types/token.go | 12 +- x/leverage/types/tx.go | 27 ++- x/leverage/types/tx.pb.go | 137 +++++++------ 21 files changed, 484 insertions(+), 424 deletions(-) create mode 100644 x/leverage/keeper/liquidate.go diff --git a/app/test_helpers.go b/app/test_helpers.go index 6084fdf229..6315c7b427 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -121,6 +121,7 @@ func IntegrationTestNetworkConfig() network.Config { EnableMsgSupply: true, EnableMsgBorrow: true, Blacklist: false, + MaxCollateralShare: 100, }) // Marshal the modified state and add it back into appGenState diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index b481623c1b..eb1bc54a12 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -30,8 +30,8 @@ service Msg { // facility. rpc RepayAsset(MsgRepayAsset) returns (MsgRepayAssetResponse); - // Liquidate defines a method for repaying a different user's borrowed coins - // to the capital facility in exchange for some of their collateral. + // Liquidate defines a method for repaying another user's borrowed + // coins in exchange for some of their collateral. rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse); } @@ -84,15 +84,14 @@ message MsgRepayAsset { cosmos.base.v1beta1.Coin amount = 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; + string borrower = 2; + cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false]; + string reward_denom = 4; } // MsgSupplyResponse defines the Msg/Supply response type. diff --git a/x/leverage/client/cli/tx.go b/x/leverage/client/cli/tx.go index 500a3b6540..a9e7d37a2f 100644 --- a/x/leverage/client/cli/tx.go +++ b/x/leverage/client/cli/tx.go @@ -243,7 +243,7 @@ func GetCmdRepayAsset() *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", RunE: func(cmd *cobra.Command, args []string) error { @@ -266,13 +266,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 60649e7ed0..04bf278699 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -274,6 +274,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { EnableMsgSupply: true, EnableMsgBorrow: true, Blacklist: false, + MaxCollateralShare: 100, }, }, }, @@ -366,17 +367,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { val.Address.String(), val.Address.String(), "5uumee", - "4uumee", - }, - nil, - } - - fixCollateral := testTransaction{ - "add back collateral received from liquidation", - cli.GetCmdAddCollateral(), - []string{ - val.Address.String(), - "4u/uumee", + "uumee", }, nil, } @@ -386,7 +377,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { cli.GetCmdRepayAsset(), []string{ val.Address.String(), - "51uumee", + "50uumee", }, nil, } @@ -396,7 +387,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { cli.GetCmdRemoveCollateral(), []string{ val.Address.String(), - "1000u/uumee", + "950u/uumee", }, nil, } @@ -406,7 +397,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { cli.GetCmdWithdrawAsset(), []string{ val.Address.String(), - "1000u/uumee", + "950u/uumee", }, nil, } @@ -420,8 +411,9 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { }, false, &types.QueryTokenMarketSizeResponse{}, - &types.QueryTokenMarketSizeResponse{MarketSize: sdk.NewInt(1001)}, + &types.QueryTokenMarketSizeResponse{MarketSize: sdk.NewInt(1000)}, }, + testQuery{ "query available to borrow", cli.GetCmdQueryAvailableBorrow(), @@ -430,7 +422,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { }, false, &types.QueryAvailableBorrowResponse{}, - &types.QueryAvailableBorrowResponse{Amount: sdk.NewInt(955)}, + &types.QueryAvailableBorrowResponse{Amount: sdk.NewInt(950)}, }, testQuery{ "query market size", @@ -440,7 +432,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { }, false, &types.QueryMarketSizeResponse{}, - &types.QueryMarketSizeResponse{MarketSizeUsd: sdk.MustNewDecFromStr("0.03424421")}, + &types.QueryMarketSizeResponse{MarketSizeUsd: sdk.MustNewDecFromStr("0.03421")}, }, testQuery{ "query supplied - all", @@ -452,7 +444,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QuerySuppliedResponse{}, &types.QuerySuppliedResponse{ Supplied: sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 1001), + sdk.NewInt64Coin(umeeapp.BondDenom, 1000), ), }, }, @@ -467,10 +459,11 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QuerySuppliedResponse{}, &types.QuerySuppliedResponse{ Supplied: sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 1001), + sdk.NewInt64Coin(umeeapp.BondDenom, 1000), ), }, }, + testQuery{ "query borrowed - all", cli.GetCmdQueryBorrowed(), @@ -481,7 +474,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryBorrowedResponse{}, &types.QueryBorrowedResponse{ Borrowed: sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 47), + sdk.NewInt64Coin(umeeapp.BondDenom, 51), ), }, }, @@ -496,7 +489,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryBorrowedResponse{}, &types.QueryBorrowedResponse{ Borrowed: sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 47), + sdk.NewInt64Coin(umeeapp.BondDenom, 51), ), }, }, @@ -509,7 +502,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { false, &types.QueryTotalBorrowedResponse{}, &types.QueryTotalBorrowedResponse{ - Amount: sdk.NewInt(47), + Amount: sdk.NewInt(51), }, }, testQuery{ @@ -565,8 +558,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { // From app/test_helpers.go/IntegrationTestNetworkConfig // This result is umee's oracle exchange rate times the // amount supplied. - SuppliedValue: sdk.MustNewDecFromStr("0.03424421"), - // (1001 / 1000000) umee * 34.21 = 0.03424421 + SuppliedValue: sdk.MustNewDecFromStr("0.03421"), + // (1000 / 1000000) umee * 34.21 = 0.034241 }, }, testQuery{ @@ -580,8 +573,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QuerySuppliedValueResponse{}, &types.QuerySuppliedValueResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - SuppliedValue: sdk.MustNewDecFromStr("0.03424421"), - // (1001 / 1000000) umee * 34.21 = 0.03424421 + SuppliedValue: sdk.MustNewDecFromStr("0.03421"), + // (1000 / 1000000) umee * 34.21 = 0.03421 }, }, testQuery{ @@ -594,8 +587,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryCollateralValueResponse{}, &types.QueryCollateralValueResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - CollateralValue: sdk.MustNewDecFromStr("0.03424421"), - // (1001 / 1000000) umee * 34.21 = 0.03424421 + CollateralValue: sdk.MustNewDecFromStr("0.03421"), + // (1001 / 1000000) umee * 34.21 = 0.03421 }, }, testQuery{ @@ -608,8 +601,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { false, &types.QueryCollateralValueResponse{}, &types.QueryCollateralValueResponse{ - CollateralValue: sdk.MustNewDecFromStr("0.03424421"), - // (1001 / 1000000) umee * 34.21 = 0.03424421 + CollateralValue: sdk.MustNewDecFromStr("0.03421"), + // (1000 / 1000000) umee * 34.21 = 0.03421 }, }, testQuery{ @@ -622,8 +615,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryBorrowedValueResponse{}, &types.QueryBorrowedValueResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - BorrowedValue: sdk.MustNewDecFromStr("0.00160787"), - // (51 / 1000000) umee * 34.21 = 0.00160787 + BorrowedValue: sdk.MustNewDecFromStr("0.00174471"), + // (51 / 1000000) umee * 34.21 = 0.00174471 }, }, testQuery{ @@ -637,8 +630,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryBorrowedValueResponse{}, &types.QueryBorrowedValueResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - BorrowedValue: sdk.MustNewDecFromStr("0.00160787"), - // (51 / 1000000) umee * 34.21 = 0.00160787 + BorrowedValue: sdk.MustNewDecFromStr("0.00174471"), + // (51 / 1000000) umee * 34.21 = 0.00174471 }, }, testQuery{ @@ -651,8 +644,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryBorrowLimitResponse{}, &types.QueryBorrowLimitResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - BorrowLimit: sdk.MustNewDecFromStr("0.0017122105"), - // (1001 / 1000000) * 0.05 * 34.21 = 0.0017122105 + BorrowLimit: sdk.MustNewDecFromStr("0.0017105"), + // (1000 / 1000000) * 0.05 * 34.21 = 0.0017105 }, }, testQuery{ @@ -665,8 +658,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { &types.QueryLiquidationThresholdResponse{}, &types.QueryLiquidationThresholdResponse{ // From app/test_helpers.go/IntegrationTestNetworkConfig - LiquidationThreshold: sdk.MustNewDecFromStr("0.0017122105"), - // (1001 / 1000000) * 0.05 * 34.21 = 0.0017122105 + LiquidationThreshold: sdk.MustNewDecFromStr("0.0017105"), + // (1000 / 1000000) * 0.05 * 34.21 = 0.0017105 }, }, } @@ -679,17 +672,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/keeper.go b/x/leverage/keeper/keeper.go index 0d9a7258ca..4439bd7ba1 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -357,215 +357,68 @@ func (k Keeper) RemoveCollateral(ctx sdk.Context, borrowerAddr sdk.AccAddress, c // 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 + ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, desiredRepay sdk.Coin, rewardDenom string, +) (sdk.Coin, sdk.Coin, sdk.Coin, error) { + if err := k.validateAcceptedAsset(ctx, desiredRepay); 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 Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances + baseRepay, collateralReward, baseReward, err := k.liquidationMaximum( + ctx, + liquidatorAddr, + borrowerAddr, + desiredRepay, + rewardDenom, + ) if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, collateral) + // 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 } - - // 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) + // 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 } - // 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 + // 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 } - - // 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) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + // 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 } - - if repayValue.GT(maxRepayValue) { - // repayment *= (maxRepayValue / repayValue) - repayment.Amount = repayment.Amount.ToDec().Mul(maxRepayValue).Quo(repayValue).TruncateInt() - } - - // Given repay denom and amount, use oracle to find equivalent amount of rewardDenom. - baseReward, err := k.EquivalentTokenValue(ctx, repayment, baseRewardDenom) - if err != nil { - return sdk.ZeroInt(), sdk.ZeroInt(), err + if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, collateralReward.Denom).Sub(collateralReward)); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // convert reward tokens back to uTokens - reward, err := k.ExchangeToken(ctx, baseReward) + // 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.ZeroInt(), sdk.ZeroInt(), err + 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) - } - - // 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 - } - - // 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 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 - } - - // 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 - } - - // 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 (see ADR-004) - 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 k.GetBorrowerCollateral(ctx, borrowerAddr).IsZero() { + // TODO: exclude blacklisted collateral + 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 ef091ef2a4..d93973b3b9 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -261,7 +261,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: @@ -584,8 +584,7 @@ func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { // 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) + _, _, _, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) s.Require().Error(err) // Note: Setting umee collateral weight to 0.0 to allow liquidation @@ -595,52 +594,48 @@ func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { 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) + repaid, collateral, reward, err := s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) s.Require().NoError(err) - s.Require().Equal(repayment.Amount, repaid) - s.Require().Equal(sdk.NewInt(11000000), reward) + s.Require().Equal(repayment, repaid) + s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 11000000), collateral) + s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 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) + // verify liquidator's u/umee balance is still zero uTokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 11000000)) + s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 0)) - // verify liquidator's new umee balance (10k - 10) = 9990 umee + // verify liquidator's new umee balance (10k + 1) = 10001 umee tokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9990000000)) + s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 10001000000)) // 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) + repaid, collateral, reward, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeDenom) s.Require().NoError(err) - s.Require().Equal(sdk.NewInt(80000000), repaid) - s.Require().Equal(sdk.NewInt(88000000), reward) + s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 80000000), repaid) + s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 88000000), collateral) + s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 88000000), reward) - // verify that repayment has not been modified + // verify that desired repayment has not been modified, despite actual repayment being lower s.Require().Equal(sdk.NewInt(300000000), repayment.Amount) - // verify liquidator's new u/umee balance = 99 = (90 + liquidation incentive) + // verify liquidator's u/umee is still zero uTokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 99000000)) + s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 0)) // 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 + // verify liquidator's new umee balance (10k + 9) = 10009 umee tokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9910000000)) + s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 10009000000), tokenBalance) } func (s *IntegrationTestSuite) TestRepayBadDebt() { diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go new file mode 100644 index 0000000000..8cdccb9aa1 --- /dev/null +++ b/x/leverage/keeper/liquidate.go @@ -0,0 +1,167 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/umee-network/umee/v2/x/leverage/types" +) + +// liquidationMaximum takes a repayment and reward proposed by a liquidator and calculates +// the maximum repayment amount a target address is eligible for, and the corresponding reward +// amount using current oracle prices and liquidation incentive. Inputs must be registered tokens. +// Outputs are base token repayment, uToken collateral cost, and base token reward from liquidation. +func (k Keeper) liquidationMaximum( + ctx sdk.Context, + liquidatorAddr sdk.AccAddress, + targetAddr sdk.AccAddress, + desiredRepay sdk.Coin, + rewardDenom string, +) (sdk.Coin, sdk.Coin, sdk.Coin, error) { + // get liquidator's available balance of base asset to repay + availableRepay := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(desiredRepay.Denom) + + // get module's available balance of reward base asset + availableReward := k.ModuleBalance(ctx, rewardDenom).Sub(k.GetReserveAmount(ctx, rewardDenom)) + + // examine target address + collateral := k.GetBorrowerCollateral(ctx, targetAddr) + borrowed := k.GetBorrowerBorrows(ctx, targetAddr) + + // calculate position health in USD values + borrowedValue, err := k.TotalTokenValue(ctx, borrowed) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, collateral) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + if liquidationThreshold.GTE(borrowedValue) { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationIneligible.Wrapf( + "%s borrowed value %s is below the liquidation threshold %s", + targetAddr, borrowedValue, liquidationThreshold) + } + + // get dynamic close factor and liquidation incentive + liquidationIncentive, closeFactor, err := k.liquidationParams(ctx, rewardDenom, borrowedValue, liquidationThreshold) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + + // maximum repayment starts at desiredRepayment but can be lower due to limiting factors + baseRepay := desiredRepay + // repayment cannot exceed borrower's borrowed amount of selected denom + baseRepay.Amount = sdk.MinInt(baseRepay.Amount, borrowed.AmountOf(baseRepay.Denom)) + // repayment cannot exceed liquidator's available balance + baseRepay.Amount = sdk.MinInt(baseRepay.Amount, availableRepay) + // repayment USD value cannot exceed borrowed USD value * close factor + repayValueLimit := borrowedValue.Mul(closeFactor) + repayValue, err := k.TokenValue(ctx, baseRepay) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + // if repayValue > repayValueLimit + // maxRepayment *= (repayValueLimit / repayValue) + ReduceProportionallyDec(repayValueLimit, repayValue, &baseRepay.Amount) + if baseRepay.IsZero() { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRepayZero + } + + // find the price ratio of repay:reward tokens + priceRatio, err := k.PriceRatio(ctx, baseRepay.Denom, rewardDenom) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + // determine uToken collateral reward, rounding up + uReward := sdk.NewCoin( + k.FromTokenToUTokenDenom(ctx, rewardDenom), + // uReward = repay * (repayPrice / rewardPrice) * (1 + incentive), rounded up + baseRepay.Amount.ToDec().Mul(priceRatio).Mul(sdk.OneDec().Add(liquidationIncentive)).Ceil().RoundInt(), + ) + + // uToken reward cannot exceed available collateral + availableCollateral := collateral.AmountOf(uReward.Denom) + // if uReward > availableCollateral + // uReward = availableCollateral + // baseRepay *= (availableCollateral / uReward) + ReduceProportionally(availableCollateral, uReward.Amount, &uReward.Amount, &baseRepay.Amount) + if uReward.IsZero() { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRewardZero + } + + // convert uToken reward to base tokens, rounding down + baseReward, err := k.ExchangeUToken(ctx, uReward) + if err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + // base reward cannot exceed available reward + // if baseReward > availableReward + // baseReward = availableReward + // uReward *= (availableReward / baseReward) + // baseRepay *= (availableReward / baseReward) + ReduceProportionally(availableReward, baseReward.Amount, &baseReward.Amount, &uReward.Amount, &baseRepay.Amount) + + return baseRepay, uReward, baseReward, nil +} + +// liquidationParams computes dynamic liquidation parameters based on a collateral reward +// denomination, and a borrower's 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, + borrowedValue sdk.Dec, + liquidationThreshold sdk.Dec, +) (sdk.Dec, sdk.Dec, error) { + if borrowedValue.IsNegative() { + return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, borrowedValue.String()) + } + if liquidationThreshold.IsNegative() { + return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, liquidationThreshold.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 liquidationThreshold.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 borrowedValue.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( + borrowedValue.Quo(liquidationThreshold).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 +} diff --git a/x/leverage/keeper/math.go b/x/leverage/keeper/math.go index 0a756d535b..304c1786e3 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/keeper/math.go @@ -27,3 +27,29 @@ func ApproxExponential(x sdk.Dec) sdk.Dec { sum = sum.Add(next.QuoInt64(6)) // 3! return sum // approximated e^x } + +// ReduceProportionally accepts two sdk.Int to be interpreted as a fraction (a/b), and +// any number of pointers to sdk.Int which will be multiplied by (a/b) if a < b, then +// rounded up. If a >= b or b == 0 this is a no-op. +func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { + if a.GTE(b) || b.IsZero() { + return + } + ratio := a.ToDec().Quo(b.ToDec()) // a/b + for _, n := range nums { + *n = n.ToDec().Mul(ratio).Ceil().RoundInt() + } +} + +// ReduceProportionallyDec accepts two sdk.Dec to be interpreted as a fraction (a/b), and +// any number of pointers to sdk.Int which will be multiplied by (a/b) if a < b, then +// rounded up. If a >= b or b == 0 this is a no-op. +func ReduceProportionallyDec(a, b sdk.Dec, nums ...*sdk.Int) { + if a.GTE(b) || b.IsZero() { + return + } + ratio := a.Quo(b) + for _, n := range nums { + *n = n.ToDec().Mul(ratio).Ceil().RoundInt() + } +} diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index b2351466a0..99d814d9eb 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -40,3 +40,32 @@ func TestInterpolate(t *testing.T) { x = Interpolate(x1, x1, y1, x1, y1) require.Equal(t, x, y1) } + +func TestReduceProportional(t *testing.T) { + testCase := func(a, b, initial, expected int64) { + n := sdk.NewInt(initial) + ReduceProportionally(sdk.NewInt(a), sdk.NewInt(b), &n) + require.Equal(t, expected, n.Int64()) + + m := sdk.NewInt(initial) + ReduceProportionallyDec(sdk.NewDecFromInt(sdk.NewInt(a)), sdk.NewDecFromInt(sdk.NewInt(b)), &m) + require.Equal(t, expected, m.Int64()) + } + + // No-op tests + testCase(2, 1, 40, 40) // a/b > 0 + testCase(1, 0, 50, 50) // b == 0 + + // Zero result tests + testCase(1, 2, 0, 0) // (1/2)0 = 0 + testCase(0, 1, 60, 0) // (0/1)60 = 0 + + // Round number tests + testCase(1, 2, 70, 35) // (1/2)70 = 35 + testCase(1, 2, 8866, 4433) // (1/2)8866 = 4433 + testCase(1, 3, 3000, 1000) // (1/3)3000 = 1000 + + // Ceiling tests + testCase(1, 3, 1, 1) // (1/3)1 -> 1 + testCase(1, 3, 10, 4) // (1/3)10 -> 4 +} diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index ce6f5bac70..24acbed8f5 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -265,21 +265,19 @@ func (s msgServer) Liquidate( return nil, err } - repaid, reward, err := s.keeper.LiquidateBorrow(ctx, liquidatorAddr, borrowerAddr, msg.Repayment, msg.Reward) + repaid, collateral, reward, err := s.keeper.LiquidateBorrow(ctx, liquidatorAddr, borrowerAddr, 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(), "attempted", msg.Repayment.String(), + "repaid", repaid.String(), + "collateral", collateral.String(), + "reward", reward.String(), ) ctx.EventManager().EmitEvents(sdk.Events{ @@ -287,9 +285,10 @@ func (s msgServer) Liquidate( 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.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, @@ -299,7 +298,7 @@ func (s msgServer) Liquidate( }) return &types.MsgLiquidateResponse{ - Repaid: repaidCoin, - Reward: rewardCoin, + Repaid: repaid, + Reward: reward, }, nil } diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 28598f652a..7799ed1607 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -66,34 +66,25 @@ 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 sdk.ZeroDec(), err } - - // return immediately on zero input value if p1.IsZero() { - return sdk.NewCoin(toDenom, sdk.ZeroInt()), nil + return sdk.ZeroDec(), types.ErrZeroValuePriceRatio.Wrap(fromDenom) } - - // get USD price of output denomination p2, err := k.TokenPrice(ctx, toDenom) if err != nil { - return sdk.Coin{}, err + return sdk.ZeroDec(), err } - if !p2.IsPositive() { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrBadValue, p2.String()) + if p2.IsZero() { + return sdk.ZeroDec(), types.ErrZeroValuePriceRatio.Wrap(toDenom) } - - // 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..4b69e49b2a 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -96,16 +96,15 @@ func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { s.Require().Equal(sdk.ZeroDec(), 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 5f8adbbb85..4a1f5ed4bb 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 { // validateBorrowAsset validates an sdk.Coin and ensures its Denom is a Token with EnableMsgBorrow func (k Keeper) validateBorrowAsset(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) validateBorrowAsset(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 1955f67be6..72664e8b2f 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 09d49a18e0..9c4dd12a20 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -321,7 +321,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: umee1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7wrm6ea borrowed value 0.001000000000000000 is below the liquidation threshold 0.005000000000000000: borrower not eligible for liquidation", ) var msg types.MsgLiquidate diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index f7929df3b2..146312cc7e 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,4 +30,7 @@ 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") + ErrLiquidationRepayZero = sdkerrors.Register(ModuleName, 1123, "calculated liquidation repayment was zero") + ErrLiquidationRewardZero = sdkerrors.Register(ModuleName, 1124, "calculated liquidation reward was zero") + ErrZeroValuePriceRatio = sdkerrors.Register(ModuleName, 1125, "price ratio attempted with zero value token") ) diff --git a/x/leverage/types/events.go b/x/leverage/types/events.go index 4c44d054a0..fa014cd96e 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 5f7d52f56a..b1cbf42c6c 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -91,7 +91,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) @@ -99,10 +99,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 c664b39e19..813872032f 100644 --- a/x/leverage/types/tx.go +++ b/x/leverage/types/tx.go @@ -199,12 +199,12 @@ func (msg *MsgRepayAsset) 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, } } @@ -212,23 +212,18 @@ func (msg MsgLiquidate) Route() string { return ModuleName } func (msg MsgLiquidate) Type() string { return EventTypeLiquidate } func (msg *MsgLiquidate) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.GetLiquidator()) - if err != nil { + if _, err := sdk.AccAddressFromBech32(msg.GetLiquidator()); err != nil { return err } - _, err = sdk.AccAddressFromBech32(msg.GetBorrower()) - if err != nil { + if _, err := sdk.AccAddressFromBech32(msg.GetBorrower()); err != nil { return err } - - if asset := msg.GetRepayment(); !asset.IsValid() { - return sdkerrors.Wrap(ErrInvalidAsset, asset.String()) + if err := msg.Repayment.Validate(); err != nil { + return err } - - if asset := msg.GetReward(); !asset.IsValid() { - return sdkerrors.Wrap(ErrInvalidAsset, asset.String()) + if err := sdk.ValidateDenom(msg.RewardDenom); err != nil { + return err } - return nil } diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index 649fc1ab4e..ee5f805c6d 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -360,15 +360,14 @@ func (m *MsgRepayAsset) GetAmount() types.Coin { return types.Coin{} } -// 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 string `protobuf:"bytes,2,opt,name=borrower,proto3" json:"borrower,omitempty"` + Repayment types.Coin `protobuf:"bytes,3,opt,name=repayment,proto3" json:"repayment"` + RewardDenom string `protobuf:"bytes,4,opt,name=reward_denom,json=rewardDenom,proto3" json:"reward_denom,omitempty"` } func (m *MsgLiquidate) Reset() { *m = MsgLiquidate{} } @@ -425,11 +424,11 @@ func (m *MsgLiquidate) GetRepayment() types.Coin { return types.Coin{} } -func (m *MsgLiquidate) GetReward() types.Coin { +func (m *MsgLiquidate) GetRewardDenom() string { if m != nil { - return m.Reward + return m.RewardDenom } - return types.Coin{} + return "" } // MsgSupplyResponse defines the Msg/Supply response type. @@ -735,45 +734,45 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 594 bytes of a gzipped FileDescriptorProto + // 607 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xc1, 0x6e, 0xd3, 0x40, - 0x10, 0x8d, 0xd3, 0x10, 0x91, 0x29, 0x45, 0xc1, 0x2d, 0xc8, 0x75, 0x85, 0x1b, 0x19, 0x01, 0x11, - 0x52, 0x6d, 0x92, 0x1e, 0x38, 0x71, 0x68, 0x7a, 0xab, 0xb0, 0x84, 0x52, 0x09, 0x04, 0x17, 0x70, - 0xe2, 0xc5, 0xb1, 0xb0, 0xbd, 0x66, 0x77, 0x93, 0x34, 0x1f, 0x80, 0xc4, 0x91, 0x6f, 0xe2, 0xd4, - 0x63, 0x8f, 0x9c, 0x10, 0x4a, 0x7e, 0x04, 0xad, 0x13, 0x6f, 0xec, 0xe0, 0x34, 0x81, 0x2a, 0xb7, - 0x5d, 0xbf, 0x37, 0xf3, 0x66, 0x66, 0xd7, 0x6f, 0x61, 0xbf, 0x1f, 0x20, 0x64, 0xfa, 0x68, 0x80, - 0x88, 0xed, 0x22, 0x73, 0xd0, 0x30, 0xd9, 0x85, 0x11, 0x11, 0xcc, 0xb0, 0x5c, 0xe5, 0x90, 0x91, - 0x40, 0xc6, 0xa0, 0xa1, 0x6a, 0x5d, 0x4c, 0x03, 0x4c, 0xcd, 0x8e, 0x4d, 0x39, 0xb5, 0x83, 0x98, - 0xdd, 0x30, 0xbb, 0xd8, 0x0b, 0xa7, 0x11, 0xea, 0x9e, 0x8b, 0x5d, 0x1c, 0x2f, 0x4d, 0xbe, 0x9a, - 0x7e, 0xd5, 0x3f, 0x42, 0xc5, 0xa2, 0xee, 0x79, 0x3f, 0x8a, 0xfc, 0x91, 0xac, 0xc2, 0x6d, 0xca, - 0x57, 0x1e, 0x22, 0x8a, 0x54, 0x93, 0xea, 0x95, 0xb6, 0xd8, 0xcb, 0x2f, 0xa0, 0x6c, 0x07, 0xb8, - 0x1f, 0x32, 0xa5, 0x58, 0x93, 0xea, 0xdb, 0xcd, 0x7d, 0x63, 0xaa, 0x67, 0x70, 0x3d, 0x63, 0xa6, - 0x67, 0x9c, 0x62, 0x2f, 0x6c, 0x95, 0x2e, 0x7f, 0x1d, 0x16, 0xda, 0x33, 0xba, 0xee, 0x42, 0xd5, - 0xa2, 0xee, 0x5b, 0x8f, 0xf5, 0x1c, 0x62, 0x0f, 0x4f, 0x28, 0x45, 0x6c, 0x33, 0x42, 0xdd, 0x58, - 0xe8, 0xc4, 0x71, 0x4e, 0xb1, 0xef, 0xdb, 0x0c, 0x11, 0xdb, 0xe7, 0x42, 0x1d, 0x4c, 0x08, 0x1e, - 0xce, 0x85, 0x92, 0xbd, 0x7c, 0x0c, 0x25, 0x3e, 0x9e, 0x75, 0x65, 0x62, 0xb2, 0xfe, 0x09, 0x76, - 0x2d, 0xea, 0xb6, 0x51, 0x80, 0x07, 0x68, 0x93, 0x3a, 0x08, 0xee, 0x5a, 0xd4, 0x6d, 0xc5, 0x39, - 0xc4, 0xcc, 0x96, 0x4a, 0xfc, 0xf7, 0xcc, 0x1c, 0xd8, 0x89, 0xdb, 0x89, 0xec, 0xd1, 0x06, 0x55, - 0x7e, 0x48, 0x70, 0xc7, 0xa2, 0xee, 0x2b, 0xef, 0x4b, 0xdf, 0x73, 0x6c, 0x86, 0x64, 0x0d, 0xc0, - 0x9f, 0x6d, 0x70, 0xa2, 0x93, 0xfa, 0x92, 0xa9, 0xa2, 0xb8, 0x50, 0xc5, 0x4b, 0xa8, 0x10, 0x5e, - 0x6f, 0x80, 0x42, 0xa6, 0x6c, 0xad, 0x57, 0xc8, 0x3c, 0x82, 0x37, 0x41, 0xd0, 0xd0, 0x26, 0x8e, - 0x52, 0x5a, 0xb3, 0x89, 0x29, 0x5d, 0xdf, 0x85, 0x7b, 0xe2, 0x4f, 0x69, 0x23, 0x1a, 0xe1, 0x90, - 0x22, 0x5d, 0x05, 0x65, 0xf1, 0x72, 0x2f, 0x60, 0x99, 0xfb, 0x28, 0xb0, 0x87, 0x70, 0x90, 0x73, - 0x8d, 0x04, 0xac, 0xc0, 0x83, 0xec, 0xe9, 0x0b, 0xe4, 0x35, 0xdc, 0xcf, 0x1c, 0x58, 0x02, 0x4c, - 0xfb, 0x8a, 0x6c, 0xcf, 0x89, 0xc7, 0xb9, 0x5e, 0x5f, 0x9c, 0xae, 0x7f, 0x93, 0x60, 0x2f, 0x7d, - 0x38, 0x37, 0xce, 0x98, 0x1a, 0x71, 0xf1, 0x9f, 0x46, 0xdc, 0xfc, 0x7a, 0x0b, 0xb6, 0x2c, 0xea, - 0xca, 0x67, 0x50, 0x9e, 0x39, 0xd2, 0x81, 0xb1, 0xe8, 0x73, 0x86, 0x38, 0x04, 0xf5, 0xd1, 0x35, - 0xa0, 0xe8, 0xe2, 0x03, 0xec, 0x64, 0xbd, 0x47, 0xcf, 0x8d, 0xca, 0x70, 0xd4, 0x67, 0xab, 0x39, - 0x69, 0x81, 0xac, 0xe7, 0xe4, 0x0b, 0x64, 0x38, 0x4b, 0x04, 0x72, 0xef, 0x8a, 0xdc, 0x83, 0xea, - 0x5f, 0x7e, 0xf3, 0x38, 0x37, 0x7e, 0x91, 0xa6, 0x1e, 0xad, 0x45, 0x13, 0x4a, 0xef, 0x60, 0x3b, - 0xed, 0x38, 0xb5, 0xdc, 0xe8, 0x14, 0x43, 0xad, 0xaf, 0x62, 0x88, 0xd4, 0x6f, 0x00, 0x52, 0x2e, - 0x73, 0xb8, 0xa4, 0xae, 0x84, 0xa0, 0x3e, 0x5d, 0x41, 0x10, 0x79, 0xcf, 0xa1, 0x92, 0xb2, 0x95, - 0xdc, 0x28, 0x81, 0xab, 0x4f, 0xae, 0xc7, 0x93, 0xa4, 0xad, 0xb3, 0xcb, 0xb1, 0x26, 0x5d, 0x8d, - 0x35, 0xe9, 0xf7, 0x58, 0x93, 0xbe, 0x4f, 0xb4, 0xc2, 0xd5, 0x44, 0x2b, 0xfc, 0x9c, 0x68, 0x85, - 0xf7, 0xcf, 0x5d, 0x8f, 0xf5, 0xfa, 0x1d, 0xa3, 0x8b, 0x03, 0x93, 0xe7, 0x3a, 0x0a, 0x11, 0x1b, - 0x62, 0xf2, 0x39, 0xde, 0x98, 0x83, 0xa6, 0x79, 0x31, 0x7f, 0xae, 0xd9, 0x28, 0x42, 0xb4, 0x53, - 0x8e, 0xdf, 0xd9, 0xe3, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x43, 0x08, 0xd3, 0xd5, 0xcc, 0x07, - 0x00, 0x00, + 0x10, 0x8d, 0xdb, 0x52, 0x91, 0x69, 0x8b, 0x82, 0x5b, 0x90, 0xeb, 0x0a, 0x37, 0x18, 0x01, 0x11, + 0x52, 0x6d, 0x92, 0x1e, 0x38, 0x71, 0x68, 0xca, 0xa9, 0xc2, 0x12, 0x4a, 0x25, 0x10, 0x5c, 0x8a, + 0x13, 0x2f, 0x8e, 0x85, 0xed, 0x35, 0xbb, 0x9b, 0xa4, 0xf9, 0x00, 0x24, 0x8e, 0x7c, 0x08, 0x1f, + 0xd2, 0x63, 0x8f, 0x9c, 0x10, 0x4a, 0x7e, 0x04, 0xad, 0x1d, 0x6f, 0xec, 0xe0, 0x34, 0x11, 0x28, + 0xb7, 0xdd, 0x79, 0x6f, 0xe6, 0xcd, 0xce, 0x8c, 0xc7, 0xb0, 0xdf, 0x0b, 0x10, 0x32, 0x7d, 0xd4, + 0x47, 0xc4, 0x76, 0x91, 0xd9, 0xaf, 0x9b, 0xec, 0xd2, 0x88, 0x08, 0x66, 0x58, 0xae, 0x70, 0xc8, + 0x48, 0x21, 0xa3, 0x5f, 0x57, 0xb5, 0x0e, 0xa6, 0x01, 0xa6, 0x66, 0xdb, 0xa6, 0x9c, 0xda, 0x46, + 0xcc, 0xae, 0x9b, 0x1d, 0xec, 0x85, 0x89, 0x87, 0xba, 0xe7, 0x62, 0x17, 0xc7, 0x47, 0x93, 0x9f, + 0x12, 0xab, 0xfe, 0x11, 0xca, 0x16, 0x75, 0xcf, 0x7b, 0x51, 0xe4, 0x0f, 0x65, 0x15, 0x6e, 0x53, + 0x7e, 0xf2, 0x10, 0x51, 0xa4, 0xaa, 0x54, 0x2b, 0xb7, 0xc4, 0x5d, 0x7e, 0x01, 0x9b, 0x76, 0x80, + 0x7b, 0x21, 0x53, 0xd6, 0xaa, 0x52, 0x6d, 0xab, 0xb1, 0x6f, 0x24, 0x7a, 0x06, 0xd7, 0x33, 0x26, + 0x7a, 0xc6, 0x29, 0xf6, 0xc2, 0xe6, 0xc6, 0xd5, 0xaf, 0xc3, 0x52, 0x6b, 0x42, 0xd7, 0x5d, 0xa8, + 0x58, 0xd4, 0x7d, 0xe7, 0xb1, 0xae, 0x43, 0xec, 0xc1, 0x09, 0xa5, 0x88, 0xad, 0x46, 0xa8, 0x13, + 0x0b, 0x9d, 0x38, 0xce, 0x29, 0xf6, 0x7d, 0x9b, 0x21, 0x62, 0xfb, 0x5c, 0xa8, 0x8d, 0x09, 0xc1, + 0x83, 0xa9, 0x50, 0x7a, 0x97, 0x8f, 0x61, 0x83, 0x97, 0x67, 0x59, 0x99, 0x98, 0xac, 0x7f, 0x82, + 0x5d, 0x8b, 0xba, 0x2d, 0x14, 0xe0, 0x3e, 0x5a, 0xa5, 0x0e, 0x82, 0x3b, 0x16, 0x75, 0x9b, 0x71, + 0x0c, 0x51, 0xb3, 0xb9, 0x12, 0xff, 0x5c, 0x33, 0x07, 0x76, 0xe2, 0xe7, 0x44, 0xf6, 0x70, 0x85, + 0x2a, 0x3f, 0x24, 0xd8, 0xb6, 0xa8, 0xfb, 0xda, 0xfb, 0xd2, 0xf3, 0x1c, 0x9b, 0x21, 0x59, 0x03, + 0xf0, 0x27, 0x17, 0x9c, 0xea, 0x64, 0x2c, 0xb9, 0x2c, 0xd6, 0x66, 0xb2, 0x78, 0x09, 0x65, 0xc2, + 0xf3, 0x0d, 0x50, 0xc8, 0x94, 0xf5, 0xe5, 0x12, 0x99, 0x7a, 0xc8, 0x0f, 0x61, 0x9b, 0xa0, 0x81, + 0x4d, 0x9c, 0x0b, 0x07, 0x85, 0x38, 0x50, 0x36, 0xe2, 0xf0, 0x5b, 0x89, 0xed, 0x15, 0x37, 0xe9, + 0xbb, 0x70, 0x57, 0x7c, 0x13, 0x2d, 0x44, 0x23, 0x1c, 0x52, 0xa4, 0xab, 0xa0, 0xcc, 0x8e, 0xf1, + 0x0c, 0x96, 0x9b, 0x3c, 0x81, 0x3d, 0x80, 0x83, 0x82, 0x81, 0x11, 0xb0, 0x02, 0xf7, 0xf3, 0x7d, + 0x16, 0xc8, 0x1b, 0xb8, 0x97, 0x6b, 0x4d, 0x0a, 0xf0, 0x36, 0xf0, 0xe7, 0x78, 0x4e, 0x5c, 0xb8, + 0x65, 0xda, 0x90, 0xd0, 0xf5, 0x6f, 0x12, 0xec, 0x65, 0xdb, 0xf0, 0xdf, 0x11, 0x13, 0x47, 0x5e, + 0xb8, 0xa5, 0x27, 0x22, 0xa1, 0x37, 0xbe, 0xde, 0x82, 0x75, 0x8b, 0xba, 0xf2, 0x19, 0x6c, 0x4e, + 0x76, 0xcf, 0x81, 0x31, 0xbb, 0xd1, 0x0c, 0xd1, 0x04, 0xf5, 0xd1, 0x0d, 0xa0, 0x78, 0xc5, 0x05, + 0xec, 0xe4, 0xb7, 0x8c, 0x5e, 0xe8, 0x95, 0xe3, 0xa8, 0xcf, 0x16, 0x73, 0xb2, 0x02, 0xf9, 0xed, + 0x52, 0x2c, 0x90, 0xe3, 0xcc, 0x11, 0x28, 0x9c, 0x15, 0xb9, 0x0b, 0x95, 0xbf, 0x36, 0xcb, 0xe3, + 0x42, 0xff, 0x59, 0x9a, 0x7a, 0xb4, 0x14, 0x4d, 0x28, 0xbd, 0x87, 0xad, 0xec, 0x6e, 0xa9, 0x16, + 0x7a, 0x67, 0x18, 0x6a, 0x6d, 0x11, 0x43, 0x84, 0x7e, 0x0b, 0x90, 0xd9, 0x27, 0x87, 0x73, 0xf2, + 0x4a, 0x09, 0xea, 0xd3, 0x05, 0x04, 0x11, 0xf7, 0x1c, 0xca, 0x99, 0x05, 0x52, 0xe8, 0x25, 0x70, + 0xf5, 0xc9, 0xcd, 0x78, 0x1a, 0xb4, 0x79, 0x76, 0x35, 0xd2, 0xa4, 0xeb, 0x91, 0x26, 0xfd, 0x1e, + 0x69, 0xd2, 0xf7, 0xb1, 0x56, 0xba, 0x1e, 0x6b, 0xa5, 0x9f, 0x63, 0xad, 0xf4, 0xe1, 0xb9, 0xeb, + 0xb1, 0x6e, 0xaf, 0x6d, 0x74, 0x70, 0x60, 0xf2, 0x58, 0x47, 0x21, 0x62, 0x03, 0x4c, 0x3e, 0xc7, + 0x17, 0xb3, 0xdf, 0x30, 0x2f, 0xa7, 0x3f, 0x66, 0x36, 0x8c, 0x10, 0x6d, 0x6f, 0xc6, 0x7f, 0xd4, + 0xe3, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x78, 0xd1, 0x22, 0x71, 0xb6, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -804,8 +803,8 @@ type MsgClient interface { // RepayAsset defines a method for repaying borrowed coins to the capital // facility. RepayAsset(ctx context.Context, in *MsgRepayAsset, opts ...grpc.CallOption) (*MsgRepayAssetResponse, error) - // Liquidate defines a method for repaying a different user's borrowed coins - // to the capital facility in exchange for some of their collateral. + // Liquidate defines a method for repaying another user's borrowed + // coins in exchange for some of their collateral. Liquidate(ctx context.Context, in *MsgLiquidate, opts ...grpc.CallOption) (*MsgLiquidateResponse, error) } @@ -898,8 +897,8 @@ type MsgServer interface { // RepayAsset defines a method for repaying borrowed coins to the capital // facility. RepayAsset(context.Context, *MsgRepayAsset) (*MsgRepayAssetResponse, error) - // Liquidate defines a method for repaying a different user's borrowed coins - // to the capital facility in exchange for some of their collateral. + // Liquidate defines a method for repaying another user's borrowed + // coins in exchange for some of their collateral. Liquidate(context.Context, *MsgLiquidate) (*MsgLiquidateResponse, error) } @@ -1356,16 +1355,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 { @@ -1701,8 +1697,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 } @@ -2599,9 +2597,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 @@ -2611,24 +2609,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 From 025f017bef3220983f06702efbbf913c70aaa115 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:08:25 -0700 Subject: [PATCH 02/42] make proto-gen --- x/leverage/types/tx.pb.go | 78 +++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index 2eb1b80275..7e593c0a0c 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -734,45 +734,45 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 596 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0x4f, 0x6f, 0xd3, 0x4e, - 0x10, 0x8d, 0xfb, 0x27, 0xfa, 0x65, 0xfa, 0x2b, 0x0a, 0x6e, 0x41, 0xae, 0x2b, 0xdc, 0xc8, 0x08, - 0x88, 0x90, 0x6a, 0x93, 0x54, 0x88, 0x13, 0x87, 0xa6, 0xb7, 0x0a, 0x4b, 0x28, 0x95, 0x40, 0x70, - 0x00, 0xad, 0xe3, 0xc5, 0xb1, 0x70, 0xb2, 0x66, 0x77, 0x93, 0x34, 0x1f, 0x00, 0x89, 0x23, 0x9f, - 0x89, 0x53, 0x8f, 0x3d, 0x72, 0x42, 0x28, 0xf9, 0x22, 0x68, 0x9d, 0x78, 0x63, 0x07, 0xa7, 0x09, - 0xa0, 0xdc, 0x76, 0xf7, 0xbd, 0x99, 0x37, 0x3b, 0x3b, 0x7e, 0x86, 0x83, 0x5e, 0x07, 0x63, 0x3b, - 0xc4, 0x7d, 0x4c, 0x91, 0x8f, 0xed, 0x7e, 0xcd, 0xe6, 0x97, 0x56, 0x44, 0x09, 0x27, 0x6a, 0x59, - 0x40, 0x56, 0x02, 0x59, 0xfd, 0x9a, 0x6e, 0xb4, 0x08, 0xeb, 0x10, 0x66, 0xbb, 0x88, 0x09, 0xaa, - 0x8b, 0x39, 0xaa, 0xd9, 0x2d, 0x12, 0x74, 0x27, 0x11, 0xfa, 0xbe, 0x4f, 0x7c, 0x12, 0x2f, 0x6d, - 0xb1, 0x9a, 0x9c, 0x9a, 0xef, 0xa0, 0xe4, 0x30, 0xff, 0xa2, 0x17, 0x45, 0xe1, 0x50, 0xd5, 0xe1, - 0x3f, 0x26, 0x56, 0x01, 0xa6, 0x9a, 0x52, 0x51, 0xaa, 0xa5, 0xa6, 0xdc, 0xab, 0x4f, 0x61, 0x1b, - 0x31, 0x86, 0xb9, 0xb6, 0x51, 0x51, 0xaa, 0x3b, 0xf5, 0x03, 0x6b, 0x22, 0x67, 0x09, 0x39, 0x6b, - 0x2a, 0x67, 0x9d, 0x91, 0xa0, 0xdb, 0xd8, 0xba, 0xfa, 0x71, 0x54, 0x68, 0x4e, 0xd8, 0x26, 0x86, - 0xb2, 0xc3, 0xfc, 0xd7, 0x01, 0x6f, 0x7b, 0x14, 0x0d, 0x4e, 0xc5, 0xd9, 0x3a, 0x64, 0x5a, 0xb1, - 0xcc, 0xa9, 0xe7, 0x9d, 0x91, 0x30, 0x44, 0x1c, 0x53, 0x14, 0x0a, 0x19, 0x97, 0x50, 0x4a, 0x06, - 0x33, 0x99, 0x64, 0xaf, 0x9e, 0xc0, 0x96, 0x68, 0xcd, 0xaa, 0x2a, 0x31, 0xd9, 0xfc, 0x00, 0x7b, - 0x0e, 0xf3, 0x9b, 0xb8, 0x43, 0xfa, 0x78, 0x9d, 0x3a, 0x2d, 0xb8, 0xe5, 0x30, 0xbf, 0x11, 0xe7, - 0x90, 0x1d, 0x5b, 0x28, 0xf1, 0x97, 0x1d, 0x73, 0x61, 0x37, 0xbe, 0x4c, 0x84, 0x86, 0x6b, 0xd3, - 0xf8, 0xa6, 0xc0, 0xff, 0x0e, 0xf3, 0x5f, 0x04, 0x9f, 0x7a, 0x81, 0x87, 0x38, 0x56, 0x0d, 0x80, - 0x70, 0xba, 0x21, 0x89, 0x4a, 0xea, 0x24, 0x53, 0xc3, 0xc6, 0x5c, 0x0d, 0xcf, 0xa1, 0x44, 0x45, - 0xb5, 0x1d, 0xdc, 0xe5, 0xda, 0xe6, 0x6a, 0x75, 0xcc, 0x22, 0xd4, 0x67, 0x50, 0xa4, 0x78, 0x80, - 0xa8, 0xa7, 0x6d, 0xad, 0x16, 0x3b, 0xa5, 0x9b, 0x7b, 0x70, 0x5b, 0x7e, 0x21, 0x4d, 0xcc, 0x22, - 0xd2, 0x65, 0xd8, 0xd4, 0x41, 0x9b, 0x1f, 0xeb, 0x39, 0x2c, 0x33, 0x8b, 0x12, 0xbb, 0x07, 0x87, - 0x39, 0x23, 0x24, 0x61, 0x0d, 0xee, 0x66, 0x5f, 0x5e, 0x22, 0x2f, 0xe1, 0x4e, 0xe6, 0xb9, 0x12, - 0x60, 0x72, 0xaf, 0x08, 0x05, 0x5e, 0xdc, 0xce, 0xd5, 0xee, 0x25, 0xe8, 0xe6, 0x17, 0x05, 0xf6, - 0xd3, 0x8f, 0xf3, 0xcf, 0x19, 0x53, 0x2d, 0xde, 0xf8, 0xa3, 0x16, 0xd7, 0x3f, 0x6f, 0xc3, 0xa6, - 0xc3, 0x7c, 0xf5, 0x1c, 0x8a, 0x53, 0x27, 0x3a, 0xb4, 0xe6, 0xfd, 0xcd, 0x92, 0x8f, 0xa0, 0xdf, - 0xbf, 0x01, 0x94, 0xb7, 0x78, 0x0f, 0xbb, 0x59, 0xd7, 0x31, 0x73, 0xa3, 0x32, 0x1c, 0xfd, 0xf1, - 0x72, 0x4e, 0x5a, 0x20, 0xeb, 0x37, 0xf9, 0x02, 0x19, 0xce, 0x02, 0x81, 0xdc, 0x59, 0x51, 0xdb, - 0x50, 0xfe, 0xcd, 0x6b, 0x1e, 0xe4, 0xc6, 0xcf, 0xd3, 0xf4, 0xe3, 0x95, 0x68, 0x52, 0xe9, 0x0d, - 0xec, 0xa4, 0xdd, 0xa6, 0x92, 0x1b, 0x9d, 0x62, 0xe8, 0xd5, 0x65, 0x0c, 0x99, 0xfa, 0x15, 0x40, - 0xca, 0x63, 0x8e, 0x16, 0xd4, 0x95, 0x10, 0xf4, 0x47, 0x4b, 0x08, 0x32, 0xef, 0x05, 0x94, 0x52, - 0xb6, 0x92, 0x1b, 0x25, 0x71, 0xfd, 0xe1, 0xcd, 0x78, 0x92, 0xb4, 0x71, 0x7e, 0x35, 0x32, 0x94, - 0xeb, 0x91, 0xa1, 0xfc, 0x1c, 0x19, 0xca, 0xd7, 0xb1, 0x51, 0xb8, 0x1e, 0x1b, 0x85, 0xef, 0x63, - 0xa3, 0xf0, 0xf6, 0x89, 0x1f, 0xf0, 0x76, 0xcf, 0xb5, 0x5a, 0xa4, 0x63, 0x8b, 0x5c, 0xc7, 0x5d, - 0xcc, 0x07, 0x84, 0x7e, 0x8c, 0x37, 0x76, 0xbf, 0x6e, 0x5f, 0xce, 0x7e, 0xd3, 0x7c, 0x18, 0x61, - 0xe6, 0x16, 0xe3, 0xff, 0xeb, 0xc9, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x8b, 0xb0, 0x2d, 0x83, - 0xc4, 0x07, 0x00, 0x00, + // 607 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcd, 0x6e, 0xd3, 0x40, + 0x10, 0x8e, 0xfb, 0x27, 0x32, 0x69, 0x51, 0x70, 0x0b, 0x72, 0x5d, 0xe1, 0x06, 0x23, 0x20, 0x42, + 0xaa, 0x4d, 0x52, 0x21, 0x4e, 0x1c, 0x9a, 0x72, 0xaa, 0xb0, 0x84, 0x52, 0x09, 0x04, 0x07, 0x2a, + 0x3b, 0x5e, 0x1c, 0x0b, 0xdb, 0x6b, 0xbc, 0x9b, 0xa4, 0x79, 0x00, 0x24, 0x8e, 0x3c, 0x08, 0x0f, + 0xd2, 0x63, 0x8f, 0x9c, 0x10, 0x4a, 0x5e, 0x04, 0xad, 0x1d, 0x6f, 0xec, 0xe0, 0x34, 0x11, 0x28, + 0xb7, 0xdd, 0xf9, 0xbe, 0x99, 0x6f, 0x76, 0x66, 0x3c, 0x86, 0xfd, 0x9e, 0x8f, 0x90, 0xee, 0xa1, + 0x3e, 0x8a, 0x4c, 0x07, 0xe9, 0xfd, 0x86, 0x4e, 0x2f, 0xb5, 0x30, 0xc2, 0x14, 0x8b, 0x55, 0x06, + 0x69, 0x29, 0xa4, 0xf5, 0x1b, 0xb2, 0xd2, 0xc1, 0xc4, 0xc7, 0x44, 0xb7, 0x4c, 0xc2, 0xa8, 0x16, + 0xa2, 0x66, 0x43, 0xef, 0x60, 0x37, 0x48, 0x3c, 0xe4, 0x3d, 0x07, 0x3b, 0x38, 0x3e, 0xea, 0xec, + 0x94, 0x58, 0xd5, 0x8f, 0x50, 0x36, 0x88, 0x73, 0xde, 0x0b, 0x43, 0x6f, 0x28, 0xca, 0x70, 0x8b, + 0xb0, 0x93, 0x8b, 0x22, 0x49, 0xa8, 0x09, 0xf5, 0x72, 0x9b, 0xdf, 0xc5, 0xe7, 0xb0, 0x69, 0x12, + 0x82, 0xa8, 0xb4, 0x56, 0x13, 0xea, 0x95, 0xe6, 0xbe, 0x96, 0xc8, 0x69, 0x4c, 0x4e, 0x9b, 0xc8, + 0x69, 0xa7, 0xd8, 0x0d, 0x5a, 0x1b, 0x57, 0xbf, 0x0e, 0x4b, 0xed, 0x84, 0xad, 0x22, 0xa8, 0x1a, + 0xc4, 0x79, 0xe7, 0xd2, 0xae, 0x1d, 0x99, 0x83, 0x13, 0x66, 0x5b, 0x85, 0x4c, 0x27, 0x96, 0x39, + 0xb1, 0xed, 0x53, 0xec, 0x79, 0x26, 0x45, 0x91, 0xe9, 0x31, 0x19, 0x0b, 0x47, 0x11, 0x1e, 0x4c, + 0x65, 0xd2, 0xbb, 0x78, 0x0c, 0x1b, 0xac, 0x34, 0xcb, 0xaa, 0xc4, 0x64, 0xf5, 0x13, 0xec, 0x1a, + 0xc4, 0x69, 0x23, 0x1f, 0xf7, 0xd1, 0x2a, 0x75, 0x3a, 0x70, 0xdb, 0x20, 0x4e, 0x2b, 0x8e, 0xc1, + 0x2b, 0x36, 0x57, 0xe2, 0x1f, 0x2b, 0x66, 0xc1, 0x4e, 0xfc, 0x98, 0xd0, 0x1c, 0xae, 0x4c, 0xe3, + 0x87, 0x00, 0xdb, 0x06, 0x71, 0x5e, 0xbb, 0x5f, 0x7a, 0xae, 0x6d, 0x52, 0x24, 0x2a, 0x00, 0xde, + 0xe4, 0x82, 0x53, 0x95, 0x8c, 0x25, 0x97, 0xc3, 0xda, 0x4c, 0x0e, 0x2f, 0xa1, 0x1c, 0xb1, 0x6c, + 0x7d, 0x14, 0x50, 0x69, 0x7d, 0xb9, 0x3c, 0xa6, 0x1e, 0xe2, 0x03, 0xd8, 0x8e, 0xd0, 0xc0, 0x8c, + 0xec, 0x0b, 0x1b, 0x05, 0xd8, 0x97, 0x36, 0xe2, 0xf0, 0x95, 0xc4, 0xf6, 0x8a, 0x99, 0xd4, 0x5d, + 0xb8, 0xc3, 0xbf, 0x85, 0x36, 0x22, 0x21, 0x0e, 0x08, 0x52, 0x65, 0x90, 0x66, 0x07, 0x78, 0x06, + 0xcb, 0x4d, 0x1d, 0xc7, 0xee, 0xc3, 0x41, 0xc1, 0xb0, 0x70, 0x58, 0x82, 0x7b, 0xf9, 0x1e, 0x73, + 0xe4, 0x0d, 0xdc, 0xcd, 0x35, 0x26, 0x05, 0xc4, 0x17, 0xb0, 0xc5, 0x9e, 0xe3, 0xda, 0x71, 0xe1, + 0x96, 0x78, 0xfd, 0x84, 0xae, 0x7e, 0x13, 0x60, 0x2f, 0xdb, 0x86, 0xff, 0x8e, 0x98, 0x38, 0xb2, + 0xc2, 0x2d, 0x3b, 0x10, 0x13, 0x7a, 0xf3, 0xeb, 0x26, 0xac, 0x1b, 0xc4, 0x11, 0xcf, 0x60, 0x6b, + 0xb2, 0x73, 0x0e, 0xb4, 0xd9, 0x4d, 0xa6, 0xf1, 0x26, 0xc8, 0x0f, 0x6f, 0x00, 0xf9, 0x2b, 0x2e, + 0x60, 0x27, 0xbf, 0x5f, 0xd4, 0x42, 0xaf, 0x1c, 0x47, 0x7e, 0xba, 0x98, 0x93, 0x15, 0xc8, 0x6f, + 0x96, 0x62, 0x81, 0x1c, 0x67, 0x8e, 0x40, 0xe1, 0xac, 0x88, 0x5d, 0xa8, 0xfe, 0xb5, 0x55, 0x1e, + 0x15, 0xfa, 0xcf, 0xd2, 0xe4, 0xa3, 0xa5, 0x68, 0x5c, 0xe9, 0x3d, 0x54, 0xb2, 0x7b, 0xa5, 0x56, + 0xe8, 0x9d, 0x61, 0xc8, 0xf5, 0x45, 0x0c, 0x1e, 0xfa, 0x2d, 0x40, 0x66, 0x9b, 0x1c, 0xce, 0xc9, + 0x2b, 0x25, 0xc8, 0x4f, 0x16, 0x10, 0x78, 0xdc, 0x73, 0x28, 0x67, 0x16, 0x48, 0xa1, 0x17, 0xc7, + 0xe5, 0xc7, 0x37, 0xe3, 0x69, 0xd0, 0xd6, 0xd9, 0xd5, 0x48, 0x11, 0xae, 0x47, 0x8a, 0xf0, 0x7b, + 0xa4, 0x08, 0xdf, 0xc7, 0x4a, 0xe9, 0x7a, 0xac, 0x94, 0x7e, 0x8e, 0x95, 0xd2, 0x87, 0x67, 0x8e, + 0x4b, 0xbb, 0x3d, 0x4b, 0xeb, 0x60, 0x5f, 0x67, 0xb1, 0x8e, 0x02, 0x44, 0x07, 0x38, 0xfa, 0x1c, + 0x5f, 0xf4, 0x7e, 0x53, 0xbf, 0x9c, 0xfe, 0x90, 0xe9, 0x30, 0x44, 0xc4, 0xda, 0x8a, 0xff, 0xa4, + 0xc7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x0b, 0xe9, 0x1b, 0x46, 0xae, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. From 51f34ddbd8614ae6fb10192c257265ba516d1b8e Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:24:37 -0700 Subject: [PATCH 03/42] changelog++ --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 861dd57275..d0973f38e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1023](https://github.com/umee-network/umee/pull/1023) Restrict MsgWithdraw to only uToken input (no base token auto-convert) - [1106](https://github.com/umee-network/umee/pull/1106) Rename Lend to Supply, including MsgLendAsset, Token EnableLend, docs, and internal functions. Also QueryLoaned similar queries to QuerySupplied. - [1113](https://github.com/umee-network/umee/pull/1113) Rename Amount field to Asset when sdk.Coin type in Msg proto. +- [1118](https://github.com/umee-network/umee/pull/1118) MsgLiquidate now has reward denom instead of full coin ### Features @@ -62,6 +63,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1096](https://github.com/umee-network/umee/pull/1096) Add `max_collateral_share` to the x/leverage token registry. - [1094](https://github.com/umee-network/umee/pull/1094) Added TotalCollateral query. - [1099](https://github.com/umee-network/umee/pull/1099) Added TotalBorrowed query. +- [1118](https://github.com/umee-network/umee/pull/1118) MsgLiquidate rewards base assets instead of requiring an addtional MsgWithdraw ### Improvements From b15cc7e2903fb3e7b7f2bf6064843cd7011f3855 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:26:27 -0700 Subject: [PATCH 04/42] comment++ --- x/leverage/keeper/liquidate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 8cdccb9aa1..1df456920c 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -7,9 +7,9 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -// liquidationMaximum takes a repayment and reward proposed by a liquidator and calculates -// the maximum repayment amount a target address is eligible for, and the corresponding reward -// amount using current oracle prices and liquidation incentive. Inputs must be registered tokens. +// liquidationMaximum takes a repayment and reward denom proposed by a liquidator and calculates +// the maximum repayment amounts a target address is eligible for, and the corresponding reward +// amounts using current oracle, params, and available balances. Inputs must be registered tokens. // Outputs are base token repayment, uToken collateral cost, and base token reward from liquidation. func (k Keeper) liquidationMaximum( ctx sdk.Context, From e129f3c28bfe1b5485d08af9441611119a2ffd90 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:29:43 -0700 Subject: [PATCH 05/42] spec++ --- x/leverage/spec/04_messages.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/x/leverage/spec/04_messages.md b/x/leverage/spec/04_messages.md index 2a8a47709f..cb62e43535 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 From 7513e986ba1137967aecba72dc4b1160f1149ab8 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:31:36 -0700 Subject: [PATCH 06/42] whitespace-- --- x/leverage/client/tests/tests.go | 1 - 1 file changed, 1 deletion(-) diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index 04bf278699..6a924db859 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -463,7 +463,6 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { ), }, }, - testQuery{ "query borrowed - all", cli.GetCmdQueryBorrowed(), From 5c61a565e8387e6d1c82d6a2fd9ff275d50261c3 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:33:49 -0700 Subject: [PATCH 07/42] function rename --- x/leverage/keeper/keeper.go | 2 +- x/leverage/keeper/liquidate.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 4439bd7ba1..eea547b368 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -367,7 +367,7 @@ func (k Keeper) LiquidateBorrow( } // calculate Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances - baseRepay, collateralReward, baseReward, err := k.liquidationMaximum( + baseRepay, collateralReward, baseReward, err := k.liquidationOutcome( ctx, liquidatorAddr, borrowerAddr, diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 1df456920c..32d71fbcd0 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -7,11 +7,11 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -// liquidationMaximum takes a repayment and reward denom proposed by a liquidator and calculates +// liquidationOutcome takes a repayment and reward denom proposed by a liquidator and calculates // the maximum repayment amounts a target address is eligible for, and the corresponding reward // amounts using current oracle, params, and available balances. Inputs must be registered tokens. // Outputs are base token repayment, uToken collateral cost, and base token reward from liquidation. -func (k Keeper) liquidationMaximum( +func (k Keeper) liquidationOutcome( ctx sdk.Context, liquidatorAddr sdk.AccAddress, targetAddr sdk.AccAddress, From bcb555ff75e459661c850eba4640a91276a4813f Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:36:29 -0700 Subject: [PATCH 08/42] add MsgLiquidate response field for collateral consumed --- proto/umee/leverage/v1/tx.proto | 5 +- x/leverage/keeper/msg_server.go | 5 +- x/leverage/types/tx.pb.go | 136 ++++++++++++++++++++++---------- 3 files changed, 101 insertions(+), 45 deletions(-) diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index 2499a643ea..354d8c21e0 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -116,6 +116,7 @@ message MsgRepayAssetResponse { // 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]; + cosmos.base.v1beta1.Coin repaid = 1 [(gogoproto.nullable) = false]; + cosmos.base.v1beta1.Coin collateral = 2 [(gogoproto.nullable) = false]; + cosmos.base.v1beta1.Coin reward = 3 [(gogoproto.nullable) = false]; } diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 37dad7c1ef..57909925d7 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -298,7 +298,8 @@ func (s msgServer) Liquidate( }) return &types.MsgLiquidateResponse{ - Repaid: repaid, - Reward: reward, + Repaid: repaid, + Collateral: collateral, + Reward: reward, }, nil } diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index 7e593c0a0c..935dedce3c 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -663,8 +663,9 @@ func (m *MsgRepayAssetResponse) GetRepaid() types.Coin { // MsgLiquidateResponse defines the Msg/Liquidate response type. type MsgLiquidateResponse struct { - Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` - Reward types.Coin `protobuf:"bytes,2,opt,name=reward,proto3" json:"reward"` + Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` + Collateral types.Coin `protobuf:"bytes,2,opt,name=collateral,proto3" json:"collateral"` + Reward types.Coin `protobuf:"bytes,3,opt,name=reward,proto3" json:"reward"` } func (m *MsgLiquidateResponse) Reset() { *m = MsgLiquidateResponse{} } @@ -707,6 +708,13 @@ func (m *MsgLiquidateResponse) GetRepaid() types.Coin { return types.Coin{} } +func (m *MsgLiquidateResponse) GetCollateral() types.Coin { + if m != nil { + return m.Collateral + } + return types.Coin{} +} + func (m *MsgLiquidateResponse) GetReward() types.Coin { if m != nil { return m.Reward @@ -734,45 +742,46 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 607 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0x8e, 0xfb, 0x27, 0x32, 0x69, 0x51, 0x70, 0x0b, 0x72, 0x5d, 0xe1, 0x06, 0x23, 0x20, 0x42, - 0xaa, 0x4d, 0x52, 0x21, 0x4e, 0x1c, 0x9a, 0x72, 0xaa, 0xb0, 0x84, 0x52, 0x09, 0x04, 0x07, 0x2a, - 0x3b, 0x5e, 0x1c, 0x0b, 0xdb, 0x6b, 0xbc, 0x9b, 0xa4, 0x79, 0x00, 0x24, 0x8e, 0x3c, 0x08, 0x0f, - 0xd2, 0x63, 0x8f, 0x9c, 0x10, 0x4a, 0x5e, 0x04, 0xad, 0x1d, 0x6f, 0xec, 0xe0, 0x34, 0x11, 0x28, - 0xb7, 0xdd, 0xf9, 0xbe, 0x99, 0x6f, 0x76, 0x66, 0x3c, 0x86, 0xfd, 0x9e, 0x8f, 0x90, 0xee, 0xa1, - 0x3e, 0x8a, 0x4c, 0x07, 0xe9, 0xfd, 0x86, 0x4e, 0x2f, 0xb5, 0x30, 0xc2, 0x14, 0x8b, 0x55, 0x06, - 0x69, 0x29, 0xa4, 0xf5, 0x1b, 0xb2, 0xd2, 0xc1, 0xc4, 0xc7, 0x44, 0xb7, 0x4c, 0xc2, 0xa8, 0x16, - 0xa2, 0x66, 0x43, 0xef, 0x60, 0x37, 0x48, 0x3c, 0xe4, 0x3d, 0x07, 0x3b, 0x38, 0x3e, 0xea, 0xec, - 0x94, 0x58, 0xd5, 0x8f, 0x50, 0x36, 0x88, 0x73, 0xde, 0x0b, 0x43, 0x6f, 0x28, 0xca, 0x70, 0x8b, - 0xb0, 0x93, 0x8b, 0x22, 0x49, 0xa8, 0x09, 0xf5, 0x72, 0x9b, 0xdf, 0xc5, 0xe7, 0xb0, 0x69, 0x12, - 0x82, 0xa8, 0xb4, 0x56, 0x13, 0xea, 0x95, 0xe6, 0xbe, 0x96, 0xc8, 0x69, 0x4c, 0x4e, 0x9b, 0xc8, - 0x69, 0xa7, 0xd8, 0x0d, 0x5a, 0x1b, 0x57, 0xbf, 0x0e, 0x4b, 0xed, 0x84, 0xad, 0x22, 0xa8, 0x1a, - 0xc4, 0x79, 0xe7, 0xd2, 0xae, 0x1d, 0x99, 0x83, 0x13, 0x66, 0x5b, 0x85, 0x4c, 0x27, 0x96, 0x39, - 0xb1, 0xed, 0x53, 0xec, 0x79, 0x26, 0x45, 0x91, 0xe9, 0x31, 0x19, 0x0b, 0x47, 0x11, 0x1e, 0x4c, - 0x65, 0xd2, 0xbb, 0x78, 0x0c, 0x1b, 0xac, 0x34, 0xcb, 0xaa, 0xc4, 0x64, 0xf5, 0x13, 0xec, 0x1a, - 0xc4, 0x69, 0x23, 0x1f, 0xf7, 0xd1, 0x2a, 0x75, 0x3a, 0x70, 0xdb, 0x20, 0x4e, 0x2b, 0x8e, 0xc1, - 0x2b, 0x36, 0x57, 0xe2, 0x1f, 0x2b, 0x66, 0xc1, 0x4e, 0xfc, 0x98, 0xd0, 0x1c, 0xae, 0x4c, 0xe3, - 0x87, 0x00, 0xdb, 0x06, 0x71, 0x5e, 0xbb, 0x5f, 0x7a, 0xae, 0x6d, 0x52, 0x24, 0x2a, 0x00, 0xde, - 0xe4, 0x82, 0x53, 0x95, 0x8c, 0x25, 0x97, 0xc3, 0xda, 0x4c, 0x0e, 0x2f, 0xa1, 0x1c, 0xb1, 0x6c, - 0x7d, 0x14, 0x50, 0x69, 0x7d, 0xb9, 0x3c, 0xa6, 0x1e, 0xe2, 0x03, 0xd8, 0x8e, 0xd0, 0xc0, 0x8c, - 0xec, 0x0b, 0x1b, 0x05, 0xd8, 0x97, 0x36, 0xe2, 0xf0, 0x95, 0xc4, 0xf6, 0x8a, 0x99, 0xd4, 0x5d, - 0xb8, 0xc3, 0xbf, 0x85, 0x36, 0x22, 0x21, 0x0e, 0x08, 0x52, 0x65, 0x90, 0x66, 0x07, 0x78, 0x06, - 0xcb, 0x4d, 0x1d, 0xc7, 0xee, 0xc3, 0x41, 0xc1, 0xb0, 0x70, 0x58, 0x82, 0x7b, 0xf9, 0x1e, 0x73, - 0xe4, 0x0d, 0xdc, 0xcd, 0x35, 0x26, 0x05, 0xc4, 0x17, 0xb0, 0xc5, 0x9e, 0xe3, 0xda, 0x71, 0xe1, - 0x96, 0x78, 0xfd, 0x84, 0xae, 0x7e, 0x13, 0x60, 0x2f, 0xdb, 0x86, 0xff, 0x8e, 0x98, 0x38, 0xb2, - 0xc2, 0x2d, 0x3b, 0x10, 0x13, 0x7a, 0xf3, 0xeb, 0x26, 0xac, 0x1b, 0xc4, 0x11, 0xcf, 0x60, 0x6b, - 0xb2, 0x73, 0x0e, 0xb4, 0xd9, 0x4d, 0xa6, 0xf1, 0x26, 0xc8, 0x0f, 0x6f, 0x00, 0xf9, 0x2b, 0x2e, - 0x60, 0x27, 0xbf, 0x5f, 0xd4, 0x42, 0xaf, 0x1c, 0x47, 0x7e, 0xba, 0x98, 0x93, 0x15, 0xc8, 0x6f, - 0x96, 0x62, 0x81, 0x1c, 0x67, 0x8e, 0x40, 0xe1, 0xac, 0x88, 0x5d, 0xa8, 0xfe, 0xb5, 0x55, 0x1e, - 0x15, 0xfa, 0xcf, 0xd2, 0xe4, 0xa3, 0xa5, 0x68, 0x5c, 0xe9, 0x3d, 0x54, 0xb2, 0x7b, 0xa5, 0x56, - 0xe8, 0x9d, 0x61, 0xc8, 0xf5, 0x45, 0x0c, 0x1e, 0xfa, 0x2d, 0x40, 0x66, 0x9b, 0x1c, 0xce, 0xc9, - 0x2b, 0x25, 0xc8, 0x4f, 0x16, 0x10, 0x78, 0xdc, 0x73, 0x28, 0x67, 0x16, 0x48, 0xa1, 0x17, 0xc7, - 0xe5, 0xc7, 0x37, 0xe3, 0x69, 0xd0, 0xd6, 0xd9, 0xd5, 0x48, 0x11, 0xae, 0x47, 0x8a, 0xf0, 0x7b, - 0xa4, 0x08, 0xdf, 0xc7, 0x4a, 0xe9, 0x7a, 0xac, 0x94, 0x7e, 0x8e, 0x95, 0xd2, 0x87, 0x67, 0x8e, - 0x4b, 0xbb, 0x3d, 0x4b, 0xeb, 0x60, 0x5f, 0x67, 0xb1, 0x8e, 0x02, 0x44, 0x07, 0x38, 0xfa, 0x1c, - 0x5f, 0xf4, 0x7e, 0x53, 0xbf, 0x9c, 0xfe, 0x90, 0xe9, 0x30, 0x44, 0xc4, 0xda, 0x8a, 0xff, 0xa4, - 0xc7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x0b, 0xe9, 0x1b, 0x46, 0xae, 0x07, 0x00, 0x00, + // 618 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcf, 0x6e, 0xd3, 0x4e, + 0x10, 0x8e, 0xfb, 0x4f, 0xbf, 0x4c, 0xda, 0x9f, 0x82, 0x5b, 0x90, 0xeb, 0x0a, 0x37, 0x18, 0x01, + 0x11, 0x52, 0x6d, 0x92, 0x0a, 0x71, 0x42, 0xa8, 0x29, 0xa7, 0x0a, 0x4b, 0x28, 0x95, 0x40, 0x70, + 0xa0, 0xf2, 0x9f, 0xc5, 0xb1, 0xb0, 0xbd, 0xc6, 0xeb, 0x24, 0xcd, 0x03, 0x70, 0xe7, 0x41, 0x78, + 0x90, 0x72, 0xeb, 0x91, 0x13, 0x42, 0xc9, 0x8b, 0xa0, 0xb5, 0xe3, 0x8d, 0x1d, 0x9c, 0xc6, 0x02, + 0xe5, 0xb6, 0x3b, 0xdf, 0x37, 0xf3, 0xcd, 0xce, 0x8c, 0xc7, 0xb0, 0xdf, 0xf7, 0x10, 0x52, 0x5d, + 0x34, 0x40, 0xa1, 0x6e, 0x23, 0x75, 0xd0, 0x52, 0xa3, 0x4b, 0x25, 0x08, 0x71, 0x84, 0xf9, 0x3a, + 0x85, 0x94, 0x14, 0x52, 0x06, 0x2d, 0x51, 0x32, 0x31, 0xf1, 0x30, 0x51, 0x0d, 0x9d, 0x50, 0xaa, + 0x81, 0x22, 0xbd, 0xa5, 0x9a, 0xd8, 0xf1, 0x13, 0x0f, 0x71, 0xcf, 0xc6, 0x36, 0x8e, 0x8f, 0x2a, + 0x3d, 0x25, 0x56, 0xf9, 0x03, 0x54, 0x35, 0x62, 0x9f, 0xf7, 0x83, 0xc0, 0x1d, 0xf1, 0x22, 0xfc, + 0x47, 0xe8, 0xc9, 0x41, 0xa1, 0xc0, 0x35, 0xb8, 0x66, 0xb5, 0xcb, 0xee, 0xfc, 0x53, 0xd8, 0xd4, + 0x09, 0x41, 0x91, 0xb0, 0xd6, 0xe0, 0x9a, 0xb5, 0xf6, 0xbe, 0x92, 0xc8, 0x29, 0x54, 0x4e, 0x99, + 0xca, 0x29, 0xa7, 0xd8, 0xf1, 0x3b, 0x1b, 0x57, 0x3f, 0x0f, 0x2b, 0xdd, 0x84, 0x2d, 0x23, 0xa8, + 0x6b, 0xc4, 0x7e, 0xeb, 0x44, 0x3d, 0x2b, 0xd4, 0x87, 0x27, 0xd4, 0xb6, 0x0a, 0x19, 0x33, 0x96, + 0x39, 0xb1, 0xac, 0x53, 0xec, 0xba, 0x7a, 0x84, 0x42, 0xdd, 0xa5, 0x32, 0x06, 0x0e, 0x43, 0x3c, + 0x9c, 0xc9, 0xa4, 0x77, 0xfe, 0x18, 0x36, 0x68, 0x69, 0xca, 0xaa, 0xc4, 0x64, 0xf9, 0x23, 0xec, + 0x6a, 0xc4, 0xee, 0x22, 0x0f, 0x0f, 0xd0, 0x2a, 0x75, 0x4c, 0xf8, 0x5f, 0x23, 0x76, 0x27, 0x8e, + 0xc1, 0x2a, 0xb6, 0x50, 0xe2, 0x2f, 0x2b, 0x66, 0xc0, 0x4e, 0xfc, 0x98, 0x40, 0x1f, 0xad, 0x4c, + 0xe3, 0x1b, 0x07, 0xdb, 0x1a, 0xb1, 0x5f, 0x39, 0x9f, 0xfb, 0x8e, 0xa5, 0x47, 0x88, 0x97, 0x00, + 0xdc, 0xe9, 0x05, 0xa7, 0x2a, 0x19, 0x4b, 0x2e, 0x87, 0xb5, 0xb9, 0x1c, 0x9e, 0x43, 0x35, 0xa4, + 0xd9, 0x7a, 0xc8, 0x8f, 0x84, 0xf5, 0x72, 0x79, 0xcc, 0x3c, 0xf8, 0x7b, 0xb0, 0x1d, 0xa2, 0xa1, + 0x1e, 0x5a, 0x17, 0x16, 0xf2, 0xb1, 0x27, 0x6c, 0xc4, 0xe1, 0x6b, 0x89, 0xed, 0x25, 0x35, 0xc9, + 0xbb, 0x70, 0x8b, 0x7d, 0x0b, 0x5d, 0x44, 0x02, 0xec, 0x13, 0x24, 0x8b, 0x20, 0xcc, 0x0f, 0xf0, + 0x1c, 0x96, 0x9b, 0x3a, 0x86, 0xdd, 0x85, 0x83, 0x82, 0x61, 0x61, 0xb0, 0x00, 0x77, 0xf2, 0x3d, + 0x66, 0xc8, 0x6b, 0xb8, 0x9d, 0x6b, 0x4c, 0x0a, 0xf0, 0xcf, 0x60, 0x8b, 0x3e, 0xc7, 0xb1, 0xe2, + 0xc2, 0x95, 0x78, 0xfd, 0x94, 0x2e, 0x7f, 0xe7, 0x60, 0x2f, 0xdb, 0x86, 0x7f, 0x8e, 0xc8, 0xbf, + 0x00, 0x30, 0xd9, 0x9b, 0xca, 0x0e, 0x45, 0xc6, 0x25, 0x51, 0xa6, 0x95, 0x2f, 0xdb, 0xc9, 0x29, + 0xbd, 0xfd, 0x65, 0x13, 0xd6, 0x35, 0x62, 0xf3, 0x67, 0xb0, 0x35, 0x5d, 0x5a, 0x07, 0xca, 0xfc, + 0x2a, 0x54, 0x58, 0x17, 0xc5, 0xfb, 0x37, 0x80, 0xac, 0x0c, 0x17, 0xb0, 0x93, 0x5f, 0x50, 0x72, + 0xa1, 0x57, 0x8e, 0x23, 0x3e, 0x5e, 0xce, 0xc9, 0x0a, 0xe4, 0x57, 0x53, 0xb1, 0x40, 0x8e, 0xb3, + 0x40, 0xa0, 0x70, 0xd8, 0xf8, 0x1e, 0xd4, 0xff, 0x58, 0x4b, 0x0f, 0x0a, 0xfd, 0xe7, 0x69, 0xe2, + 0x51, 0x29, 0x1a, 0x53, 0x7a, 0x07, 0xb5, 0xec, 0x62, 0x6a, 0x14, 0x7a, 0x67, 0x18, 0x62, 0x73, + 0x19, 0x83, 0x85, 0x7e, 0x03, 0x90, 0x59, 0x47, 0x87, 0x0b, 0xf2, 0x4a, 0x09, 0xe2, 0xa3, 0x25, + 0x04, 0x16, 0xf7, 0x1c, 0xaa, 0x99, 0x0d, 0x54, 0xe8, 0xc5, 0x70, 0xf1, 0xe1, 0xcd, 0x78, 0x1a, + 0xb4, 0x73, 0x76, 0x35, 0x96, 0xb8, 0xeb, 0xb1, 0xc4, 0xfd, 0x1a, 0x4b, 0xdc, 0xd7, 0x89, 0x54, + 0xb9, 0x9e, 0x48, 0x95, 0x1f, 0x13, 0xa9, 0xf2, 0xfe, 0x89, 0xed, 0x44, 0xbd, 0xbe, 0xa1, 0x98, + 0xd8, 0x53, 0x69, 0xac, 0x23, 0x1f, 0x45, 0x43, 0x1c, 0x7e, 0x8a, 0x2f, 0xea, 0xa0, 0xad, 0x5e, + 0xce, 0xfe, 0xe8, 0xd1, 0x28, 0x40, 0xc4, 0xd8, 0x8a, 0x7f, 0xc5, 0xc7, 0xbf, 0x03, 0x00, 0x00, + 0xff, 0xff, 0x01, 0x3f, 0x43, 0x30, 0xef, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -1566,6 +1575,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]) @@ -1768,6 +1787,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 @@ -3044,6 +3065,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) } From 1ef4e90f158d558a69939770f7aed9f2b720b069 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 7 Jul 2022 14:41:30 -0700 Subject: [PATCH 09/42] line length-- --- x/leverage/keeper/msg_server.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 57909925d7..62fe13c5f9 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -255,25 +255,25 @@ 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, collateral, reward, err := s.keeper.LiquidateBorrow(ctx, liquidatorAddr, borrowerAddr, msg.Repayment, msg.RewardDenom) + repaid, collateral, reward, err := s.keeper.LiquidateBorrow(ctx, liquidator, borrower, msg.Repayment, msg.RewardDenom) if err != nil { return nil, err } s.keeper.Logger(ctx).Debug( "borrowed assets repaid by liquidator", - "liquidator", liquidatorAddr.String(), - "borrower", borrowerAddr.String(), + "liquidator", liquidator.String(), + "borrower", borrower.String(), "attempted", msg.Repayment.String(), "repaid", repaid.String(), "collateral", collateral.String(), @@ -283,8 +283,8 @@ func (s msgServer) Liquidate( ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeLiquidate, - sdk.NewAttribute(types.EventAttrLiquidator, liquidatorAddr.String()), - sdk.NewAttribute(types.EventAttrBorrower, borrowerAddr.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()), @@ -293,7 +293,7 @@ func (s msgServer) Liquidate( sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.EventAttrModule), - sdk.NewAttribute(sdk.AttributeKeySender, liquidatorAddr.String()), + sdk.NewAttribute(sdk.AttributeKeySender, liquidator.String()), ), }) From e0aa350da3e5881c89751966bb0f1d372bc61d3b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:24:38 -0700 Subject: [PATCH 10/42] update comment and rename return values --- x/leverage/keeper/keeper.go | 23 ++++++++++------------- x/leverage/keeper/keeper_test.go | 6 +++--- x/leverage/keeper/msg_server.go | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index eea547b368..563f5cd482 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -346,19 +346,16 @@ func (k Keeper) RemoveCollateral(ctx sdk.Context, borrowerAddr sdk.AccAddress, c 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( +// 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 (in that order). +func (k Keeper) Liquidate( ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, desiredRepay sdk.Coin, rewardDenom string, -) (sdk.Coin, sdk.Coin, sdk.Coin, error) { +) (baseRepay sdk.Coin, collateralReward sdk.Coin, baseReward sdk.Coin, err error) { if err := k.validateAcceptedAsset(ctx, desiredRepay); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } @@ -367,7 +364,7 @@ func (k Keeper) LiquidateBorrow( } // calculate Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances - baseRepay, collateralReward, baseReward, err := k.liquidationOutcome( + baseRepay, collateralReward, baseReward, err = k.liquidationOutcome( ctx, liquidatorAddr, borrowerAddr, diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index d93973b3b9..d6e833ae47 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -584,7 +584,7 @@ func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { // 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) - _, _, _, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) + _, _, _, err = s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) s.Require().Error(err) // Note: Setting umee collateral weight to 0.0 to allow liquidation @@ -596,7 +596,7 @@ func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { // liquidator partially liquidates user, receiving some collateral repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 10000000) // 10 umee - repaid, collateral, reward, err := s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) + repaid, collateral, reward, err := s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) s.Require().NoError(err) s.Require().Equal(repayment, repaid) s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 11000000), collateral) @@ -616,7 +616,7 @@ func (s *IntegrationTestSuite) TestLiqudateBorrow_Valid() { // liquidator fully liquidates user, receiving more collateral and reducing borrowed amount to zero repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 300000000) // 300 umee - repaid, collateral, reward, err = s.app.LeverageKeeper.LiquidateBorrow(ctx, liquidatorAddr, addr, repayment, umeeDenom) + repaid, collateral, reward, err = s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeDenom) s.Require().NoError(err) s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 80000000), repaid) s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 88000000), collateral) diff --git a/x/leverage/keeper/msg_server.go b/x/leverage/keeper/msg_server.go index 62fe13c5f9..25d9a55946 100644 --- a/x/leverage/keeper/msg_server.go +++ b/x/leverage/keeper/msg_server.go @@ -265,7 +265,7 @@ func (s msgServer) Liquidate( return nil, err } - repaid, collateral, reward, err := s.keeper.LiquidateBorrow(ctx, liquidator, borrower, msg.Repayment, msg.RewardDenom) + repaid, collateral, reward, err := s.keeper.Liquidate(ctx, liquidator, borrower, msg.Repayment, msg.RewardDenom) if err != nil { return nil, err } From f498ef485a387c0eef3161a346bb462c46f0d6ab Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:30:42 -0700 Subject: [PATCH 11/42] make proto-gen --- x/leverage/types/tx.pb.go | 125 +++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 57 deletions(-) diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index a2f000f270..3e26d85fe8 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -742,46 +742,46 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 618 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcf, 0x6e, 0xd3, 0x4e, - 0x10, 0x8e, 0xfb, 0x4f, 0xbf, 0x4c, 0xda, 0x9f, 0x82, 0x5b, 0x90, 0xeb, 0x0a, 0x37, 0x18, 0x01, - 0x11, 0x52, 0x6d, 0x92, 0x0a, 0x71, 0x42, 0xa8, 0x29, 0xa7, 0x0a, 0x4b, 0x28, 0x95, 0x40, 0x70, - 0xa0, 0xf2, 0x9f, 0xc5, 0xb1, 0xb0, 0xbd, 0xc6, 0xeb, 0x24, 0xcd, 0x03, 0x70, 0xe7, 0x41, 0x78, - 0x90, 0x72, 0xeb, 0x91, 0x13, 0x42, 0xc9, 0x8b, 0xa0, 0xb5, 0xe3, 0x8d, 0x1d, 0x9c, 0xc6, 0x02, - 0xe5, 0xb6, 0x3b, 0xdf, 0x37, 0xf3, 0xcd, 0xce, 0x8c, 0xc7, 0xb0, 0xdf, 0xf7, 0x10, 0x52, 0x5d, - 0x34, 0x40, 0xa1, 0x6e, 0x23, 0x75, 0xd0, 0x52, 0xa3, 0x4b, 0x25, 0x08, 0x71, 0x84, 0xf9, 0x3a, - 0x85, 0x94, 0x14, 0x52, 0x06, 0x2d, 0x51, 0x32, 0x31, 0xf1, 0x30, 0x51, 0x0d, 0x9d, 0x50, 0xaa, - 0x81, 0x22, 0xbd, 0xa5, 0x9a, 0xd8, 0xf1, 0x13, 0x0f, 0x71, 0xcf, 0xc6, 0x36, 0x8e, 0x8f, 0x2a, - 0x3d, 0x25, 0x56, 0xf9, 0x03, 0x54, 0x35, 0x62, 0x9f, 0xf7, 0x83, 0xc0, 0x1d, 0xf1, 0x22, 0xfc, - 0x47, 0xe8, 0xc9, 0x41, 0xa1, 0xc0, 0x35, 0xb8, 0x66, 0xb5, 0xcb, 0xee, 0xfc, 0x53, 0xd8, 0xd4, - 0x09, 0x41, 0x91, 0xb0, 0xd6, 0xe0, 0x9a, 0xb5, 0xf6, 0xbe, 0x92, 0xc8, 0x29, 0x54, 0x4e, 0x99, - 0xca, 0x29, 0xa7, 0xd8, 0xf1, 0x3b, 0x1b, 0x57, 0x3f, 0x0f, 0x2b, 0xdd, 0x84, 0x2d, 0x23, 0xa8, - 0x6b, 0xc4, 0x7e, 0xeb, 0x44, 0x3d, 0x2b, 0xd4, 0x87, 0x27, 0xd4, 0xb6, 0x0a, 0x19, 0x33, 0x96, - 0x39, 0xb1, 0xac, 0x53, 0xec, 0xba, 0x7a, 0x84, 0x42, 0xdd, 0xa5, 0x32, 0x06, 0x0e, 0x43, 0x3c, - 0x9c, 0xc9, 0xa4, 0x77, 0xfe, 0x18, 0x36, 0x68, 0x69, 0xca, 0xaa, 0xc4, 0x64, 0xf9, 0x23, 0xec, - 0x6a, 0xc4, 0xee, 0x22, 0x0f, 0x0f, 0xd0, 0x2a, 0x75, 0x4c, 0xf8, 0x5f, 0x23, 0x76, 0x27, 0x8e, - 0xc1, 0x2a, 0xb6, 0x50, 0xe2, 0x2f, 0x2b, 0x66, 0xc0, 0x4e, 0xfc, 0x98, 0x40, 0x1f, 0xad, 0x4c, - 0xe3, 0x1b, 0x07, 0xdb, 0x1a, 0xb1, 0x5f, 0x39, 0x9f, 0xfb, 0x8e, 0xa5, 0x47, 0x88, 0x97, 0x00, - 0xdc, 0xe9, 0x05, 0xa7, 0x2a, 0x19, 0x4b, 0x2e, 0x87, 0xb5, 0xb9, 0x1c, 0x9e, 0x43, 0x35, 0xa4, - 0xd9, 0x7a, 0xc8, 0x8f, 0x84, 0xf5, 0x72, 0x79, 0xcc, 0x3c, 0xf8, 0x7b, 0xb0, 0x1d, 0xa2, 0xa1, - 0x1e, 0x5a, 0x17, 0x16, 0xf2, 0xb1, 0x27, 0x6c, 0xc4, 0xe1, 0x6b, 0x89, 0xed, 0x25, 0x35, 0xc9, - 0xbb, 0x70, 0x8b, 0x7d, 0x0b, 0x5d, 0x44, 0x02, 0xec, 0x13, 0x24, 0x8b, 0x20, 0xcc, 0x0f, 0xf0, - 0x1c, 0x96, 0x9b, 0x3a, 0x86, 0xdd, 0x85, 0x83, 0x82, 0x61, 0x61, 0xb0, 0x00, 0x77, 0xf2, 0x3d, - 0x66, 0xc8, 0x6b, 0xb8, 0x9d, 0x6b, 0x4c, 0x0a, 0xf0, 0xcf, 0x60, 0x8b, 0x3e, 0xc7, 0xb1, 0xe2, - 0xc2, 0x95, 0x78, 0xfd, 0x94, 0x2e, 0x7f, 0xe7, 0x60, 0x2f, 0xdb, 0x86, 0x7f, 0x8e, 0xc8, 0xbf, - 0x00, 0x30, 0xd9, 0x9b, 0xca, 0x0e, 0x45, 0xc6, 0x25, 0x51, 0xa6, 0x95, 0x2f, 0xdb, 0xc9, 0x29, - 0xbd, 0xfd, 0x65, 0x13, 0xd6, 0x35, 0x62, 0xf3, 0x67, 0xb0, 0x35, 0x5d, 0x5a, 0x07, 0xca, 0xfc, - 0x2a, 0x54, 0x58, 0x17, 0xc5, 0xfb, 0x37, 0x80, 0xac, 0x0c, 0x17, 0xb0, 0x93, 0x5f, 0x50, 0x72, - 0xa1, 0x57, 0x8e, 0x23, 0x3e, 0x5e, 0xce, 0xc9, 0x0a, 0xe4, 0x57, 0x53, 0xb1, 0x40, 0x8e, 0xb3, - 0x40, 0xa0, 0x70, 0xd8, 0xf8, 0x1e, 0xd4, 0xff, 0x58, 0x4b, 0x0f, 0x0a, 0xfd, 0xe7, 0x69, 0xe2, - 0x51, 0x29, 0x1a, 0x53, 0x7a, 0x07, 0xb5, 0xec, 0x62, 0x6a, 0x14, 0x7a, 0x67, 0x18, 0x62, 0x73, - 0x19, 0x83, 0x85, 0x7e, 0x03, 0x90, 0x59, 0x47, 0x87, 0x0b, 0xf2, 0x4a, 0x09, 0xe2, 0xa3, 0x25, - 0x04, 0x16, 0xf7, 0x1c, 0xaa, 0x99, 0x0d, 0x54, 0xe8, 0xc5, 0x70, 0xf1, 0xe1, 0xcd, 0x78, 0x1a, - 0xb4, 0x73, 0x76, 0x35, 0x96, 0xb8, 0xeb, 0xb1, 0xc4, 0xfd, 0x1a, 0x4b, 0xdc, 0xd7, 0x89, 0x54, - 0xb9, 0x9e, 0x48, 0x95, 0x1f, 0x13, 0xa9, 0xf2, 0xfe, 0x89, 0xed, 0x44, 0xbd, 0xbe, 0xa1, 0x98, - 0xd8, 0x53, 0x69, 0xac, 0x23, 0x1f, 0x45, 0x43, 0x1c, 0x7e, 0x8a, 0x2f, 0xea, 0xa0, 0xad, 0x5e, - 0xce, 0xfe, 0xe8, 0xd1, 0x28, 0x40, 0xc4, 0xd8, 0x8a, 0x7f, 0xc5, 0xc7, 0xbf, 0x03, 0x00, 0x00, - 0xff, 0xff, 0x01, 0x3f, 0x43, 0x30, 0xef, 0x07, 0x00, 0x00, + // 614 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdd, 0x6e, 0xd3, 0x4c, + 0x10, 0x8d, 0xdb, 0x34, 0x6a, 0xa6, 0xfd, 0xf4, 0x15, 0xb7, 0x48, 0xa9, 0x01, 0x53, 0xcc, 0x8f, + 0x2a, 0x04, 0x36, 0x49, 0x85, 0xb8, 0x42, 0x48, 0x69, 0x25, 0xa4, 0x80, 0x25, 0x94, 0x5e, 0x20, + 0x21, 0x41, 0x71, 0x92, 0x91, 0x6b, 0xe1, 0x78, 0xcd, 0xee, 0x26, 0x69, 0x78, 0x0a, 0x1e, 0x84, + 0x07, 0x29, 0x77, 0xbd, 0x41, 0xe2, 0x0a, 0xa1, 0xe4, 0x45, 0xd0, 0xfa, 0x67, 0xf3, 0x83, 0x1b, + 0x2c, 0x50, 0xee, 0x76, 0xe7, 0x9c, 0x39, 0x67, 0x3d, 0x33, 0x1a, 0xc3, 0x6e, 0xaf, 0x8b, 0x68, + 0xf9, 0xd8, 0x47, 0xea, 0xb8, 0x68, 0xf5, 0xab, 0x16, 0x3f, 0x33, 0x43, 0x4a, 0x38, 0x51, 0xb7, + 0x04, 0x64, 0xa6, 0x90, 0xd9, 0xaf, 0x6a, 0x7a, 0x9b, 0xb0, 0x2e, 0x61, 0x56, 0xcb, 0x61, 0x82, + 0xda, 0x42, 0xee, 0x54, 0xad, 0x36, 0xf1, 0x82, 0x38, 0x43, 0xdb, 0x71, 0x89, 0x4b, 0xa2, 0xa3, + 0x25, 0x4e, 0x71, 0xd4, 0x78, 0x07, 0x65, 0x9b, 0xb9, 0xc7, 0xbd, 0x30, 0xf4, 0x87, 0xaa, 0x06, + 0xeb, 0x4c, 0x9c, 0x3c, 0xa4, 0x15, 0x65, 0x4f, 0xd9, 0x2f, 0x37, 0xe5, 0x5d, 0x7d, 0x0c, 0x6b, + 0x0e, 0x63, 0xc8, 0x2b, 0x2b, 0x7b, 0xca, 0xfe, 0x46, 0x6d, 0xd7, 0x8c, 0xed, 0x4c, 0x61, 0x67, + 0x26, 0x76, 0xe6, 0x21, 0xf1, 0x82, 0x7a, 0xf1, 0xfc, 0xc7, 0xcd, 0x42, 0x33, 0x66, 0x1b, 0xef, + 0x61, 0xc3, 0x66, 0xee, 0x6b, 0x8f, 0x9f, 0x76, 0xa8, 0x33, 0x58, 0x86, 0x43, 0x1b, 0xb6, 0x6c, + 0xe6, 0x1e, 0x12, 0xdf, 0x77, 0x38, 0x52, 0xc7, 0xf7, 0x3e, 0xa1, 0xb0, 0x69, 0x11, 0x4a, 0xc9, + 0x60, 0x62, 0x93, 0xde, 0xd5, 0x03, 0x28, 0x8a, 0xaa, 0xe4, 0x75, 0x89, 0xc8, 0x06, 0x82, 0x6a, + 0x33, 0xf7, 0x08, 0xdb, 0xcb, 0xb5, 0x89, 0xbb, 0x51, 0x8f, 0x34, 0x16, 0xaa, 0xff, 0x65, 0xad, + 0xde, 0xc2, 0xba, 0xcd, 0xdc, 0x26, 0x86, 0xce, 0x70, 0x19, 0xf2, 0x5f, 0x14, 0xd8, 0xb4, 0x99, + 0xfb, 0xd2, 0xfb, 0xd8, 0xf3, 0x3a, 0x0e, 0x47, 0x55, 0x07, 0xf0, 0x93, 0x0b, 0x49, 0x5d, 0xa6, + 0x22, 0x33, 0x6f, 0x58, 0x99, 0x7b, 0xc3, 0x53, 0x28, 0x53, 0xf1, 0xd0, 0x2e, 0x06, 0xbc, 0xb2, + 0x9a, 0xef, 0x1d, 0x93, 0x0c, 0xf5, 0x16, 0x6c, 0x52, 0x1c, 0x38, 0xb4, 0x73, 0xd2, 0xc1, 0x80, + 0x74, 0x2b, 0xc5, 0x48, 0x7e, 0x23, 0x8e, 0x1d, 0x89, 0x90, 0xb1, 0x0d, 0x57, 0xe4, 0xec, 0x37, + 0x91, 0x85, 0x24, 0x60, 0x68, 0x5c, 0x85, 0xed, 0xa9, 0x81, 0x95, 0x61, 0x0d, 0x2a, 0xf3, 0x53, + 0x26, 0xb1, 0xeb, 0xa0, 0xfd, 0x3e, 0x1c, 0x12, 0x8d, 0x5d, 0xe2, 0x9e, 0xca, 0xe0, 0x8b, 0x68, + 0x68, 0xa3, 0x46, 0xa4, 0x31, 0xf5, 0x09, 0x94, 0xc4, 0xf3, 0xbd, 0x4e, 0x54, 0xa8, 0x1c, 0x5f, + 0x9b, 0xd0, 0x8d, 0xaf, 0x0a, 0xec, 0x4c, 0x97, 0xfd, 0x9f, 0x15, 0xd5, 0x67, 0x00, 0x93, 0x8f, + 0xc9, 0x3b, 0x04, 0x53, 0x29, 0xb1, 0xb3, 0xa8, 0x74, 0xde, 0xce, 0x25, 0xf4, 0xda, 0xb7, 0x22, + 0xac, 0xda, 0xcc, 0x55, 0x1b, 0x50, 0x4a, 0x96, 0xd2, 0x35, 0x73, 0x7e, 0xd5, 0x99, 0xb2, 0x6b, + 0xda, 0xed, 0x05, 0xa0, 0x2c, 0xc3, 0x2b, 0x58, 0x97, 0x0b, 0xe8, 0x46, 0x66, 0x42, 0x0a, 0x6b, + 0x77, 0x17, 0xc2, 0x52, 0xf1, 0x04, 0xfe, 0x9b, 0x5d, 0x38, 0x46, 0x66, 0xde, 0x0c, 0x47, 0xbb, + 0xff, 0x67, 0x8e, 0x34, 0x40, 0xf8, 0x7f, 0x7e, 0xd9, 0xdc, 0xc9, 0x4c, 0x9f, 0x63, 0x69, 0x0f, + 0xf2, 0xb0, 0xa4, 0x4d, 0x03, 0x4a, 0xc9, 0xb2, 0xc9, 0xae, 0x72, 0x0c, 0x5e, 0x52, 0xe5, 0xd9, + 0x91, 0x56, 0x9f, 0xc3, 0x5a, 0xb2, 0x58, 0x32, 0xd9, 0x11, 0xa6, 0x19, 0x97, 0x63, 0x52, 0xe8, + 0x18, 0xca, 0x53, 0x1b, 0x24, 0x33, 0x41, 0xe2, 0xda, 0xbd, 0xc5, 0x78, 0x2a, 0x5a, 0x6f, 0x9c, + 0x8f, 0x74, 0xe5, 0x62, 0xa4, 0x2b, 0x3f, 0x47, 0xba, 0xf2, 0x79, 0xac, 0x17, 0x2e, 0xc6, 0x7a, + 0xe1, 0xfb, 0x58, 0x2f, 0xbc, 0x79, 0xe4, 0x7a, 0xfc, 0xb4, 0xd7, 0x32, 0xdb, 0xa4, 0x6b, 0x09, + 0xad, 0x87, 0x01, 0xf2, 0x01, 0xa1, 0x1f, 0xa2, 0x8b, 0xd5, 0xaf, 0x59, 0x67, 0x93, 0x3f, 0x30, + 0x1f, 0x86, 0xc8, 0x5a, 0xa5, 0xe8, 0xd7, 0x79, 0xf0, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xe1, + 0xe1, 0x82, 0x9f, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -799,6 +799,20 @@ type MsgClient interface { // Supply moves tokens from user balance to the module for lending or collateral. // The user receives uTokens in return. Supply(ctx context.Context, in *MsgSupply, opts ...grpc.CallOption) (*MsgSupplyResponse, error) + // Withdraw moves previously supplied tokens from the module back to the user balance in + // exchange for burning uTokens. + Withdraw(ctx context.Context, in *MsgWithdraw, opts ...grpc.CallOption) (*MsgWithdrawResponse, error) + // Collateralize enables selected uTokens as collateral, which moves them to the module. + Collateralize(ctx context.Context, in *MsgCollateralize, opts ...grpc.CallOption) (*MsgCollateralizeResponse, error) + // Decollateralize disables selected uTokens as collateral. They are returned to the user's + // balance from the module. + Decollateralize(ctx context.Context, in *MsgDecollateralize, opts ...grpc.CallOption) (*MsgDecollateralizeResponse, error) + // Borrow allows a user to borrow tokens from the module if they have sufficient collateral. + Borrow(ctx context.Context, in *MsgBorrow, opts ...grpc.CallOption) (*MsgBorrowResponse, error) + // 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 the target's collateral. Liquidate(ctx context.Context, in *MsgLiquidate, opts ...grpc.CallOption) (*MsgLiquidateResponse, error) } @@ -878,23 +892,20 @@ type MsgServer interface { // Supply moves tokens from user balance to the module for lending or collateral. // The user receives uTokens in return. Supply(context.Context, *MsgSupply) (*MsgSupplyResponse, error) - // WithdrawAsset defines a method for withdrawing previously supplied coins from - // the capital facility. - WithdrawAsset(context.Context, *MsgWithdrawAsset) (*MsgWithdrawAssetResponse, error) - // AddCollateral defines a method for users to enable selected uTokens - // as collateral. - AddCollateral(context.Context, *MsgAddCollateral) (*MsgAddCollateralResponse, error) - // RemoveCollateral defines a method for users to disable selected uTokens - // as collateral. - RemoveCollateral(context.Context, *MsgRemoveCollateral) (*MsgRemoveCollateralResponse, error) - // BorrowAsset defines a method for borrowing coins from the capital facility. - BorrowAsset(context.Context, *MsgBorrowAsset) (*MsgBorrowAssetResponse, error) - // RepayAsset defines a method for repaying borrowed coins to the capital - // facility. - RepayAsset(context.Context, *MsgRepayAsset) (*MsgRepayAssetResponse, error) - // Liquidate defines a method for repaying another user's borrowed - // coins in exchange for some of their collateral. - + // Withdraw moves previously supplied tokens from the module back to the user balance in + // exchange for burning uTokens. + Withdraw(context.Context, *MsgWithdraw) (*MsgWithdrawResponse, error) + // Collateralize enables selected uTokens as collateral, which moves them to the module. + Collateralize(context.Context, *MsgCollateralize) (*MsgCollateralizeResponse, error) + // Decollateralize disables selected uTokens as collateral. They are returned to the user's + // balance from the module. + Decollateralize(context.Context, *MsgDecollateralize) (*MsgDecollateralizeResponse, error) + // Borrow allows a user to borrow tokens from the module if they have sufficient collateral. + Borrow(context.Context, *MsgBorrow) (*MsgBorrowResponse, error) + // 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 the target's collateral. Liquidate(context.Context, *MsgLiquidate) (*MsgLiquidateResponse, error) } From 035fb5312668aa0742695b63ad71befb35b5413d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 07:51:08 -0700 Subject: [PATCH 12/42] ++ Co-authored-by: Robert Zaremba --- x/leverage/keeper/keeper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 50a701bd11..1d85829a3d 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -352,7 +352,7 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, co // 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 (in that order). +// returns the actual amount of tokens repaid, uTokens consumed, and base tokens rewarded. func (k Keeper) Liquidate( ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, desiredRepay sdk.Coin, rewardDenom string, ) (baseRepay sdk.Coin, collateralReward sdk.Coin, baseReward sdk.Coin, err error) { From 3145f57a24ebbd362e82a227bbc89e0952b6ff4c Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 08:06:23 -0700 Subject: [PATCH 13/42] suggestion++ Co-authored-by: Robert Zaremba --- x/leverage/keeper/math.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/math.go b/x/leverage/keeper/math.go index 304c1786e3..481c2cc937 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/keeper/math.go @@ -29,7 +29,7 @@ func ApproxExponential(x sdk.Dec) sdk.Dec { } // ReduceProportionally accepts two sdk.Int to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied by (a/b) if a < b, then +// any number of pointers to sdk.Int which will be multiplied (and updated) by (a/b) if a < b, then // rounded up. If a >= b or b == 0 this is a no-op. func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { if a.GTE(b) || b.IsZero() { From 654e6edc598656bb0d4eb4929e42bef0703718b4 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 08:06:42 -0700 Subject: [PATCH 14/42] suggestion++ Co-authored-by: Robert Zaremba --- x/leverage/keeper/math.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/math.go b/x/leverage/keeper/math.go index 481c2cc937..84993e4e83 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/keeper/math.go @@ -42,7 +42,7 @@ func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { } // ReduceProportionallyDec accepts two sdk.Dec to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied by (a/b) if a < b, then +// any number of pointers to sdk.Int which will be multiplied (and updated) by (a/b) if a < b, then // rounded up. If a >= b or b == 0 this is a no-op. func ReduceProportionallyDec(a, b sdk.Dec, nums ...*sdk.Int) { if a.GTE(b) || b.IsZero() { From 5ee767bdaf394e3639b484dde20739d4c1c308d3 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 09:33:57 -0700 Subject: [PATCH 15/42] comment++ --- x/leverage/keeper/math.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/math.go b/x/leverage/keeper/math.go index 84993e4e83..48923c3497 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/keeper/math.go @@ -29,8 +29,8 @@ func ApproxExponential(x sdk.Dec) sdk.Dec { } // ReduceProportionally accepts two sdk.Int to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied (and updated) by (a/b) if a < b, then -// rounded up. If a >= b or b == 0 this is a no-op. +// any number of pointers to sdk.Int which will be multiplied in place by (a/b) if a < b, +// then rounded up. If a >= b or b == 0 this is a no-op. func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { if a.GTE(b) || b.IsZero() { return @@ -42,8 +42,8 @@ func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { } // ReduceProportionallyDec accepts two sdk.Dec to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied (and updated) by (a/b) if a < b, then -// rounded up. If a >= b or b == 0 this is a no-op. +// any number of pointers to sdk.Int which will be multiplied in place by (a/b) if a < b, +// then rounded up. If a >= b or b == 0 this is a no-op. func ReduceProportionallyDec(a, b sdk.Dec, nums ...*sdk.Int) { if a.GTE(b) || b.IsZero() { return From 59a058f3996753d64b288327cf41521c1755e5b2 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 09:41:18 -0700 Subject: [PATCH 16/42] move an error between functions --- x/leverage/keeper/liquidate.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 32d71fbcd0..1a3446fe1b 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -37,11 +37,6 @@ func (k Keeper) liquidationOutcome( if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - if liquidationThreshold.GTE(borrowedValue) { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationIneligible.Wrapf( - "%s borrowed value %s is below the liquidation threshold %s", - targetAddr, borrowedValue, liquidationThreshold) - } // get dynamic close factor and liquidation incentive liquidationIncentive, closeFactor, err := k.liquidationParams(ctx, rewardDenom, borrowedValue, liquidationThreshold) @@ -123,6 +118,12 @@ func (k Keeper) liquidationParams( return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, liquidationThreshold.String()) } + if liquidationThreshold.GTE(borrowedValue) { + return sdk.ZeroDec(), sdk.ZeroDec(), types.ErrLiquidationIneligible.Wrapf( + "borrowed value %s is below the liquidation threshold %s", + borrowedValue, liquidationThreshold) + } + ts, err := k.GetTokenSettings(ctx, rewardDenom) if err != nil { return sdk.ZeroDec(), sdk.ZeroDec(), err From fd214350f5005c43970ed99b35b7b1b97c69239a Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 09:50:24 -0700 Subject: [PATCH 17/42] fix test --- x/leverage/keeper/math_test.go | 9 +++++++-- x/leverage/simulation/operations_test.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index 99d814d9eb..f10853e197 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -53,8 +53,13 @@ func TestReduceProportional(t *testing.T) { } // No-op tests - testCase(2, 1, 40, 40) // a/b > 0 - testCase(1, 0, 50, 50) // b == 0 + testCase(2, 1, 40, 40) // a/b > 0 + testCase(1, 0, 50, 50) // b == 0 + testCase(3, 3, 1, 1) // a/b == 1 + testCase(3, 3, 1000, 1000) // a/b == 1 + testCase(1000, 1000, 3, 3) // a/b == 1 + testCase(3333, 3333, 1000, 1000) // a/b == 1 + testCase(1000, 1000, 3333, 3333) // a/b == 1 // Zero result tests testCase(1, 2, 0, 0) // (1/2)0 = 0 diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index 93ca6f95ed..6a663edb07 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -321,7 +321,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 0.001000000000000000 is below the liquidation threshold 0.005000000000000000: borrower not eligible for liquidation", + "failed to execute message; message index: 0: borrowed value 0.001000000000000000 is below the liquidation threshold 0.005000000000000000: borrower not eligible for liquidation", ) var msg types.MsgLiquidate From 9ccab78d1da25269619e31158c75f4081be50a93 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 10:01:39 -0700 Subject: [PATCH 18/42] add descriptive strings to math test cases --- x/leverage/keeper/math_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index f10853e197..8401c339e4 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -42,35 +42,35 @@ func TestInterpolate(t *testing.T) { } func TestReduceProportional(t *testing.T) { - testCase := func(a, b, initial, expected int64) { + testCase := func(a, b, initial, expected int64, message string) { n := sdk.NewInt(initial) ReduceProportionally(sdk.NewInt(a), sdk.NewInt(b), &n) - require.Equal(t, expected, n.Int64()) + require.Equal(t, expected, n.Int64(), message) m := sdk.NewInt(initial) ReduceProportionallyDec(sdk.NewDecFromInt(sdk.NewInt(a)), sdk.NewDecFromInt(sdk.NewInt(b)), &m) - require.Equal(t, expected, m.Int64()) + require.Equal(t, expected, m.Int64(), message) } // No-op tests - testCase(2, 1, 40, 40) // a/b > 0 - testCase(1, 0, 50, 50) // b == 0 - testCase(3, 3, 1, 1) // a/b == 1 - testCase(3, 3, 1000, 1000) // a/b == 1 - testCase(1000, 1000, 3, 3) // a/b == 1 - testCase(3333, 3333, 1000, 1000) // a/b == 1 - testCase(1000, 1000, 3333, 3333) // a/b == 1 + testCase(2, 1, 40, 40, "a/b > 0") + testCase(1, 0, 50, 50, "b = 0") + testCase(3, 3, 1, 1, "3/3 * 1 = 1") + testCase(3, 3, 1000, 1000, "3/3 * 1000 = 1000") + testCase(1000, 1000, 3, 3, "1000/1000 * 3 = 3") + testCase(3333, 3333, 1000, 1000, "3333/3333 * 1000 = 1000") + testCase(1000, 1000, 3333, 3333, "1000/1000 * 3333 = 3333") // Zero result tests - testCase(1, 2, 0, 0) // (1/2)0 = 0 - testCase(0, 1, 60, 0) // (0/1)60 = 0 + testCase(1, 2, 0, 0, "1/2 * 0 = 0") + testCase(0, 1, 60, 0, "0/1 * 60 = 0") // Round number tests - testCase(1, 2, 70, 35) // (1/2)70 = 35 - testCase(1, 2, 8866, 4433) // (1/2)8866 = 4433 - testCase(1, 3, 3000, 1000) // (1/3)3000 = 1000 + testCase(1, 2, 70, 35, "1/2 * 70 = 35") + testCase(1, 2, 8866, 4433, "1/2 * 8866 = 4433") + testCase(1, 3, 3000, 1000, "1/3 * 3000 = 1000") // Ceiling tests - testCase(1, 3, 1, 1) // (1/3)1 -> 1 - testCase(1, 3, 10, 4) // (1/3)10 -> 4 + testCase(1, 3, 1, 1, "1/3 * 1 = 1") + testCase(1, 3, 10, 4, "1/3 * 10 = 4") } From 144993e5ce706db69c5461bf7004cf00c17e6f41 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 13 Jul 2022 10:18:22 -0700 Subject: [PATCH 19/42] proto comments --- proto/umee/leverage/v1/tx.proto | 15 +++++ x/leverage/types/tx.pb.go | 107 ++++++++++++++++++-------------- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index 25ff636526..62ea482c7c 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -90,8 +90,15 @@ message MsgLiquidate { // Liquidator is the account address performing a liquidation and the signer // of the message. 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; } @@ -112,12 +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 { + // 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 burned 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/types/tx.pb.go b/x/leverage/types/tx.pb.go index 0d68cd1e54..f00c8582e7 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -280,10 +280,17 @@ var xxx_messageInfo_MsgRepay proto.InternalMessageInfo 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"` - RewardDenom string `protobuf:"bytes,4,opt,name=reward_denom,json=rewardDenom,proto3" json:"reward_denom,omitempty"` + 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{} } @@ -506,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"` } @@ -544,9 +553,15 @@ var xxx_messageInfo_MsgRepayResponse proto.InternalMessageInfo // MsgLiquidateResponse defines the Msg/Liquidate response type. type MsgLiquidateResponse struct { - Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` + // 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"` + // Collateral is the amount of the borrower's uToken collateral that + // was burned as a result of liquidation. Collateral types.Coin `protobuf:"bytes,2,opt,name=collateral,proto3" json:"collateral"` - Reward types.Coin `protobuf:"bytes,3,opt,name=reward,proto3" json:"reward"` + // 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,46 +617,46 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/tx.proto", fileDescriptor_72683128ee6e8843) } var fileDescriptor_72683128ee6e8843 = []byte{ - // 614 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdd, 0x6e, 0xd3, 0x4c, - 0x10, 0x8d, 0xdb, 0x34, 0x6a, 0xa6, 0xfd, 0xf4, 0x15, 0xb7, 0x48, 0xa9, 0x01, 0x53, 0xcc, 0x8f, - 0x2a, 0x04, 0x36, 0x49, 0x85, 0xb8, 0x42, 0x48, 0x69, 0x25, 0xa4, 0x80, 0x25, 0x94, 0x5e, 0x20, - 0x21, 0x41, 0x71, 0x92, 0x91, 0x6b, 0xe1, 0x78, 0xcd, 0xee, 0x26, 0x69, 0x78, 0x0a, 0x1e, 0x84, - 0x07, 0x29, 0x77, 0xbd, 0x41, 0xe2, 0x0a, 0xa1, 0xe4, 0x45, 0xd0, 0xfa, 0x67, 0xf3, 0x83, 0x1b, - 0x2c, 0x50, 0xee, 0x76, 0xe7, 0x9c, 0x39, 0x67, 0x3d, 0x33, 0x1a, 0xc3, 0x6e, 0xaf, 0x8b, 0x68, - 0xf9, 0xd8, 0x47, 0xea, 0xb8, 0x68, 0xf5, 0xab, 0x16, 0x3f, 0x33, 0x43, 0x4a, 0x38, 0x51, 0xb7, - 0x04, 0x64, 0xa6, 0x90, 0xd9, 0xaf, 0x6a, 0x7a, 0x9b, 0xb0, 0x2e, 0x61, 0x56, 0xcb, 0x61, 0x82, - 0xda, 0x42, 0xee, 0x54, 0xad, 0x36, 0xf1, 0x82, 0x38, 0x43, 0xdb, 0x71, 0x89, 0x4b, 0xa2, 0xa3, - 0x25, 0x4e, 0x71, 0xd4, 0x78, 0x07, 0x65, 0x9b, 0xb9, 0xc7, 0xbd, 0x30, 0xf4, 0x87, 0xaa, 0x06, - 0xeb, 0x4c, 0x9c, 0x3c, 0xa4, 0x15, 0x65, 0x4f, 0xd9, 0x2f, 0x37, 0xe5, 0x5d, 0x7d, 0x0c, 0x6b, - 0x0e, 0x63, 0xc8, 0x2b, 0x2b, 0x7b, 0xca, 0xfe, 0x46, 0x6d, 0xd7, 0x8c, 0xed, 0x4c, 0x61, 0x67, - 0x26, 0x76, 0xe6, 0x21, 0xf1, 0x82, 0x7a, 0xf1, 0xfc, 0xc7, 0xcd, 0x42, 0x33, 0x66, 0x1b, 0xef, - 0x61, 0xc3, 0x66, 0xee, 0x6b, 0x8f, 0x9f, 0x76, 0xa8, 0x33, 0x58, 0x86, 0x43, 0x1b, 0xb6, 0x6c, - 0xe6, 0x1e, 0x12, 0xdf, 0x77, 0x38, 0x52, 0xc7, 0xf7, 0x3e, 0xa1, 0xb0, 0x69, 0x11, 0x4a, 0xc9, - 0x60, 0x62, 0x93, 0xde, 0xd5, 0x03, 0x28, 0x8a, 0xaa, 0xe4, 0x75, 0x89, 0xc8, 0x06, 0x82, 0x6a, - 0x33, 0xf7, 0x08, 0xdb, 0xcb, 0xb5, 0x89, 0xbb, 0x51, 0x8f, 0x34, 0x16, 0xaa, 0xff, 0x65, 0xad, - 0xde, 0xc2, 0xba, 0xcd, 0xdc, 0x26, 0x86, 0xce, 0x70, 0x19, 0xf2, 0x5f, 0x14, 0xd8, 0xb4, 0x99, - 0xfb, 0xd2, 0xfb, 0xd8, 0xf3, 0x3a, 0x0e, 0x47, 0x55, 0x07, 0xf0, 0x93, 0x0b, 0x49, 0x5d, 0xa6, - 0x22, 0x33, 0x6f, 0x58, 0x99, 0x7b, 0xc3, 0x53, 0x28, 0x53, 0xf1, 0xd0, 0x2e, 0x06, 0xbc, 0xb2, - 0x9a, 0xef, 0x1d, 0x93, 0x0c, 0xf5, 0x16, 0x6c, 0x52, 0x1c, 0x38, 0xb4, 0x73, 0xd2, 0xc1, 0x80, - 0x74, 0x2b, 0xc5, 0x48, 0x7e, 0x23, 0x8e, 0x1d, 0x89, 0x90, 0xb1, 0x0d, 0x57, 0xe4, 0xec, 0x37, - 0x91, 0x85, 0x24, 0x60, 0x68, 0x5c, 0x85, 0xed, 0xa9, 0x81, 0x95, 0x61, 0x0d, 0x2a, 0xf3, 0x53, - 0x26, 0xb1, 0xeb, 0xa0, 0xfd, 0x3e, 0x1c, 0x12, 0x8d, 0x5d, 0xe2, 0x9e, 0xca, 0xe0, 0x8b, 0x68, - 0x68, 0xa3, 0x46, 0xa4, 0x31, 0xf5, 0x09, 0x94, 0xc4, 0xf3, 0xbd, 0x4e, 0x54, 0xa8, 0x1c, 0x5f, - 0x9b, 0xd0, 0x8d, 0xaf, 0x0a, 0xec, 0x4c, 0x97, 0xfd, 0x9f, 0x15, 0xd5, 0x67, 0x00, 0x93, 0x8f, - 0xc9, 0x3b, 0x04, 0x53, 0x29, 0xb1, 0xb3, 0xa8, 0x74, 0xde, 0xce, 0x25, 0xf4, 0xda, 0xb7, 0x22, - 0xac, 0xda, 0xcc, 0x55, 0x1b, 0x50, 0x4a, 0x96, 0xd2, 0x35, 0x73, 0x7e, 0xd5, 0x99, 0xb2, 0x6b, - 0xda, 0xed, 0x05, 0xa0, 0x2c, 0xc3, 0x2b, 0x58, 0x97, 0x0b, 0xe8, 0x46, 0x66, 0x42, 0x0a, 0x6b, - 0x77, 0x17, 0xc2, 0x52, 0xf1, 0x04, 0xfe, 0x9b, 0x5d, 0x38, 0x46, 0x66, 0xde, 0x0c, 0x47, 0xbb, - 0xff, 0x67, 0x8e, 0x34, 0x40, 0xf8, 0x7f, 0x7e, 0xd9, 0xdc, 0xc9, 0x4c, 0x9f, 0x63, 0x69, 0x0f, - 0xf2, 0xb0, 0xa4, 0x4d, 0x03, 0x4a, 0xc9, 0xb2, 0xc9, 0xae, 0x72, 0x0c, 0x5e, 0x52, 0xe5, 0xd9, - 0x91, 0x56, 0x9f, 0xc3, 0x5a, 0xb2, 0x58, 0x32, 0xd9, 0x11, 0xa6, 0x19, 0x97, 0x63, 0x52, 0xe8, - 0x18, 0xca, 0x53, 0x1b, 0x24, 0x33, 0x41, 0xe2, 0xda, 0xbd, 0xc5, 0x78, 0x2a, 0x5a, 0x6f, 0x9c, - 0x8f, 0x74, 0xe5, 0x62, 0xa4, 0x2b, 0x3f, 0x47, 0xba, 0xf2, 0x79, 0xac, 0x17, 0x2e, 0xc6, 0x7a, - 0xe1, 0xfb, 0x58, 0x2f, 0xbc, 0x79, 0xe4, 0x7a, 0xfc, 0xb4, 0xd7, 0x32, 0xdb, 0xa4, 0x6b, 0x09, - 0xad, 0x87, 0x01, 0xf2, 0x01, 0xa1, 0x1f, 0xa2, 0x8b, 0xd5, 0xaf, 0x59, 0x67, 0x93, 0x3f, 0x30, - 0x1f, 0x86, 0xc8, 0x5a, 0xa5, 0xe8, 0xd7, 0x79, 0xf0, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x9b, 0xe1, - 0xe1, 0x82, 0x9f, 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. From 03d0e084be57c3f77f34fdf7d13db4c8b5295319 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Sun, 17 Jul 2022 09:32:39 -0700 Subject: [PATCH 20/42] filter blacklisted collateral before checking for bad debt --- x/leverage/keeper/keeper.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 1d85829a3d..5bf2301566 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -406,9 +406,16 @@ func (k Keeper) Liquidate( return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } + // get remaining collateral, ignoring blacklisted + remainingCollateral := filterCoins( + k.GetBorrowerCollateral(ctx, borrowerAddr), + func(c sdk.Coin) bool { + return k.validateAcceptedAsset(ctx, c) == nil + }, + ) + // detect bad debt if collateral is completely exhausted - if k.GetBorrowerCollateral(ctx, borrowerAddr).IsZero() { - // TODO: exclude blacklisted collateral + 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 { @@ -419,3 +426,14 @@ func (k Keeper) Liquidate( return baseRepay, collateralReward, baseReward, nil } + +// filterCoins returns the subset of an sdk.Coins that meet a given condition +func 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 +} From 93cf56c80f1a9f7affda32e7406a2a7f83bd9d59 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:21:10 -0700 Subject: [PATCH 21/42] separate out liquidation function and reduce rounding --- x/leverage/keeper/borrows.go | 35 ++++---- x/leverage/keeper/filter.go | 27 ++++++ x/leverage/keeper/keeper.go | 26 ++---- x/leverage/keeper/liquidate.go | 154 +++++++++++++++++++-------------- x/leverage/keeper/math.go | 26 ------ x/leverage/keeper/math_test.go | 34 -------- x/leverage/keeper/oracle.go | 13 +-- 7 files changed, 149 insertions(+), 166 deletions(-) create mode 100644 x/leverage/keeper/filter.go diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index bcfffd08db..9cbad5617c 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,21 @@ 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..c09442b762 --- /dev/null +++ b/x/leverage/keeper/filter.go @@ -0,0 +1,27 @@ +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(ctx sdk.Context, 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( + ctx, + 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 5bf2301566..c3430a4073 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -354,9 +354,9 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, co // 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, desiredRepay sdk.Coin, rewardDenom string, + 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, desiredRepay); err != nil { + if err := k.validateAcceptedAsset(ctx, maxRepay); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } if err := k.validateAcceptedDenom(ctx, rewardDenom); err != nil { @@ -364,11 +364,11 @@ func (k Keeper) Liquidate( } // calculate Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances - baseRepay, collateralReward, baseReward, err = k.liquidationOutcome( + baseRepay, collateralReward, baseReward, err = k.computeLiquidation( ctx, liquidatorAddr, borrowerAddr, - desiredRepay, + maxRepay, rewardDenom, ) if err != nil { @@ -407,12 +407,7 @@ func (k Keeper) Liquidate( } // get remaining collateral, ignoring blacklisted - remainingCollateral := filterCoins( - k.GetBorrowerCollateral(ctx, borrowerAddr), - func(c sdk.Coin) bool { - return k.validateAcceptedAsset(ctx, c) == nil - }, - ) + remainingCollateral := k.filterAcceptedCoins(ctx, k.GetBorrowerCollateral(ctx, borrowerAddr)) // detect bad debt if collateral is completely exhausted if remainingCollateral.IsZero() { @@ -426,14 +421,3 @@ func (k Keeper) Liquidate( return baseRepay, collateralReward, baseReward, nil } - -// filterCoins returns the subset of an sdk.Coins that meet a given condition -func 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 -} diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 1a3446fe1b..b7bb7b03d4 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -7,33 +7,86 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -// liquidationOutcome takes a repayment and reward denom proposed by a liquidator and calculates -// the maximum repayment amounts a target address is eligible for, and the corresponding reward -// amounts using current oracle, params, and available balances. Inputs must be registered tokens. -// Outputs are base token repayment, uToken collateral cost, and base token reward from liquidation. -func (k Keeper) liquidationOutcome( +// reduceLiquidation 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 reduceLiquidation( + 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, +) (tokenRepay sdk.Int, collateralBurn sdk.Int, tokenReward sdk.Int) { + // Start with the maximum possible repayment amount, as a decimal + repay := availableRepay.ToDec() + // Determine the base reward amount that would result from maximum repayment + reward := repay.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) + // Determine the collateral burn amount that corresponds to base reward amount + collateral := reward.Quo(uTokenExchangeRate) + + // After computing all limiting conditions, all three values will be reduced by the same ratio + ratio := sdk.OneDec() + // Repaid value cannot exceed borrowed value times close factor + ratio = sdk.MinDec(ratio, + borrowedValue.Mul(closeFactor).Quo(repay.Mul(repayTokenPrice)), + ) + // Collateral burned cannot exceed borrower's collateral + ratio = sdk.MinDec(ratio, + availableCollateral.ToDec().Quo(collateral), + ) + // Base token reward cannot exceed available unreserved module balance + ratio = sdk.MinDec(ratio, + availableReward.ToDec().Quo(reward), + ) + + // The amount of borrowed token the liquidator will repay is rounded up after reduction + tokenRepay = repay.Mul(ratio).Ceil().RoundInt() + // The amount of collateral uToken the borrower will lose is rounded up after reduction + collateralBurn = collateral.Mul(ratio).Ceil().RoundInt() + // The amount of reward token the liquidator will receive is rounded down after reduction + tokenReward = reward.Mul(ratio).TruncateInt() + + return tokenRepay, collateralBurn, tokenReward +} + +// computeLiquidation 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) computeLiquidation( ctx sdk.Context, liquidatorAddr sdk.AccAddress, targetAddr sdk.AccAddress, - desiredRepay sdk.Coin, + maxRepay sdk.Coin, rewardDenom string, -) (sdk.Coin, sdk.Coin, sdk.Coin, error) { - // get liquidator's available balance of base asset to repay - availableRepay := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(desiredRepay.Denom) - - // get module's available balance of reward base asset - availableReward := k.ModuleBalance(ctx, rewardDenom).Sub(k.GetReserveAmount(ctx, rewardDenom)) +) (tokenRepay sdk.Coin, collateralBurn sdk.Coin, tokenReward sdk.Coin, err error) { + repayDenom := maxRepay.Denom + collateralDenom := k.FromTokenToUTokenDenom(ctx, rewardDenom) - // examine target address - collateral := k.GetBorrowerCollateral(ctx, targetAddr) - borrowed := k.GetBorrowerBorrows(ctx, targetAddr) + // 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 position health in USD values - borrowedValue, err := k.TotalTokenValue(ctx, borrowed) + // 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, collateral) + liquidationThreshold, err := k.CalculateLiquidationThreshold(ctx, borrowerCollateral) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } @@ -44,60 +97,33 @@ func (k Keeper) liquidationOutcome( return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // maximum repayment starts at desiredRepayment but can be lower due to limiting factors - baseRepay := desiredRepay - // repayment cannot exceed borrower's borrowed amount of selected denom - baseRepay.Amount = sdk.MinInt(baseRepay.Amount, borrowed.AmountOf(baseRepay.Denom)) - // repayment cannot exceed liquidator's available balance - baseRepay.Amount = sdk.MinInt(baseRepay.Amount, availableRepay) - // repayment USD value cannot exceed borrowed USD value * close factor - repayValueLimit := borrowedValue.Mul(closeFactor) - repayValue, err := k.TokenValue(ctx, baseRepay) + // 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 } - // if repayValue > repayValueLimit - // maxRepayment *= (repayValueLimit / repayValue) - ReduceProportionallyDec(repayValueLimit, repayValue, &baseRepay.Amount) - if baseRepay.IsZero() { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRepayZero - } - - // find the price ratio of repay:reward tokens - priceRatio, err := k.PriceRatio(ctx, baseRepay.Denom, rewardDenom) + rewardTokenPrice, err := k.TokenPrice(ctx, rewardDenom) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // determine uToken collateral reward, rounding up - uReward := sdk.NewCoin( - k.FromTokenToUTokenDenom(ctx, rewardDenom), - // uReward = repay * (repayPrice / rewardPrice) * (1 + incentive), rounded up - baseRepay.Amount.ToDec().Mul(priceRatio).Mul(sdk.OneDec().Add(liquidationIncentive)).Ceil().RoundInt(), - ) - // uToken reward cannot exceed available collateral - availableCollateral := collateral.AmountOf(uReward.Denom) - // if uReward > availableCollateral - // uReward = availableCollateral - // baseRepay *= (availableCollateral / uReward) - ReduceProportionally(availableCollateral, uReward.Amount, &uReward.Amount, &baseRepay.Amount) - if uReward.IsZero() { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRewardZero - } + // get collateral uToken exchange rate + exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) + + // compute final liquidation amounts + repay, burn, reward := reduceLiquidation( + 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, + liquidationIncentive, + closeFactor, + borrowedValue, + ) - // convert uToken reward to base tokens, rounding down - baseReward, err := k.ExchangeUToken(ctx, uReward) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - // base reward cannot exceed available reward - // if baseReward > availableReward - // baseReward = availableReward - // uReward *= (availableReward / baseReward) - // baseRepay *= (availableReward / baseReward) - ReduceProportionally(availableReward, baseReward.Amount, &baseReward.Amount, &uReward.Amount, &baseRepay.Amount) - - return baseRepay, uReward, baseReward, nil + return sdk.NewCoin(repayDenom, repay), sdk.NewCoin(collateralDenom, burn), sdk.NewCoin(rewardDenom, reward), nil } // liquidationParams computes dynamic liquidation parameters based on a collateral reward diff --git a/x/leverage/keeper/math.go b/x/leverage/keeper/math.go index 48923c3497..0a756d535b 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/keeper/math.go @@ -27,29 +27,3 @@ func ApproxExponential(x sdk.Dec) sdk.Dec { sum = sum.Add(next.QuoInt64(6)) // 3! return sum // approximated e^x } - -// ReduceProportionally accepts two sdk.Int to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied in place by (a/b) if a < b, -// then rounded up. If a >= b or b == 0 this is a no-op. -func ReduceProportionally(a, b sdk.Int, nums ...*sdk.Int) { - if a.GTE(b) || b.IsZero() { - return - } - ratio := a.ToDec().Quo(b.ToDec()) // a/b - for _, n := range nums { - *n = n.ToDec().Mul(ratio).Ceil().RoundInt() - } -} - -// ReduceProportionallyDec accepts two sdk.Dec to be interpreted as a fraction (a/b), and -// any number of pointers to sdk.Int which will be multiplied in place by (a/b) if a < b, -// then rounded up. If a >= b or b == 0 this is a no-op. -func ReduceProportionallyDec(a, b sdk.Dec, nums ...*sdk.Int) { - if a.GTE(b) || b.IsZero() { - return - } - ratio := a.Quo(b) - for _, n := range nums { - *n = n.ToDec().Mul(ratio).Ceil().RoundInt() - } -} diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index 8401c339e4..b2351466a0 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -40,37 +40,3 @@ func TestInterpolate(t *testing.T) { x = Interpolate(x1, x1, y1, x1, y1) require.Equal(t, x, y1) } - -func TestReduceProportional(t *testing.T) { - testCase := func(a, b, initial, expected int64, message string) { - n := sdk.NewInt(initial) - ReduceProportionally(sdk.NewInt(a), sdk.NewInt(b), &n) - require.Equal(t, expected, n.Int64(), message) - - m := sdk.NewInt(initial) - ReduceProportionallyDec(sdk.NewDecFromInt(sdk.NewInt(a)), sdk.NewDecFromInt(sdk.NewInt(b)), &m) - require.Equal(t, expected, m.Int64(), message) - } - - // No-op tests - testCase(2, 1, 40, 40, "a/b > 0") - testCase(1, 0, 50, 50, "b = 0") - testCase(3, 3, 1, 1, "3/3 * 1 = 1") - testCase(3, 3, 1000, 1000, "3/3 * 1000 = 1000") - testCase(1000, 1000, 3, 3, "1000/1000 * 3 = 3") - testCase(3333, 3333, 1000, 1000, "3333/3333 * 1000 = 1000") - testCase(1000, 1000, 3333, 3333, "1000/1000 * 3333 = 3333") - - // Zero result tests - testCase(1, 2, 0, 0, "1/2 * 0 = 0") - testCase(0, 1, 60, 0, "0/1 * 60 = 0") - - // Round number tests - testCase(1, 2, 70, 35, "1/2 * 70 = 35") - testCase(1, 2, 8866, 4433, "1/2 * 8866 = 4433") - testCase(1, 3, 3000, 1000, "1/3 * 3000 = 1000") - - // Ceiling tests - testCase(1, 3, 1, 1, "1/3 * 1 = 1") - testCase(1, 3, 10, 4, "1/3 * 10 = 4") -} diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 7799ed1607..4c5f737956 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) @@ -50,11 +48,14 @@ func (k Keeper) TokenValue(ctx sdk.Context, coin sdk.Coin) (sdk.Dec, error) { } // 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 coins 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 From d27f9041c234fbda89b6e8b61f7f591193e7669b Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:26:00 -0700 Subject: [PATCH 22/42] clarify math comments --- x/leverage/keeper/liquidate.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index b7bb7b03d4..dc4613a603 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -38,7 +38,7 @@ func reduceLiquidation( // Determine the collateral burn amount that corresponds to base reward amount collateral := reward.Quo(uTokenExchangeRate) - // After computing all limiting conditions, all three values will be reduced by the same ratio + // 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, @@ -52,13 +52,20 @@ func reduceLiquidation( ratio = sdk.MinDec(ratio, availableReward.ToDec().Quo(reward), ) + // Catch edge cases + ratio = sdk.MaxDec(ratio, sdk.ZeroDec()) + + // Reduce all three values by the most severe limiting factor encountered + repay = repay.Mul(ratio) + collateral = collateral.Mul(ratio) + reward = reward.Mul(ratio) // The amount of borrowed token the liquidator will repay is rounded up after reduction - tokenRepay = repay.Mul(ratio).Ceil().RoundInt() + tokenRepay = repay.Ceil().RoundInt() // The amount of collateral uToken the borrower will lose is rounded up after reduction - collateralBurn = collateral.Mul(ratio).Ceil().RoundInt() + collateralBurn = collateral.Ceil().RoundInt() // The amount of reward token the liquidator will receive is rounded down after reduction - tokenReward = reward.Mul(ratio).TruncateInt() + tokenReward = reward.TruncateInt() return tokenRepay, collateralBurn, tokenReward } From 9713fffec2b7a239fa3b24b7eaf6f82734721711 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:26:20 -0700 Subject: [PATCH 23/42] clarify math comments --- x/leverage/keeper/liquidate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index dc4613a603..c44507bdc7 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -60,11 +60,11 @@ func reduceLiquidation( collateral = collateral.Mul(ratio) reward = reward.Mul(ratio) - // The amount of borrowed token the liquidator will repay is rounded up after reduction + // The amount of borrowed token the liquidator will repay is rounded up tokenRepay = repay.Ceil().RoundInt() - // The amount of collateral uToken the borrower will lose is rounded up after reduction + // The amount of collateral uToken the borrower will lose is rounded up collateralBurn = collateral.Ceil().RoundInt() - // The amount of reward token the liquidator will receive is rounded down after reduction + // The amount of reward token the liquidator will receive is rounded down tokenReward = reward.TruncateInt() return tokenRepay, collateralBurn, tokenReward From 3331a0003639e82c349f7ea407043b35c110aa78 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:47:35 -0700 Subject: [PATCH 24/42] more test cases and a rounding TODO --- x/leverage/keeper/borrows.go | 1 + x/leverage/keeper/interest.go | 6 +- x/leverage/keeper/keeper.go | 5 + x/leverage/keeper/keeper_test.go | 82 ---------- x/leverage/keeper/liquidate.go | 153 +++--------------- x/leverage/keeper/oracle.go | 3 +- x/leverage/keeper/oracle_test.go | 5 +- x/leverage/simulation/operations_test.go | 2 +- x/leverage/types/errors.go | 5 +- x/leverage/types/leverage.go | 131 +++++++++++++++ x/leverage/types/leverage_test.go | 187 ++++++++++++++++++++++ x/leverage/{keeper => types}/math.go | 2 +- x/leverage/{keeper => types}/math_test.go | 20 +-- 13 files changed, 364 insertions(+), 238 deletions(-) create mode 100644 x/leverage/types/leverage.go create mode 100644 x/leverage/types/leverage_test.go rename x/leverage/{keeper => types}/math.go (98%) rename x/leverage/{keeper => types}/math_test.go (61%) diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 9cbad5617c..975c46462e 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -134,6 +134,7 @@ func (k Keeper) CalculateLiquidationThreshold(ctx sdk.Context, collateral sdk.Co if err != nil { return sdk.ZeroDec(), err } + // add each collateral coin's weighted value to liquidation threshold totalThreshold = totalThreshold.Add(v.Mul(ts.LiquidationThreshold)) } diff --git a/x/leverage/keeper/interest.go b/x/leverage/keeper/interest.go index ee3eacf483..103ff69050 100644 --- a/x/leverage/keeper/interest.go +++ b/x/leverage/keeper/interest.go @@ -26,7 +26,7 @@ func (k Keeper) DeriveBorrowAPY(ctx sdk.Context, denom string) sdk.Dec { utilization := k.SupplyUtilization(ctx, denom) if utilization.GTE(token.KinkUtilization) { - return Interpolate( + return types.Interpolate( utilization, // x token.KinkUtilization, // x1 token.KinkBorrowRate, // y1 @@ -36,7 +36,7 @@ func (k Keeper) DeriveBorrowAPY(ctx sdk.Context, denom string) sdk.Dec { } // utilization is between 0% and kink value - return Interpolate( + return types.Interpolate( utilization, // x sdk.ZeroDec(), // x1 token.BaseBorrowRate, // y1 @@ -104,7 +104,7 @@ func (k Keeper) AccrueAllInterest(ctx sdk.Context) error { // interest is accrued by continuous compound interest on each denom's Interest Scalar scalar := k.getInterestScalar(ctx, token.BaseDenom) // calculate e^(APY*time) - exponential := ApproxExponential(k.DeriveBorrowAPY(ctx, token.BaseDenom).Mul(yearsElapsed)) + exponential := types.ApproxExponential(k.DeriveBorrowAPY(ctx, token.BaseDenom).Mul(yearsElapsed)) // multiply interest scalar by e^(APY*time) if err := k.setInterestScalar(ctx, token.BaseDenom, scalar.Mul(exponential)); err != nil { return err diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index c3430a4073..87364f8cca 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -374,6 +374,11 @@ func (k Keeper) Liquidate( if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } + 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 + } // send repayment from liquidator to leverage module account err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, liquidatorAddr, types.ModuleName, sdk.NewCoins(baseRepay)) diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index 8f21e257d3..00fdc6a782 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -556,88 +556,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) - _, _, _, err = s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) - 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 partially liquidates user, receiving some collateral - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 10000000) // 10 umee - repaid, collateral, reward, err := s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(repayment, repaid) - s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 11000000), collateral) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 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 u/umee balance is still zero - uTokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 0)) - - // verify liquidator's new umee balance (10k + 1) = 10001 umee - tokenBalance := app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 10001000000)) - - // liquidator fully liquidates user, receiving more collateral and reducing borrowed amount to zero - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 300000000) // 300 umee - repaid, collateral, reward, err = s.app.LeverageKeeper.Liquidate(ctx, liquidatorAddr, addr, repayment, umeeDenom) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 80000000), repaid) - s.Require().Equal(sdk.NewInt64Coin("u/"+umeeDenom, 88000000), collateral) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 88000000), reward) - - // verify that desired repayment has not been modified, despite actual repayment being lower - s.Require().Equal(sdk.NewInt(300000000), repayment.Amount) - - // verify liquidator's u/umee is still zero - uTokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin(rewardDenom, 0)) - - // 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 + 9) = 10009 umee - tokenBalance = app.BankKeeper.GetBalance(ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 10009000000), tokenBalance) -} - 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 index c44507bdc7..7f211acae8 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -2,74 +2,10 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/umee-network/umee/v2/x/leverage/types" ) -// reduceLiquidation 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 reduceLiquidation( - 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, -) (tokenRepay sdk.Int, collateralBurn sdk.Int, tokenReward sdk.Int) { - // Start with the maximum possible repayment amount, as a decimal - repay := availableRepay.ToDec() - // Determine the base reward amount that would result from maximum repayment - reward := repay.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) - // Determine the collateral burn amount that corresponds to base reward amount - collateral := reward.Quo(uTokenExchangeRate) - - // 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(repay.Mul(repayTokenPrice)), - ) - // Collateral burned cannot exceed borrower's collateral - ratio = sdk.MinDec(ratio, - availableCollateral.ToDec().Quo(collateral), - ) - // Base token reward cannot exceed available unreserved module balance - ratio = sdk.MinDec(ratio, - availableReward.ToDec().Quo(reward), - ) - // Catch edge cases - ratio = sdk.MaxDec(ratio, sdk.ZeroDec()) - - // Reduce all three values by the most severe limiting factor encountered - repay = repay.Mul(ratio) - collateral = collateral.Mul(ratio) - reward = reward.Mul(ratio) - - // The amount of borrowed token the liquidator will repay is rounded up - tokenRepay = repay.Ceil().RoundInt() - // The amount of collateral uToken the borrower will lose is rounded up - collateralBurn = collateral.Ceil().RoundInt() - // The amount of reward token the liquidator will receive is rounded down - tokenReward = reward.TruncateInt() - - return tokenRepay, collateralBurn, tokenReward -} - // computeLiquidation 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. @@ -97,13 +33,27 @@ func (k Keeper) computeLiquidation( 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 dynamic close factor and liquidation incentive - liquidationIncentive, closeFactor, err := k.liquidationParams(ctx, rewardDenom, borrowedValue, liquidationThreshold) + // 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 := types.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 { @@ -118,84 +68,17 @@ func (k Keeper) computeLiquidation( exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) // compute final liquidation amounts - repay, burn, reward := reduceLiquidation( + repay, burn, reward := types.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, - liquidationIncentive, + ts.LiquidationIncentive, closeFactor, borrowedValue, ) return sdk.NewCoin(repayDenom, repay), sdk.NewCoin(collateralDenom, burn), sdk.NewCoin(rewardDenom, reward), nil } - -// liquidationParams computes dynamic liquidation parameters based on a collateral reward -// denomination, and a borrower's 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, - borrowedValue sdk.Dec, - liquidationThreshold sdk.Dec, -) (sdk.Dec, sdk.Dec, error) { - if borrowedValue.IsNegative() { - return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, borrowedValue.String()) - } - if liquidationThreshold.IsNegative() { - return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, liquidationThreshold.String()) - } - - if liquidationThreshold.GTE(borrowedValue) { - return sdk.ZeroDec(), sdk.ZeroDec(), types.ErrLiquidationIneligible.Wrapf( - "borrowed value %s is below the liquidation threshold %s", - borrowedValue, liquidationThreshold) - } - - 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 liquidationThreshold.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 borrowedValue.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( - borrowedValue.Quo(liquidationThreshold).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 -} diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index 4c5f737956..abb09c0f92 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -43,13 +43,12 @@ 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 the sum of TokenValue on each coin individually, except it -// ignores unregistered and blacklisted coins instead of returning an error. +// 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() diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index 4b69e49b2a..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,8 +93,8 @@ 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_PriceRatio() { diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index 6a663edb07..a30f8eb302 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -321,7 +321,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: borrowed value 0.001000000000000000 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/types/errors.go b/x/leverage/types/errors.go index 146312cc7e..8a95a1d019 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -30,7 +30,6 @@ 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") - ErrLiquidationRepayZero = sdkerrors.Register(ModuleName, 1123, "calculated liquidation repayment was zero") - ErrLiquidationRewardZero = sdkerrors.Register(ModuleName, 1124, "calculated liquidation reward was zero") - ErrZeroValuePriceRatio = sdkerrors.Register(ModuleName, 1125, "price ratio attempted with zero value token") + ErrLiquidationInvalid = sdkerrors.Register(ModuleName, 1123, "liquidation invalid") + ErrZeroValuePriceRatio = sdkerrors.Register(ModuleName, 1124, "price ratio attempted with zero value token") ) diff --git a/x/leverage/types/leverage.go b/x/leverage/types/leverage.go new file mode 100644 index 0000000000..23ea10e614 --- /dev/null +++ b/x/leverage/types/leverage.go @@ -0,0 +1,131 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// 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 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, +) (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 + repayDec := availableRepay.ToDec() + // Determine the base rewardDec amount that would result from maximum repayment + rewardDec := repayDec.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) + // Determine the collateralDec burn amount that corresponds to base reward amount + collateralDec := rewardDec.Quo(uTokenExchangeRate) + + // Catch no-ops early + if repayDec.IsZero() || rewardDec.IsZero() || collateralDec.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(repayDec.Mul(repayTokenPrice)), + ) + // Collateral burned cannot exceed borrower's collateral + ratio = sdk.MinDec(ratio, + availableCollateral.ToDec().Quo(collateralDec), + ) + // Base token reward cannot exceed available unreserved module balance + ratio = sdk.MinDec(ratio, + availableReward.ToDec().Quo(rewardDec), + ) + // Catch edge cases + ratio = sdk.MaxDec(ratio, sdk.ZeroDec()) + + // Reduce all three values by the most severe limiting factor encountered + repayDec = repayDec.Mul(ratio) + collateralDec = collateralDec.Mul(ratio) + rewardDec = rewardDec.Mul(ratio) + + // No rounding has occurred yet. In dust scenarios, the limiting factor will be a clean integer + // and the other two will need rounding. Otherwise, all three outputs will need rounding. + + // REQUIREMENTS: + // - Prevent REPAY VALUE > COLLATERAL VALUE rounding attacks + // - Prevent REWARD VALUE > COLLATERAL VALUE rounding attacks + // - Eliminate COLLATERAL DUST (required for bad debt flag) + + // TODO: Satisfy these requirements + + // The amount of borrowed token the liquidator will repay is rounded up + tokenRepay = repayDec.Ceil().RoundInt() + // The amount of collateral uToken the borrower will lose is rounded down + collateralBurn = collateralDec.TruncateInt() + + // Liquidator base token reward is derived from collateral burn then rounded down + tokenReward = collateralBurn.ToDec().Mul(uTokenExchangeRate).TruncateInt() + return tokenRepay, collateralBurn, tokenReward +} + +// ComputeCloseFactor uses a borrower's borrowed value and liquidation threshold and +// some leverage module parameters to derive a dynamic close factor for liquidation. +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/types/leverage_test.go b/x/leverage/types/leverage_test.go new file mode 100644 index 0000000000..55ec25aaf5 --- /dev/null +++ b/x/leverage/types/leverage_test.go @@ -0,0 +1,187 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/umee-network/umee/v2/x/leverage/types" +) + +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 := types.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 and no rounding + 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") + // is this a potential module drain? (discount repay) + + // collateral dust case, with high collateral token value and no rounding + 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") + // is this a potential module drain? (discount repay) + + // TODO: more rounding and dust scenarios. + // borrowed/collat/reward high/low price and maybe high/low utoken +} diff --git a/x/leverage/keeper/math.go b/x/leverage/types/math.go similarity index 98% rename from x/leverage/keeper/math.go rename to x/leverage/types/math.go index 0a756d535b..49d7df3c89 100644 --- a/x/leverage/keeper/math.go +++ b/x/leverage/types/math.go @@ -1,4 +1,4 @@ -package keeper +package types import sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/leverage/keeper/math_test.go b/x/leverage/types/math_test.go similarity index 61% rename from x/leverage/keeper/math_test.go rename to x/leverage/types/math_test.go index b2351466a0..4003650498 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/types/math_test.go @@ -1,10 +1,12 @@ -package keeper +package types_test import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + + "github.com/umee-network/umee/v2/x/leverage/types" ) func TestInterpolate(t *testing.T) { @@ -15,28 +17,28 @@ func TestInterpolate(t *testing.T) { y2 := sdk.MustNewDecFromStr("17.4") // Sloped line, endpoint checks - x := Interpolate(x1, x1, y1, x2, y2) + x := types.Interpolate(x1, x1, y1, x2, y2) require.Equal(t, x, y1) - x = Interpolate(x2, x1, y1, x2, y2) + x = types.Interpolate(x2, x1, y1, x2, y2) require.Equal(t, x, y2) // Sloped line, point on segment - x = Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) + x = types.Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) require.Equal(t, x, sdk.MustNewDecFromStr("13.2")) // Sloped line, point outside of segment - x = Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) + x = types.Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) require.Equal(t, x, sdk.MustNewDecFromStr("9.0")) // Vertical line: always return y1 - x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) + x = types.Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) require.Equal(t, x, y1) - x = Interpolate(x1, x1, y1, x1, y2) + x = types.Interpolate(x1, x1, y1, x1, y2) require.Equal(t, x, y1) // Undefined line (x1=x2, y1=y2): always return y1 - x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) + x = types.Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) require.Equal(t, x, y1) - x = Interpolate(x1, x1, y1, x1, y1) + x = types.Interpolate(x1, x1, y1, x1, y1) require.Equal(t, x, y1) } From c007643b65af172c7730f8fbfdc5721ba235f79a Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:48:50 -0700 Subject: [PATCH 25/42] ++ --- x/leverage/types/leverage_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/x/leverage/types/leverage_test.go b/x/leverage/types/leverage_test.go index 55ec25aaf5..eedfdbf021 100644 --- a/x/leverage/types/leverage_test.go +++ b/x/leverage/types/leverage_test.go @@ -171,7 +171,6 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustUp.rewardTokenPrice = sdk.MustNewDecFromStr("40.1") expensiveCollateralDustUp.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDustUp, 21, 1, 1, "expensive collateral dust with price up") - // is this a potential module drain? (discount repay) // collateral dust case, with high collateral token value and no rounding expensiveCollateralDustDown := baseCase() @@ -180,7 +179,6 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustDown.rewardTokenPrice = sdk.MustNewDecFromStr("39.9") expensiveCollateralDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDustDown, 20, 1, 1, "expensive collateral dust with price down") - // is this a potential module drain? (discount repay) // TODO: more rounding and dust scenarios. // borrowed/collat/reward high/low price and maybe high/low utoken From 865c5d3392f6fd08b8c6da0e9d638ebaf6689788 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 00:36:24 -0700 Subject: [PATCH 26/42] refactor and add detailed reasoning to liquidate computation --- x/leverage/types/leverage.go | 68 +++++++++++++++++-------------- x/leverage/types/leverage_test.go | 3 +- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/x/leverage/types/leverage.go b/x/leverage/types/leverage.go index 23ea10e614..2f67e4f104 100644 --- a/x/leverage/types/leverage.go +++ b/x/leverage/types/leverage.go @@ -34,14 +34,14 @@ func ComputeLiquidation( } // Start with the maximum possible repayment amount, as a decimal - repayDec := availableRepay.ToDec() - // Determine the base rewardDec amount that would result from maximum repayment - rewardDec := repayDec.Mul(repayTokenPrice).Mul(sdk.OneDec().Add(liquidationIncentive)).Quo(rewardTokenPrice) - // Determine the collateralDec burn amount that corresponds to base reward amount - collateralDec := rewardDec.Quo(uTokenExchangeRate) + 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 repayDec.IsZero() || rewardDec.IsZero() || collateralDec.IsZero() || closeFactor.IsZero() || borrowedValue.IsZero() { + if maxRepay.IsZero() || maxReward.IsZero() || maxCollateral.IsZero() || closeFactor.IsZero() || borrowedValue.IsZero() { return sdk.ZeroInt(), sdk.ZeroInt(), sdk.ZeroInt() } @@ -49,41 +49,49 @@ func ComputeLiquidation( ratio := sdk.OneDec() // Repaid value cannot exceed borrowed value times close factor ratio = sdk.MinDec(ratio, - borrowedValue.Mul(closeFactor).Quo(repayDec.Mul(repayTokenPrice)), + borrowedValue.Mul(closeFactor).Quo(maxRepay.Mul(repayTokenPrice)), ) // Collateral burned cannot exceed borrower's collateral ratio = sdk.MinDec(ratio, - availableCollateral.ToDec().Quo(collateralDec), + availableCollateral.ToDec().Quo(maxCollateral), ) // Base token reward cannot exceed available unreserved module balance ratio = sdk.MinDec(ratio, - availableReward.ToDec().Quo(rewardDec), + availableReward.ToDec().Quo(maxReward), ) // Catch edge cases ratio = sdk.MaxDec(ratio, sdk.ZeroDec()) - // Reduce all three values by the most severe limiting factor encountered - repayDec = repayDec.Mul(ratio) - collateralDec = collateralDec.Mul(ratio) - rewardDec = rewardDec.Mul(ratio) - - // No rounding has occurred yet. In dust scenarios, the limiting factor will be a clean integer - // and the other two will need rounding. Otherwise, all three outputs will need rounding. - - // REQUIREMENTS: - // - Prevent REPAY VALUE > COLLATERAL VALUE rounding attacks - // - Prevent REWARD VALUE > COLLATERAL VALUE rounding attacks - // - Eliminate COLLATERAL DUST (required for bad debt flag) - - // TODO: Satisfy these requirements - - // The amount of borrowed token the liquidator will repay is rounded up - tokenRepay = repayDec.Ceil().RoundInt() - // The amount of collateral uToken the borrower will lose is rounded down - collateralBurn = collateralDec.TruncateInt() - - // Liquidator base token reward is derived from collateral burn then rounded down + // 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. In the rare case that borrow dust and collateral + // dust co-occur and repay amount is the limiting factor, repay will round up, + // thus eliminating the borrow dust and creating a simpler scenario. + + // 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 } diff --git a/x/leverage/types/leverage_test.go b/x/leverage/types/leverage_test.go index eedfdbf021..40be1c45b3 100644 --- a/x/leverage/types/leverage_test.go +++ b/x/leverage/types/leverage_test.go @@ -180,6 +180,7 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDustDown, 20, 1, 1, "expensive collateral dust with price down") - // TODO: more rounding and dust scenarios. + // TODO: more rounding and dust scenarios, especially troublesome collateral dust + // and co-occurring dust // borrowed/collat/reward high/low price and maybe high/low utoken } From c2ceae91071d5f390307d6b1a6987d687629323d Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 00:48:14 -0700 Subject: [PATCH 27/42] final test cases --- x/leverage/types/leverage.go | 4 +--- x/leverage/types/leverage_test.go | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/x/leverage/types/leverage.go b/x/leverage/types/leverage.go index 2f67e4f104..3688e1703d 100644 --- a/x/leverage/types/leverage.go +++ b/x/leverage/types/leverage.go @@ -83,9 +83,7 @@ func ComputeLiquidation( // 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. In the rare case that borrow dust and collateral - // dust co-occur and repay amount is the limiting factor, repay will round up, - // thus eliminating the borrow dust and creating a simpler scenario. + // 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 diff --git a/x/leverage/types/leverage_test.go b/x/leverage/types/leverage_test.go index 40be1c45b3..8632ae240f 100644 --- a/x/leverage/types/leverage_test.go +++ b/x/leverage/types/leverage_test.go @@ -164,7 +164,7 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDust.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDust, 20, 1, 1, "expensive collateral dust") - // collateral dust case, with high collateral token value and no rounding + // collateral dust case, with high collateral token value rounds required repayment up expensiveCollateralDustUp := baseCase() expensiveCollateralDustUp.availableCollateral = sdk.NewInt(1) expensiveCollateralDustUp.repayTokenPrice = sdk.MustNewDecFromStr("2") @@ -172,7 +172,7 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustUp.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDustUp, 21, 1, 1, "expensive collateral dust with price up") - // collateral dust case, with high collateral token value and no rounding + // collateral dust case, with high collateral token value rounds required repayment up expensiveCollateralDustDown := baseCase() expensiveCollateralDustDown.availableCollateral = sdk.NewInt(1) expensiveCollateralDustDown.repayTokenPrice = sdk.MustNewDecFromStr("2") @@ -180,7 +180,21 @@ func TestComputeLiquidation(t *testing.T) { expensiveCollateralDustDown.liquidationIncentive = sdk.MustNewDecFromStr("0") runTestCase(expensiveCollateralDustDown, 20, 1, 1, "expensive collateral dust with price down") - // TODO: more rounding and dust scenarios, especially troublesome collateral dust - // and co-occurring dust - // borrowed/collat/reward high/low price and maybe high/low utoken + // 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") } From 47113cda047bd7702a4d370f1b68e45b50c6b8ab Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 00:57:48 -0700 Subject: [PATCH 28/42] remove redundant zero-price checks in PriceRatio --- x/leverage/keeper/oracle.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x/leverage/keeper/oracle.go b/x/leverage/keeper/oracle.go index abb09c0f92..6d4df27e31 100644 --- a/x/leverage/keeper/oracle.go +++ b/x/leverage/keeper/oracle.go @@ -73,16 +73,10 @@ func (k Keeper) PriceRatio(ctx sdk.Context, fromDenom, toDenom string) (sdk.Dec, if err != nil { return sdk.ZeroDec(), err } - if p1.IsZero() { - return sdk.ZeroDec(), types.ErrZeroValuePriceRatio.Wrap(fromDenom) - } p2, err := k.TokenPrice(ctx, toDenom) if err != nil { return sdk.ZeroDec(), err } - if p2.IsZero() { - return sdk.ZeroDec(), types.ErrZeroValuePriceRatio.Wrap(toDenom) - } // Price ratio > 1 if fromDenom is worth more than toDenom. return p1.Quo(p2), nil } From 1dea021805b986942383a323fa70b5f1d621ffd2 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:02:28 -0700 Subject: [PATCH 29/42] -- --- x/leverage/types/errors.go | 1 - 1 file changed, 1 deletion(-) diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 8a95a1d019..6032f3fe82 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -31,5 +31,4 @@ var ( ErrBlacklisted = sdkerrors.Register(ModuleName, 1121, "base denom blacklisted") ErrCollateralWeightZero = sdkerrors.Register(ModuleName, 1122, "token collateral weight is zero") ErrLiquidationInvalid = sdkerrors.Register(ModuleName, 1123, "liquidation invalid") - ErrZeroValuePriceRatio = sdkerrors.Register(ModuleName, 1124, "price ratio attempted with zero value token") ) From ee010030548ac1830751220cada3952dfa50f632 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 08:26:24 -0700 Subject: [PATCH 30/42] move liquidate computations back to keeper package --- x/leverage/keeper/interest.go | 6 +- x/leverage/keeper/keeper.go | 2 +- x/leverage/keeper/liquidate.go | 140 ++++++++++++++++++++- x/leverage/types/leverage.go | 137 -------------------- x/leverage/types/leverage_test.go | 200 ------------------------------ x/leverage/types/math.go | 29 ----- x/leverage/types/math_test.go | 44 ------- 7 files changed, 140 insertions(+), 418 deletions(-) delete mode 100644 x/leverage/types/leverage.go delete mode 100644 x/leverage/types/leverage_test.go delete mode 100644 x/leverage/types/math.go delete mode 100644 x/leverage/types/math_test.go diff --git a/x/leverage/keeper/interest.go b/x/leverage/keeper/interest.go index 103ff69050..ee3eacf483 100644 --- a/x/leverage/keeper/interest.go +++ b/x/leverage/keeper/interest.go @@ -26,7 +26,7 @@ func (k Keeper) DeriveBorrowAPY(ctx sdk.Context, denom string) sdk.Dec { utilization := k.SupplyUtilization(ctx, denom) if utilization.GTE(token.KinkUtilization) { - return types.Interpolate( + return Interpolate( utilization, // x token.KinkUtilization, // x1 token.KinkBorrowRate, // y1 @@ -36,7 +36,7 @@ func (k Keeper) DeriveBorrowAPY(ctx sdk.Context, denom string) sdk.Dec { } // utilization is between 0% and kink value - return types.Interpolate( + return Interpolate( utilization, // x sdk.ZeroDec(), // x1 token.BaseBorrowRate, // y1 @@ -104,7 +104,7 @@ func (k Keeper) AccrueAllInterest(ctx sdk.Context) error { // interest is accrued by continuous compound interest on each denom's Interest Scalar scalar := k.getInterestScalar(ctx, token.BaseDenom) // calculate e^(APY*time) - exponential := types.ApproxExponential(k.DeriveBorrowAPY(ctx, token.BaseDenom).Mul(yearsElapsed)) + exponential := ApproxExponential(k.DeriveBorrowAPY(ctx, token.BaseDenom).Mul(yearsElapsed)) // multiply interest scalar by e^(APY*time) if err := k.setInterestScalar(ctx, token.BaseDenom, scalar.Mul(exponential)); err != nil { return err diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 87364f8cca..b2ebc975c4 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -364,7 +364,7 @@ func (k Keeper) Liquidate( } // calculate Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances - baseRepay, collateralReward, baseReward, err = k.computeLiquidation( + baseRepay, collateralReward, baseReward, err = k.getLiquidationAmounts( ctx, liquidatorAddr, borrowerAddr, diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 7f211acae8..22ea71450c 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -6,10 +6,10 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -// computeLiquidation takes a repayment and reward denom proposed by a liquidator and calculates +// 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) computeLiquidation( +func (k Keeper) getLiquidationAmounts( ctx sdk.Context, liquidatorAddr sdk.AccAddress, targetAddr sdk.AccAddress, @@ -46,7 +46,7 @@ func (k Keeper) computeLiquidation( // get dynamic close factor params := k.GetParams(ctx) - closeFactor := types.ComputeCloseFactor( + closeFactor := ComputeCloseFactor( borrowedValue, liquidationThreshold, params.SmallLiquidationSize, @@ -68,7 +68,7 @@ func (k Keeper) computeLiquidation( exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) // compute final liquidation amounts - repay, burn, reward := types.ComputeLiquidation( + 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)), @@ -82,3 +82,135 @@ func (k Keeper) computeLiquidation( 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 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, +) (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 uses a borrower's borrowed value and liquidation threshold and +// some leverage module parameters to derive a dynamic close factor for liquidation. +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/types/leverage.go b/x/leverage/types/leverage.go deleted file mode 100644 index 3688e1703d..0000000000 --- a/x/leverage/types/leverage.go +++ /dev/null @@ -1,137 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// 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 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, -) (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 uses a borrower's borrowed value and liquidation threshold and -// some leverage module parameters to derive a dynamic close factor for liquidation. -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/types/leverage_test.go b/x/leverage/types/leverage_test.go deleted file mode 100644 index 8632ae240f..0000000000 --- a/x/leverage/types/leverage_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package types_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/umee-network/umee/v2/x/leverage/types" -) - -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 := types.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/types/math.go b/x/leverage/types/math.go deleted file mode 100644 index 49d7df3c89..0000000000 --- a/x/leverage/types/math.go +++ /dev/null @@ -1,29 +0,0 @@ -package types - -import sdk "github.com/cosmos/cosmos-sdk/types" - -// Interpolate takes a line defined by two points (xMin, yMin) and (xMax, yMax), then finds the y-value of the -// point on that line for an input x-value. It will return yMin if xMin = xMax (i.e. a vertical line). -// While this function is intended for interpolation (xMin < x < xMax), it works correctly even when x is outside -// that range or when xMin > xMax. -func Interpolate(x, xMin, yMin, xMax, yMax sdk.Dec) sdk.Dec { - if xMin.Equal(xMax) { - return yMin - } - slope := yMax.Sub(yMin).Quo(xMax.Sub(xMin)) - // y = y1 + m(x-x1) - return yMin.Add(x.Sub(xMin).Mul(slope)) -} - -// ApproxExponential is the taylor series expansion of e^x centered around x=0, truncated -// to the cubic term. It can be used with great accuracy to determine e^x when x is very small. -// Note that e^x = 1 + x/1! + x^2/2! + x^3 / 3! + ... -func ApproxExponential(x sdk.Dec) sdk.Dec { - sum := sdk.OneDec() // 1 - sum = sum.Add(x) // x / 1! - next := x.Mul(x) // x^2 - sum = sum.Add(next.QuoInt64(2)) // 2! - next = next.Mul(x) // x^3 - sum = sum.Add(next.QuoInt64(6)) // 3! - return sum // approximated e^x -} diff --git a/x/leverage/types/math_test.go b/x/leverage/types/math_test.go deleted file mode 100644 index 4003650498..0000000000 --- a/x/leverage/types/math_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package types_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/umee-network/umee/v2/x/leverage/types" -) - -func TestInterpolate(t *testing.T) { - // Define two points (x1,y1) and (x2,y2) - x1 := sdk.MustNewDecFromStr("3.0") - x2 := sdk.MustNewDecFromStr("6.0") - y1 := sdk.MustNewDecFromStr("11.1") - y2 := sdk.MustNewDecFromStr("17.4") - - // Sloped line, endpoint checks - x := types.Interpolate(x1, x1, y1, x2, y2) - require.Equal(t, x, y1) - x = types.Interpolate(x2, x1, y1, x2, y2) - require.Equal(t, x, y2) - - // Sloped line, point on segment - x = types.Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) - require.Equal(t, x, sdk.MustNewDecFromStr("13.2")) - - // Sloped line, point outside of segment - x = types.Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) - require.Equal(t, x, sdk.MustNewDecFromStr("9.0")) - - // Vertical line: always return y1 - x = types.Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) - require.Equal(t, x, y1) - x = types.Interpolate(x1, x1, y1, x1, y2) - require.Equal(t, x, y1) - - // Undefined line (x1=x2, y1=y2): always return y1 - x = types.Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) - require.Equal(t, x, y1) - x = types.Interpolate(x1, x1, y1, x1, y1) - require.Equal(t, x, y1) -} From 90037038582bd41df71e2497e88f96745639ba73 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 26 Jul 2022 08:26:44 -0700 Subject: [PATCH 31/42] move liquidate computations back to keeper package --- x/leverage/keeper/liquidate_test.go | 200 ++++++++++++++++++++++++++++ x/leverage/keeper/math.go | 29 ++++ x/leverage/keeper/math_test.go | 44 ++++++ 3 files changed, 273 insertions(+) create mode 100644 x/leverage/keeper/liquidate_test.go create mode 100644 x/leverage/keeper/math.go create mode 100644 x/leverage/keeper/math_test.go 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/math.go b/x/leverage/keeper/math.go new file mode 100644 index 0000000000..0a756d535b --- /dev/null +++ b/x/leverage/keeper/math.go @@ -0,0 +1,29 @@ +package keeper + +import sdk "github.com/cosmos/cosmos-sdk/types" + +// Interpolate takes a line defined by two points (xMin, yMin) and (xMax, yMax), then finds the y-value of the +// point on that line for an input x-value. It will return yMin if xMin = xMax (i.e. a vertical line). +// While this function is intended for interpolation (xMin < x < xMax), it works correctly even when x is outside +// that range or when xMin > xMax. +func Interpolate(x, xMin, yMin, xMax, yMax sdk.Dec) sdk.Dec { + if xMin.Equal(xMax) { + return yMin + } + slope := yMax.Sub(yMin).Quo(xMax.Sub(xMin)) + // y = y1 + m(x-x1) + return yMin.Add(x.Sub(xMin).Mul(slope)) +} + +// ApproxExponential is the taylor series expansion of e^x centered around x=0, truncated +// to the cubic term. It can be used with great accuracy to determine e^x when x is very small. +// Note that e^x = 1 + x/1! + x^2/2! + x^3 / 3! + ... +func ApproxExponential(x sdk.Dec) sdk.Dec { + sum := sdk.OneDec() // 1 + sum = sum.Add(x) // x / 1! + next := x.Mul(x) // x^2 + sum = sum.Add(next.QuoInt64(2)) // 2! + next = next.Mul(x) // x^3 + sum = sum.Add(next.QuoInt64(6)) // 3! + return sum // approximated e^x +} diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go new file mode 100644 index 0000000000..7772b69342 --- /dev/null +++ b/x/leverage/keeper/math_test.go @@ -0,0 +1,44 @@ +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 TestInterpolate(t *testing.T) { + // Define two points (x1,y1) and (x2,y2) + x1 := sdk.MustNewDecFromStr("3.0") + x2 := sdk.MustNewDecFromStr("6.0") + y1 := sdk.MustNewDecFromStr("11.1") + y2 := sdk.MustNewDecFromStr("17.4") + + // Sloped line, endpoint checks + x := keeper.Interpolate(x1, x1, y1, x2, y2) + require.Equal(t, x, y1) + x = keeper.Interpolate(x2, x1, y1, x2, y2) + require.Equal(t, x, y2) + + // Sloped line, point on segment + x = keeper.Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) + require.Equal(t, x, sdk.MustNewDecFromStr("13.2")) + + // Sloped line, point outside of segment + x = keeper.Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) + require.Equal(t, x, sdk.MustNewDecFromStr("9.0")) + + // Vertical line: always return y1 + x = keeper.Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) + require.Equal(t, x, y1) + x = keeper.Interpolate(x1, x1, y1, x1, y2) + require.Equal(t, x, y1) + + // Undefined line (x1=x2, y1=y2): always return y1 + x = keeper.Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) + require.Equal(t, x, y1) + x = keeper.Interpolate(x1, x1, y1, x1, y1) + require.Equal(t, x, y1) +} From 7696a8518d1059a5bb0baa951029d8fe342f9d10 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 2 Aug 2022 18:01:50 -0700 Subject: [PATCH 32/42] suggestion++ Co-authored-by: Robert Zaremba --- proto/umee/leverage/v1/tx.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index 62ea482c7c..95554057f0 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -130,7 +130,7 @@ message MsgLiquidateResponse { // 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 burned as a result of liquidation. + // 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. From b4113aa29d17979d19c53786a41f15fd6a1be4ac Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 2 Aug 2022 18:10:59 -0700 Subject: [PATCH 33/42] make proto-gen --- x/leverage/types/tx.pb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index f00c8582e7..9a3c7fda1f 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -557,7 +557,7 @@ type MsgLiquidateResponse struct { // to the module on behalf of the borrower. Repaid types.Coin `protobuf:"bytes,1,opt,name=repaid,proto3" json:"repaid"` // Collateral is the amount of the borrower's uToken collateral that - // was burned as a result of liquidation. + // 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. From b0187036acce6ad93aa67e3503a646b76e4ebbe7 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Tue, 2 Aug 2022 18:16:35 -0700 Subject: [PATCH 34/42] remove ctx from filterCoins --- x/leverage/keeper/filter.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x/leverage/keeper/filter.go b/x/leverage/keeper/filter.go index c09442b762..864c958273 100644 --- a/x/leverage/keeper/filter.go +++ b/x/leverage/keeper/filter.go @@ -5,7 +5,7 @@ import ( ) // filterCoins returns the subset of an sdk.Coins that meet a given condition -func (k Keeper) filterCoins(ctx sdk.Context, coins sdk.Coins, accept func(sdk.Coin) bool) sdk.Coins { +func (k Keeper) filterCoins(coins sdk.Coins, accept func(sdk.Coin) bool) sdk.Coins { filtered := sdk.Coins{} for _, c := range coins { if accept(c) { @@ -18,7 +18,6 @@ func (k Keeper) filterCoins(ctx sdk.Context, coins sdk.Coins, accept func(sdk.Co // 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( - ctx, coins, func(c sdk.Coin) bool { return k.validateAcceptedAsset(ctx, c) == nil From 3b1a3a084e07d42706cc8042513d67554c868bce Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:15:48 -0700 Subject: [PATCH 35/42] long example liquidate command --- x/leverage/client/cli/tx.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x/leverage/client/cli/tx.go b/x/leverage/client/cli/tx.go index 9e876c9fe4..8ca4507354 100644 --- a/x/leverage/client/cli/tx.go +++ b/x/leverage/client/cli/tx.go @@ -246,6 +246,17 @@ func GetCmdLiquidate() *cobra.Command { 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 From ed87cb0b14375ae1b50f27d446d6135e3239d6a6 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 19:21:19 -0700 Subject: [PATCH 36/42] package keeper_test -> keeper for math unit tests --- x/leverage/keeper/math_test.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index 7772b69342..b2351466a0 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -1,12 +1,10 @@ -package keeper_test +package keeper import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - - "github.com/umee-network/umee/v2/x/leverage/keeper" ) func TestInterpolate(t *testing.T) { @@ -17,28 +15,28 @@ func TestInterpolate(t *testing.T) { y2 := sdk.MustNewDecFromStr("17.4") // Sloped line, endpoint checks - x := keeper.Interpolate(x1, x1, y1, x2, y2) + x := Interpolate(x1, x1, y1, x2, y2) require.Equal(t, x, y1) - x = keeper.Interpolate(x2, x1, y1, x2, y2) + x = Interpolate(x2, x1, y1, x2, y2) require.Equal(t, x, y2) // Sloped line, point on segment - x = keeper.Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) + x = Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) require.Equal(t, x, sdk.MustNewDecFromStr("13.2")) // Sloped line, point outside of segment - x = keeper.Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) + x = Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) require.Equal(t, x, sdk.MustNewDecFromStr("9.0")) // Vertical line: always return y1 - x = keeper.Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) + x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) require.Equal(t, x, y1) - x = keeper.Interpolate(x1, x1, y1, x1, y2) + x = Interpolate(x1, x1, y1, x1, y2) require.Equal(t, x, y1) // Undefined line (x1=x2, y1=y2): always return y1 - x = keeper.Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) + x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) require.Equal(t, x, y1) - x = keeper.Interpolate(x1, x1, y1, x1, y1) + x = Interpolate(x1, x1, y1, x1, y1) require.Equal(t, x, y1) } From dacfe9f8445f8e6a554d91daf2273d32504ed6d8 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 19:57:40 -0700 Subject: [PATCH 37/42] suggestion++ Co-authored-by: Robert Zaremba --- x/leverage/keeper/keeper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index b2ebc975c4..fac6330095 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -363,7 +363,7 @@ func (k Keeper) Liquidate( return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // calculate Token repay, and uToken and Token reward amounts allowed by liquidation rules and available balances + // 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, From 4b1f543292cf97b85a6178c52b674b7bb73a50aa Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 19:58:11 -0700 Subject: [PATCH 38/42] suggestion++ Co-authored-by: Robert Zaremba --- x/leverage/keeper/liquidate.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index 22ea71450c..b6237afebd 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -97,14 +97,14 @@ func (k Keeper) getLiquidationAmounts( // - 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 sdk.Int, - availableCollateral sdk.Int, + availableRepay, + availableCollateral, availableReward sdk.Int, - repayTokenPrice sdk.Dec, - rewardTokenPrice sdk.Dec, - uTokenExchangeRate sdk.Dec, - liquidationIncentive sdk.Dec, - closeFactor sdk.Dec, + repayTokenPrice, + rewardTokenPrice, + uTokenExchangeRate, + liquidationIncentive, + closeFactor, borrowedValue sdk.Dec, ) (tokenRepay sdk.Int, collateralBurn sdk.Int, tokenReward sdk.Int) { // Prevent division by zero From 11432bff9a4dd4a686f67b88648d8f66897cf86e Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 20:08:28 -0700 Subject: [PATCH 39/42] fix test amounts --- x/leverage/client/tests/tests.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index 3268c703ec..f7bedbd573 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -234,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"), }, }, } From 50abc3365e420b6be36e80b0c6afdd6f029065ab Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:45:01 -0700 Subject: [PATCH 40/42] update CloseFactor comment --- x/leverage/keeper/liquidate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index b6237afebd..26813f5ffc 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -172,8 +172,8 @@ func ComputeLiquidation( return tokenRepay, collateralBurn, tokenReward } -// ComputeCloseFactor uses a borrower's borrowed value and liquidation threshold and -// some leverage module parameters to derive a dynamic close factor for liquidation. +// 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, From 0ed0234fb265b47147f1440de0edddbfe7086605 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 4 Aug 2022 07:46:11 -0700 Subject: [PATCH 41/42] ++ --- x/leverage/types/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/leverage/types/tx.go b/x/leverage/types/tx.go index 23a130d061..323b53626e 100644 --- a/x/leverage/types/tx.go +++ b/x/leverage/types/tx.go @@ -167,7 +167,7 @@ func (msg *MsgLiquidate) ValidateBasic() error { if err := validateSenderAndAsset(msg.Borrower, &msg.Repayment); err != nil { return err } - return sdk.ValidateDenom(msg.RewardDenom); err != nil { + if err := sdk.ValidateDenom(msg.RewardDenom); err != nil { return err } return validateSenderAndAsset(msg.Liquidator, &msg.Repayment) From df91850595c40db80c95b84abdce91d43435fffb Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Thu, 4 Aug 2022 07:49:11 -0700 Subject: [PATCH 42/42] ++ --- x/leverage/types/tx.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x/leverage/types/tx.go b/x/leverage/types/tx.go index 323b53626e..a0ea980782 100644 --- a/x/leverage/types/tx.go +++ b/x/leverage/types/tx.go @@ -170,7 +170,8 @@ func (msg *MsgLiquidate) ValidateBasic() error { if err := sdk.ValidateDenom(msg.RewardDenom); err != nil { return err } - return validateSenderAndAsset(msg.Liquidator, &msg.Repayment) + _, err := sdk.AccAddressFromBech32(msg.Liquidator) + return err } func (msg *MsgLiquidate) GetSigners() []sdk.AccAddress {