Skip to content

Commit

Permalink
feat: max repay on MsgLeveragedLiquidate (#2242)
Browse files Browse the repository at this point in the history
* proto and cli

* tx behavior

* lint

* lint

* fix e2d

* nonzero tests
  • Loading branch information
toteki authored Sep 14, 2023
1 parent ab31779 commit 64171d4
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 94 deletions.
7 changes: 7 additions & 0 deletions proto/umee/leverage/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ service Msg {
// executing this transaction, instead of the regular 100%. Also allows repayment and reward denoms to
// be left blank - if not specified, the module will automatically select the first (alphabetically by denom)
// borrow and/or collateral on the target account and the proceed normally.
// After v6.0, includes a MaxRepay field which limits repay value in USD. To prevent dust exploits, this
// value cannot be below $1.00
rpc LeveragedLiquidate(MsgLeveragedLiquidate) returns (MsgLeveragedLiquidateResponse);

// SupplyCollateral combines the Supply and Collateralize actions.
Expand Down Expand Up @@ -174,6 +176,11 @@ message MsgLeveragedLiquidate {
// RewardDenom is the uToken denom that the liquidator will receive as a liquidation reward
// and immediately collateralize.
string reward_denom = 4;
// MaxRepay optionally limits the USD value to repay. If specified, this cannot be below $1.00
string max_repay = 5 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

// MsgSupplyCollateral represents a user's request to supply and collateralize assets to the module.
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/e2e_leverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (s *E2ETest) leverageLiquidate(addr, target sdk.AccAddress, repayDenom stri
}

func (s *E2ETest) leverageLeveragedLiquidate(addr, target sdk.AccAddress, repay, reward string) {
s.mustSucceedTx(leveragetypes.NewMsgLeveragedLiquidate(addr, target, repay, reward))
s.mustSucceedTx(leveragetypes.NewMsgLeveragedLiquidate(addr, target, repay, reward, sdk.ZeroDec()))
}

func (s *E2ETest) TestLeverageBasics() {
Expand Down
13 changes: 9 additions & 4 deletions x/leverage/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,19 +321,19 @@ $ umeed tx leverage liquidate %s 50000000uumee u/uumee --from mykey`,
// transaction with a MsgLeveragedLiquidate message.
func LeveragedLiquidate() *cobra.Command {
cmd := &cobra.Command{
Use: "lev-liquidate [borrower] [repay-denom] [reward-denom]",
Use: "lev-liquidate [borrower] [repay-denom] [reward-denom] [max-repay]",
Args: cobra.RangeArgs(1, 3),
Short: "Liquidates by moving borrower debt to the liquidator and immediately collateralizes the reward.",
Long: strings.TrimSpace(
fmt.Sprintf(`
Borrow tokens to liquidate a borrower's debt and immediately collateralize the reward.
Will attempt to repay the maximum amount allowed by the targeted borrower's debt and collateral positions.
Will attempt to repay the max amount allowed by the targeted borrower's position unless max repay (USD) is specified.
The transaction will fail if the liquidator, with new borrow and collateral positions, would be above 0.8 borrow limit.
Example:
$ umeed tx leverage lev-liquidate %s uumee uumee --from mykey`,
$ umeed tx leverage lev-liquidate %s uumee uumee 123.4 --from mykey`,
"umee1qqy7cst5qm83ldupph2dcq0wypprkfpc9l3jg2",
),
),
Expand All @@ -350,6 +350,7 @@ $ umeed tx leverage lev-liquidate %s uumee uumee --from mykey`,
}

var repayDenom, rewardDenom string
maxRepay := sdk.ZeroDec()
if len(args) > 1 {
repayDenom = args[1]
}
Expand All @@ -358,7 +359,11 @@ $ umeed tx leverage lev-liquidate %s uumee uumee --from mykey`,
rewardDenom = args[2]
}

msg := types.NewMsgLeveragedLiquidate(clientCtx.GetFromAddress(), borrowerAddr, repayDenom, rewardDenom)
if len(args) > 3 {
maxRepay = sdk.MustNewDecFromStr(args[3])
}

msg := types.NewMsgLeveragedLiquidate(clientCtx.GetFromAddress(), borrowerAddr, repayDenom, rewardDenom, maxRepay)
if err = msg.ValidateBasic(); err != nil {
return err
}
Expand Down
17 changes: 14 additions & 3 deletions x/leverage/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,12 +463,13 @@ func (k Keeper) Liquidate(

// LeveragedLiquidate
func (k Keeper) LeveragedLiquidate(
ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, repayDenom, rewardDenom string,
ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress,
repayDenom, rewardDenom string, maxRepay sdk.Dec,
) (repaid sdk.Coin, reward sdk.Coin, err error) {
// If the message did not specify repay or reward denoms, select one arbitrarily (first in
// denom alphabetical order) from borrower position. Then proceed normally with the transaction.
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)
if repayDenom == "" {
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)
if !borrowed.IsZero() {
repayDenom = borrowed[0].Denom
}
Expand All @@ -488,11 +489,21 @@ func (k Keeper) LeveragedLiquidate(
}
uRewardDenom := coin.ToUTokenDenom(rewardDenom)

// If the message did not specify max repay, attempt to liquidate the full amount
maxRepayCoin := sdk.NewCoin(repayDenom, borrowed.AmountOf(repayDenom))
if !maxRepay.IsZero() {
// Otherwise, convert from max USD repay to token
maxRepayCoin, err = k.TokenWithValue(ctx, repayDenom, maxRepay, types.PriceModeSpot)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
}

tokenRepay, uTokenReward, _, err := k.getLiquidationAmounts(
ctx,
liquidatorAddr,
borrowerAddr,
sdk.NewCoin(repayDenom, sdk.OneInt()), // amount is ignored for LeveragedLiquidate
maxRepayCoin,
rewardDenom,
false,
true,
Expand Down
4 changes: 2 additions & 2 deletions x/leverage/keeper/liquidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ func (k Keeper) getLiquidationAmounts(
// for traditional liquidations, liquidator account balance limits repayment
availableRepay := k.bankKeeper.SpendableCoins(ctx, liquidatorAddr).AmountOf(repayDenom)
maxRepay = sdk.MinInt(maxRepay, availableRepay)
// maximum requested by liquidator
maxRepay = sdk.MinInt(maxRepay, requestedRepay.Amount)
}
// maximum requested by liquidator
maxRepay = sdk.MinInt(maxRepay, requestedRepay.Amount)
maxRepay = sdk.MinInt(maxRepay, maxRepayAfterCloseFactor) // close factor

// compute final liquidation amounts
Expand Down
3 changes: 2 additions & 1 deletion x/leverage/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,8 @@ func (s msgServer) LeveragedLiquidate(
return nil, err
}

repaid, reward, err := s.keeper.LeveragedLiquidate(ctx, liquidator, borrower, msg.RepayDenom, msg.RewardDenom)
repaid, reward, err := s.keeper.LeveragedLiquidate(
ctx, liquidator, borrower, msg.RepayDenom, msg.RewardDenom, msg.MaxRepay)
if err != nil {
return nil, err
}
Expand Down
76 changes: 72 additions & 4 deletions x/leverage/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2145,9 +2145,21 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require()

// create and fund a liquidator which supplies plenty of UMEE and ATOM to the module
liquidator := s.newAccount(coin.New(umeeDenom, 10000_000000), coin.New(atomDenom, 10000_000000))
s.supply(liquidator, coin.New(umeeDenom, 10000_000000), coin.New(atomDenom, 10000_000000))
s.collateralize(liquidator, coin.New("u/"+umeeDenom, 10000_000000), coin.New("u/"+atomDenom, 10000_000000))
liquidator := s.newAccount(
coin.New(umeeDenom, 10000_000000),
coin.New(atomDenom, 10000_000000),
coin.New(daiDenom, 10000_000000),
)
s.supply(liquidator,
coin.New(umeeDenom, 10000_000000),
coin.New(atomDenom, 10000_000000),
coin.New(daiDenom, 10000_000000),
)
s.collateralize(liquidator,
coin.New("u/"+umeeDenom, 10000_000000),
coin.New("u/"+atomDenom, 10000_000000),
coin.New("u/"+daiDenom, 10000_000000),
)

// create a healthy borrower
healthyBorrower := s.newAccount(coin.New(umeeDenom, 100_000000))
Expand Down Expand Up @@ -2183,12 +2195,27 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
// artificially borrow just barely above liquidation threshold to simulate interest accruing
s.forceBorrow(closeBorrower, coin.New(umeeDenom, 106_000000))

// creates a borrower with $10 PAIRED (stablecoin) collateral which will have a close factor = 1
daiBorrower := s.newAccount(coin.New(pairedDenom, 10_000000))
s.supply(daiBorrower, coin.New(pairedDenom, 10_000000))
s.collateralize(daiBorrower, coin.New("u/"+pairedDenom, 10_000000))
// artificially borrow an amount much greater than liquidation threshold
s.forceBorrow(daiBorrower, coin.New(pairedDenom, 7_000000))

// creates another realistic borrower with 400 UMEE collateral which will have a close factor < 1
closeBorrower2 := s.newAccount(coin.New(umeeDenom, 400_000000))
s.supply(closeBorrower2, coin.New(umeeDenom, 400_000000))
s.collateralize(closeBorrower2, coin.New("u/"+umeeDenom, 400_000000))
// artificially borrow just barely above liquidation threshold to simulate interest accruing
s.forceBorrow(closeBorrower2, coin.New(umeeDenom, 106_000000))

tcs := []struct {
msg string
liquidator sdk.AccAddress
borrower sdk.AccAddress
repayDenom string
rewardDenom string
maxRepay sdk.Dec
expectedRepay sdk.Coin
expectedReward sdk.Coin
err error
Expand All @@ -2199,6 +2226,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
healthyBorrower,
atomDenom,
atomDenom,
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrLiquidationIneligible,
Expand All @@ -2208,6 +2236,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
"u/" + umeeDenom,
umeeDenom,
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrUToken,
Expand All @@ -2217,6 +2246,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
umeeDenom,
"u/" + umeeDenom,
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrUToken,
Expand All @@ -2226,6 +2256,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
"foo",
umeeDenom,
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrNotRegisteredToken,
Expand All @@ -2235,6 +2266,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
atomDenom,
"foo",
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrNotRegisteredToken,
Expand All @@ -2244,6 +2276,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
atomDenom,
atomDenom,
sdk.ZeroDec(),
sdk.Coin{},
sdk.Coin{},
types.ErrLiquidationRepayZero,
Expand All @@ -2253,6 +2286,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
atomBorrower,
atomDenom,
atomDenom,
sdk.ZeroDec(),
coin.New(atomDenom, 500_000000),
coin.New("u/"+atomDenom, 550_000000),
nil,
Expand All @@ -2262,6 +2296,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
umeeBorrower,
umeeDenom,
umeeDenom,
sdk.ZeroDec(),
coin.New(umeeDenom, 100_000000),
coin.New("u/"+umeeDenom, 110_000000),
nil,
Expand All @@ -2271,6 +2306,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
complexBorrower,
umeeDenom,
atomDenom,
sdk.ZeroDec(),
coin.New(umeeDenom, 30_000000),
coin.New("u/"+atomDenom, 3_527933),
nil,
Expand All @@ -2280,9 +2316,40 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
closeBorrower,
"",
"",
sdk.ZeroDec(),
coin.New(umeeDenom, 8_150541),
coin.New("u/"+umeeDenom, 8_965596),
nil,
}, {
"stable borrower with nonzero max repay",
liquidator,
daiBorrower,
"",
"",
sdk.OneDec(),
coin.New(pairedDenom, 1_000000),
coin.New("u/"+pairedDenom, 1_100000),
nil,
}, {
"stable borrower with nonzero max repay (again)",
liquidator,
daiBorrower,
"",
"",
sdk.MustNewDecFromStr("2.0"),
coin.New(pairedDenom, 2_000000),
coin.New("u/"+pairedDenom, 2_200000),
nil,
}, {
"umee borrower with nonzero max repay and close factor < 1",
liquidator,
closeBorrower2,
"",
"",
sdk.MustNewDecFromStr("10.00"),
coin.New(umeeDenom, 2_375296), // $10 of UMEE at price $4.21
coin.New("u/"+umeeDenom, 2_612826),
nil,
},
}

Expand All @@ -2292,6 +2359,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
Borrower: tc.borrower.String(),
RepayDenom: tc.repayDenom,
RewardDenom: tc.rewardDenom,
MaxRepay: tc.maxRepay,
}
if tc.err != nil {
_, err := srv.LeveragedLiquidate(ctx, msg)
Expand Down Expand Up @@ -2327,7 +2395,7 @@ func (s *IntegrationTestSuite) TestMsgLeveragedLiquidate() {
liCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator)
liBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator)

// verify the output of fast-liquidate function
// verify the output of leveraged-liquidate function
resp, err := srv.LeveragedLiquidate(ctx, msg)
require.NoError(err, tc.msg)
require.Equal(tc.expectedRepay.String(), resp.Repaid.String(), tc.msg)
Expand Down
11 changes: 10 additions & 1 deletion x/leverage/types/tx.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package types

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/umee-network/umee/v6/util/checkers"
Expand Down Expand Up @@ -255,13 +257,17 @@ func (msg *MsgLiquidate) GetSignBytes() []byte {
return sdk.MustSortJSON(bz)
}

func NewMsgLeveragedLiquidate(liquidator, borrower sdk.AccAddress, repayDenom, rewardDenom string,
func NewMsgLeveragedLiquidate(
liquidator, borrower sdk.AccAddress,
repayDenom, rewardDenom string,
maxRepay sdk.Dec,
) *MsgLeveragedLiquidate {
return &MsgLeveragedLiquidate{
Liquidator: liquidator.String(),
Borrower: borrower.String(),
RepayDenom: repayDenom,
RewardDenom: rewardDenom,
MaxRepay: maxRepay,
}
}

Expand All @@ -279,6 +285,9 @@ func (msg *MsgLeveragedLiquidate) ValidateBasic() error {
return err
}
}
if !msg.MaxRepay.IsZero() && msg.MaxRepay.LT(sdk.OneDec()) {
return fmt.Errorf("nonzero max repay %s is less than one", msg.MaxRepay)
}
_, err := sdk.AccAddressFromBech32(msg.Borrower)
if err != nil {
return err
Expand Down
Loading

0 comments on commit 64171d4

Please sign in to comment.