Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement fast-liquidation #2106

Merged
merged 33 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c52a440
save progress pre-refactor
toteki May 30, 2023
feb7cc7
godoc--
toteki Jun 5, 2023
30e1450
proto comment update
toteki Jun 21, 2023
3d786da
cleanup PR = empty function
toteki Jun 21, 2023
94d4ad7
Merge branch 'main' into adam/flashliq
toteki Jun 21, 2023
b55226a
make proto-all
toteki Jun 21, 2023
3b6ea4d
rename to FastLiquidate
toteki Jun 22, 2023
ce7087b
reoute returns empty string
toteki Jun 22, 2023
98907d5
changelog
toteki Jun 22, 2023
ac66cce
lint
toteki Jun 22, 2023
39d6f64
Merge branch 'main' into adam/flash
toteki Jun 22, 2023
ec064aa
add safety param to assertBorrowHealth
toteki Jun 22, 2023
026ac53
comment++
toteki Jun 22, 2023
e0a4ef9
++
toteki Jun 22, 2023
e6d67aa
refactor: postLiquidate
toteki Jun 22, 2023
59bbfad
Merge branch 'main' into adam/flash
toteki Jun 22, 2023
214cd78
implement FastLiquidate msg and adjust rounding
toteki Jun 23, 2023
84e37ed
cli and lint
toteki Jun 23, 2023
280df29
tests
toteki Jun 23, 2023
ae5c74a
Merge branch 'main' into adam/flash
toteki Jun 23, 2023
6b4ad62
cli doc++
toteki Jun 23, 2023
db5e8a3
80% limit and doc
toteki Jun 23, 2023
7d71421
rename to LeveragedLiquidate
toteki Jun 23, 2023
f611d60
++
toteki Jun 23, 2023
f7091b6
Update proto/umee/leverage/v1/tx.proto
toteki Jun 23, 2023
af1c8b4
rename++
toteki Jun 23, 2023
6404b62
rename++
toteki Jun 23, 2023
e4c7779
lint and docs
toteki Jun 23, 2023
9bb1f13
lint
toteki Jun 23, 2023
e333cff
Merge branch 'main' into adam/flash
robert-zaremba Jun 26, 2023
3eaf13d
Update x/leverage/README.md
toteki Jun 26, 2023
aa48275
Update x/leverage/client/cli/tx.go
toteki Jun 26, 2023
1925266
Update x/leverage/README.md
toteki Jun 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

- [2192](https://github.com/umee-network/umee/pull/2102) Add `MsgFastLiquidate` to `x/leverage`
- [2102](https://github.com/umee-network/umee/pull/2102) and [2106](https://github.com/umee-network/umee/pull/2106) Add `MsgLeveragedLiquidate` to `x/leverage`
- [2085](https://github.com/umee-network/umee/pull/2085) Add `inspect` query to leverage module, which msut be enabled on a node by running with `-l` liquidator query flag.
- [1952](https://github.com/umee-network/umee/pull/1952) Add `x/incentive` module.
- [2015](https://github.com/umee-network/umee/pull/2015), [2050](https://github.com/umee-network/umee/pull/2050) Add `x/ugov` module.
Expand Down
19 changes: 10 additions & 9 deletions proto/umee/leverage/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ service Msg {
// of the target's collateral.
rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse);

// FastLiquidate allows a user to repay a different user's borrowed coins in exchange for some
// of the target's collateral. For flash liquidations, the tokens to repay are borrowed instead of
// LeveragedLiquidate allows a user to repay a different user's borrowed coins in exchange for some
// of the target's collateral. For leveraged liquidations, the tokens to repay are borrowed instead of
// being taken from the liquidator's wallet, and the reward is immediately collateralized. Borrow
// limit checks for the liquidator are deferred until after the reward is collateralized, allowing
// this initial borrow to exceed the liquidator's borrow limit as long as it is healthy by the end
// of the transaction. Repay amount is calculated automatically, so the liquidator only specifies
// repay and reward token denoms.
rpc FastLiquidate(MsgFastLiquidate) returns (MsgFastLiquidateResponse);
// repay and reward token denoms. For safety, the liquidator cannot exceed 80% of their borrow limit when
// executing this transaction, instead of the regular 100%.
rpc LeveragedLiquidate(MsgLeveragedLiquidate) returns (MsgLeveragedLiquidateResponse);

// SupplyCollateral combines the Supply and Collateralize actions.
rpc SupplyCollateral(MsgSupplyCollateral) returns (MsgSupplyCollateralResponse);
Expand Down Expand Up @@ -149,15 +150,15 @@ message MsgLiquidate {
string reward_denom = 4;
}

// MsgFastLiquidate is the request structure for the FastLiquidate RPC.
message MsgFastLiquidate {
// MsgLeveragedLiquidate is the request structure for the LeveragedLiquidate RPC.
message MsgLeveragedLiquidate {
// Liquidator is the account address performing a liquidation and the signer
// of the message.
string liquidator = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// Borrower is the account whose borrow is being repaid, and collateral consumed,
// by the liquidation. It does not sign the message.
string borrower = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// RepayDenom is the base token that the liquidator will borrow and repay on behalf of
// RepayDenom is the base token that the liquidator will borrow in order to repay on behalf of
// the borrower.
string repay_denom = 3;
// RewardDenom is the uToken denom that the liquidator will receive as a liquidation reward
Expand Down Expand Up @@ -226,8 +227,8 @@ message MsgLiquidateResponse {
cosmos.base.v1beta1.Coin reward = 3 [(gogoproto.nullable) = false];
}

// MsgFastLiquidateResponse defines the Msg/FastLiquidate response type.
message MsgFastLiquidateResponse {
// MsgLeveragedLiquidateResponse defines the Msg/LeveragedLiquidate response type.
message MsgLeveragedLiquidateResponse {
// Repaid is the amount of base tokens that the liquidator borrowed and repaid
// to the module on behalf of the borrower.
cosmos.base.v1beta1.Coin repaid = 1 [(gogoproto.nullable) = false];
Expand Down
29 changes: 28 additions & 1 deletion x/leverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,34 @@ umeed start

## Messages

See [leverage tx proto](https://github.com/umee-network/umee/blob/main/proto/umee/leverage/v1/tx.proto#L11) for list of supported messages.
See [leverage tx proto](https://github.com/umee-network/umee/blob/main/proto/umee/leverage/v1/tx.proto#L11) for full documentation of supported messages.

Here are their basic functions:

### Supplying

- `MsgSupply`: Supplies base tokens to the module and receives uTokens in exchange. UTokens can later be used to withdraw.
- `MsgWithdraw`: Exchanges uTokens for the base tokens originally supplied, plus interest. UTokens withdrawn can be any combination of wallet uTokens (from supply) or collateral uTokens. When withdrawing collateral, borrow limit cannot be exceeded or the withdrawal will fail.
- `MsgMaxWithdraw`: Withdraws the maximum allowed amount of uTokens, respecting the user's borrow limit and the module's liquidity requirements, if any.

### Collateralizing

- `MsgCollateralize`: Sends uTokens to the module as the collateral. Collateral increases a user's borrow limit, but can be siezed in a liquidation if borrowed value exceeds a certain threshold above borrow limit due to price movements or interest owed. Collateral tokens still earn supply interest while collateralized.
- `MsgSupplyCollateral`: Combines `MsgSupply` and `MsgCollateralize`.
- `MsgDecollateralize`: Returns some collateral uTokens to wallet balance, without withdrawing base tokens. Borrow limit cannot be exceeded or the decollateralize will fail.

### Borrowing

- `MsgBorrow` Borrows base tokens from the module. Borrow limit cannot be exceeded or the transaction will fail.
- `MsgRepay` Repays borrowed tokens to the module, plus interest owed.

### Liquidation

- `MsgLiquidate` Liquidates a borrower whose borrowed value has exceeded their liquidation threshold (which is a certain amount above their borrow limit). The liquidator repays a portion of their debt using base tokens, and receives uTokens from the target's collateral, or the equivalent base tokens. The maximum liquidation amount is restricted by both the liquidator's specified amount and the borrower's liquidation eligibility, which may be partial.
- `MsgLeveragedLiquidate` Liquidates a borrower, but instead of repaying with base tokens from the liquidator wallet balance, moves the debt from the borrower to the liquidator and creates a new borrow position for the liquidator. Liquidator receives uTokens from the bororwer's collateral, and immediately collateralizes them to secure the liquidator positoin.

This transaction will succeed even if the liquidator could not afford to borrow the initial tokens (thanks to the new collateral position acquired from the borrower), as long as they are below 80% usage of their new borrow limit after the reward collateral is added.
The liquidator is left with a new borrow that they must pay off, and new collateral which can eventually be withdrawn.

## Update Registry Proposal

Expand Down
50 changes: 50 additions & 0 deletions x/leverage/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func GetTxCmd() *cobra.Command {
GetCmdMaxBorrow(),
GetCmdRepay(),
GetCmdLiquidate(),
GetCmdLeveragedLiquidate(),
GetCmdSupplyCollateral(),
)

Expand Down Expand Up @@ -316,6 +317,55 @@ $ umeed tx leverage liquidate %s 50000000uumee u/uumee --from mykey`,
return cmd
}

// GetCmdLeveragedLiquidate creates a Cobra command to generate or broadcast a
// transaction with a MsgLeveragedLiquidate message.
func GetCmdLeveragedLiquidate() *cobra.Command {
cmd := &cobra.Command{
Use: "lev-liquidate [borrower] [repay-denom] [reward-denom]",
Args: cobra.ExactArgs(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.

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`,
"umee1qqy7cst5qm83ldupph2dcq0wypprkfpc9l3jg2",
),
),

RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

borrowerAddr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}

repayDenom := args[1]
rewardDenom := args[2]

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

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

// GetCmdSupplyCollateral creates a Cobra command to generate or broadcast a
// transaction with a MsgSupply message.
func GetCmdSupplyCollateral() *cobra.Command {
Expand Down
11 changes: 11 additions & 0 deletions x/leverage/client/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,16 @@ func (s *IntegrationTests) TestLeverageScenario() {
},
ExpectedErr: types.ErrLiquidationIneligible,
}
leveragedLiquidate := itestsuite.TestTransaction{
Name: "liquidate",
Command: cli.GetCmdLeveragedLiquidate(),
Args: []string{
val.Address.String(),
"uumee", // borrower attempts to liquidate itself, but is ineligible
"uumee",
},
ExpectedErr: types.ErrLiquidationIneligible,
}

repay := itestsuite.TestTransaction{
Name: "repay",
Expand Down Expand Up @@ -483,6 +493,7 @@ func (s *IntegrationTests) TestLeverageScenario() {
// These transactions run after nonzero queries are finished
s.RunTestTransactions(
liquidate,
leveragedLiquidate,
repay,
removeCollateral,
withdraw,
Expand Down
19 changes: 16 additions & 3 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (
// unless the remaining collateral is enough to cover all borrows.
// This should be checked in msg_server.go at the end of any transaction which is restricted
// by borrow limits, i.e. Borrow, Decollateralize, Withdraw, MaxWithdraw.
func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress) error {
// MaxUsage sets the maximum percent of a user's borrow limit that can be in use: set to 1
// to allow up to 100% borrow limit, or a lower value (e.g. 0.9) if a transaction should fail
// if a safety margin is desired (e.g. <90% borrow limit).
func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddress, maxUsage sdk.Dec) error {
borrowed := k.GetBorrowerBorrows(ctx, borrowerAddr)
collateral := k.GetBorrowerCollateral(ctx, borrowerAddr)

Expand All @@ -26,9 +29,9 @@ func (k Keeper) assertBorrowerHealth(ctx sdk.Context, borrowerAddr sdk.AccAddres
if err != nil {
return err
}
if value.GT(limit) {
if value.GT(limit.Mul(maxUsage)) {
return types.ErrUndercollaterized.Wrapf(
"borrowed: %s, limit: %s", value, limit)
"borrowed: %s, limit: %s, max usage %s", value, limit, maxUsage)
}
return nil
}
Expand All @@ -52,6 +55,16 @@ func (k Keeper) repayBorrow(ctx sdk.Context, fromAddr, borrowAddr sdk.AccAddress
return k.setBorrow(ctx, borrowAddr, k.GetBorrow(ctx, borrowAddr, repay.Denom).Sub(repay))
}

// moveBorrow transfers a debt from fromAddr to toAddr without moving any tokens. This occurs during
// fast liquidations, where a liquidator takes on a borrower's debt.
func (k Keeper) moveBorrow(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, repay sdk.Coin) error {
err := k.setBorrow(ctx, fromAddr, k.GetBorrow(ctx, fromAddr, repay.Denom).Sub(repay))
if err != nil {
return err
}
return k.setBorrow(ctx, toAddr, k.GetBorrow(ctx, toAddr, repay.Denom).Add(repay))
}

// setBorrow sets the amount borrowed by an address in a given denom.
// If the amount is zero, any stored value is cleared.
func (k Keeper) setBorrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk.Coin) error {
Expand Down
10 changes: 10 additions & 0 deletions x/leverage/keeper/collateral.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func (k Keeper) decollateralize(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, sdk.NewCoins(uToken))
}

// moveCollateral moves collateral from one address to another while keeping the uTokens in the module.
// It occurs during fast liquidations.
func (k Keeper) moveCollateral(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, uToken sdk.Coin) error {
err := k.setCollateral(ctx, fromAddr, k.GetCollateral(ctx, fromAddr, uToken.Denom).Sub(uToken))
if err != nil {
return err
}
return k.setCollateral(ctx, toAddr, k.GetCollateral(ctx, toAddr, uToken.Denom).Add(uToken))
}

// GetTotalCollateral returns an sdk.Coin representing how much of a given uToken
// the x/leverage module account currently holds as collateral. Non-uTokens return zero.
func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Coin {
Expand Down
56 changes: 42 additions & 14 deletions x/leverage/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ func (k Keeper) Liquidate(
requestedRepay,
rewardDenom,
directLiquidation,
false,
)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
Expand All @@ -355,16 +356,8 @@ func (k Keeper) Liquidate(
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}

// if borrower's collateral has reached zero, mark any remaining borrows as bad debt
if err := k.checkBadDebt(ctx, borrowerAddr); err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}

// finally, force incentive module to update bond and unbonding amounts if required,
// by ending existing unbondings early or instantly unbonding some bonded tokens
// until bonded + unbonding for the account is not greater than its collateral amount
err = k.reduceBondTo(ctx, borrowerAddr, k.GetCollateral(ctx, borrowerAddr, uTokenLiquidate.Denom))
if err != nil {
// check for bad debt and trigger forced unbond hooks
if err := k.postLiquidate(ctx, borrowerAddr, uTokenLiquidate.Denom); err != nil {
return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
}

Expand All @@ -375,9 +368,44 @@ func (k Keeper) Liquidate(
return tokenRepay, uTokenLiquidate, uTokenLiquidate, nil
}

// FastLiquidate
func (k Keeper) FastLiquidate(
_ sdk.Context, _, _ sdk.AccAddress, _, _ string,
// LeveragedLiquidate
func (k Keeper) LeveragedLiquidate(
ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, repayDenom, rewardDenom string,
) (repaid sdk.Coin, reward sdk.Coin, err error) {
return sdk.Coin{}, sdk.Coin{}, err
if err := k.validateAcceptedDenom(ctx, repayDenom); err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
if err := k.validateAcceptedDenom(ctx, rewardDenom); err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
uRewardDenom := types.ToUTokenDenom(rewardDenom)

tokenRepay, uTokenReward, _, err := k.getLiquidationAmounts(
toteki marked this conversation as resolved.
Show resolved Hide resolved
ctx,
liquidatorAddr,
borrowerAddr,
sdk.NewCoin(repayDenom, sdk.OneInt()), // amount is ignored for LeveragedLiquidate
rewardDenom,
false,
true,
)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}
if tokenRepay.IsZero() || uTokenReward.IsZero() {
return sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRepayZero
}

// directly move debt from borrower to liquidator without transferring any tokens between accounts
if err := k.moveBorrow(ctx, borrowerAddr, liquidatorAddr, tokenRepay); err != nil {
toteki marked this conversation as resolved.
Show resolved Hide resolved
return sdk.Coin{}, sdk.Coin{}, err
}

// directly move collateral from borrower to liquidator while keeping it collateralized
if err := k.moveCollateral(ctx, borrowerAddr, liquidatorAddr, uTokenReward); err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}

// check for bad debt and trigger forced unbond hooks
return tokenRepay, uTokenReward, k.postLiquidate(ctx, borrowerAddr, uRewardDenom)
toteki marked this conversation as resolved.
Show resolved Hide resolved
}
Loading