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: add MsgMaxBorrow #1690

Merged
merged 11 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- [1654](https://github.com/umee-network/umee/pull/1654) Leverage historacle integration.
- [1685](https://github.com/umee-network/umee/pull/1685) Add medians param to Token registry.
- [1683](https://github.com/umee-network/umee/pull/1683) Add MaxBorrow query and allow returning all denoms from MaxWithdraw.
- [1690](https://github.com/umee-network/umee/pull/1690) Add MaxBorrow message type.

## [v3.3.0](https://github.com/umee-network/umee/releases/tag/v3.3.0) - 2022-12-20

Expand Down
18 changes: 18 additions & 0 deletions proto/umee/leverage/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ service Msg {
// Borrow allows a user to borrow tokens from the module if they have sufficient collateral.
rpc Borrow(MsgBorrow) returns (MsgBorrowResponse);

// MaxBorrow allows a user to borrow the maximum amount of tokens their collateral will allow.
rpc MaxBorrow(MsgMaxBorrow) returns (MsgMaxBorrowResponse);

// Repay allows a user to repay previously borrowed tokens and interest.
rpc Repay(MsgRepay) returns (MsgRepayResponse);

Expand Down Expand Up @@ -99,6 +102,15 @@ message MsgBorrow {
cosmos.base.v1beta1.Coin asset = 2 [(gogoproto.nullable) = false];
}

// MsgMaxBorrow represents a user's request to borrow a base asset type
// from the module, using the maximum available amount.
message MsgMaxBorrow {
// Borrower is the account address taking a loan and the signer
// of the message.
string borrower = 1;
string denom = 2;
}

// MsgRepay represents a user's request to repay a borrowed base asset
// type to the module.
message MsgRepay {
Expand Down Expand Up @@ -162,6 +174,12 @@ message MsgDecollateralizeResponse {}
// MsgBorrowResponse defines the Msg/Borrow response type.
message MsgBorrowResponse {}

// MsgMaxBorrowResponse defines the Msg/MaxBorrow response type.
message MsgMaxBorrowResponse {
// Borrowed is the amount of tokens borrowed.
cosmos.base.v1beta1.Coin borrowed = 1 [(gogoproto.nullable) = false];
}

// MsgRepayResponse defines the Msg/Repay response type.
message MsgRepayResponse {
// Repaid is the amount of base tokens repaid to the module.
Expand Down
2 changes: 2 additions & 0 deletions x/leverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Users have the following actions available to them:

Interest will accrue on borrows for as long as they are not paid off, with the amount owed increasing at a rate of the asset's [Borrow APY](#borrow-apy).

- `MsgMaxBorrow` borrows assets by automatically calculating the maximum amount that can be borrowed.

- `MsgRepay` assets of a borrowed type, directly reducing the amount owed.

Repayments that exceed a borrower's amount owed in the selected denomination succeed at paying the reduced amount rather than failing outright.
Expand Down
25 changes: 25 additions & 0 deletions x/leverage/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func GetTxCmd() *cobra.Command {
GetCmdCollateralize(),
GetCmdDecollateralize(),
GetCmdBorrow(),
GetCmdMaxBorrow(),
GetCmdRepay(),
GetCmdLiquidate(),
GetCmdSupplyCollateral(),
Expand Down Expand Up @@ -213,6 +214,30 @@ func GetCmdBorrow() *cobra.Command {
return cmd
}

// GetCmdMaxBorrow creates a Cobra command to generate or broadcast a
// transaction with a MsgBorrow message.
func GetCmdMaxBorrow() *cobra.Command {
cmd := &cobra.Command{
Use: "max-borrow [denom]",
Args: cobra.ExactArgs(1),
Short: "Borrow the maximum acceptable amount of a supported asset",
adamewozniak marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

msg := types.NewMsgMaxBorrow(clientCtx.GetFromAddress(), args[0])

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

flags.AddTxFlagsToCmd(cmd)

return cmd
}

// GetCmdRepay creates a Cobra command to generate or broadcast a
// transaction with a MsgRepay message.
func GetCmdRepay() *cobra.Command {
Expand Down
36 changes: 23 additions & 13 deletions x/leverage/client/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,16 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
"borrow",
cli.GetCmdBorrow(),
[]string{
"249uumee", // produces a borrowed amount of 250 due to rounding
"150uumee",
},
nil,
}

maxborrow := testTransaction{
"max-borrow",
cli.GetCmdMaxBorrow(),
[]string{
"uumee", // should borrow up to the max of 250 uumee, which will become 251 due to rounding
},
nil,
}
Expand Down Expand Up @@ -253,13 +262,13 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
&types.QueryAccountBalancesResponse{},
&types.QueryAccountBalancesResponse{
Supplied: sdk.NewCoins(
sdk.NewInt64Coin(appparams.BondDenom, 1000),
sdk.NewInt64Coin(appparams.BondDenom, 1001),
),
Collateral: sdk.NewCoins(
sdk.NewInt64Coin(types.ToUTokenDenom(appparams.BondDenom), 1000),
),
Borrowed: sdk.NewCoins(
sdk.NewInt64Coin(appparams.BondDenom, 250),
sdk.NewInt64Coin(appparams.BondDenom, 251),
),
},
},
Expand All @@ -275,16 +284,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
// (1000 / 1000000) * 34.21 = 0.03421
SuppliedValue: sdk.MustNewDecFromStr("0.03421"),
// (1000 / 1000000) * 34.21 = 0.03421
CollateralValue: sdk.MustNewDecFromStr("0.03421"),
// (250 / 1000000) * 34.21 = 0.0085525
BorrowedValue: sdk.MustNewDecFromStr("0.0085525"),
// (1000 / 1000000) * 34.21 * 0.25 = 0.0085525
BorrowLimit: sdk.MustNewDecFromStr("0.0085525"),
// (1000 / 1000000) * 0.25 * 34.21 = 0.0085525
LiquidationThreshold: sdk.MustNewDecFromStr("0.0085525"),
// (1001 / 1000000) * 34.21 = 0.03424421
SuppliedValue: sdk.MustNewDecFromStr("0.03424421"),
// (1001 / 1000000) * 34.21 = 0.03424421
CollateralValue: sdk.MustNewDecFromStr("0.03424421"),
// (251 / 1000000) * 34.21 = 0.00858671
BorrowedValue: sdk.MustNewDecFromStr("0.00858671"),
// (1001 / 1000000) * 34.21 * 0.25 = 0.0085610525
BorrowLimit: sdk.MustNewDecFromStr("0.0085610525"),
// (1001 / 1000000) * 0.25 * 34.21 = 0.0085610525
LiquidationThreshold: sdk.MustNewDecFromStr("0.0085610525"),
},
},
{
Expand Down Expand Up @@ -442,6 +451,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
addCollateral,
supplyCollateral,
borrow,
maxborrow,
)

// These queries run while the supplying and borrowing is active to produce nonzero output
Expand Down
62 changes: 62 additions & 0 deletions x/leverage/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,68 @@ func (s msgServer) Borrow(
return &types.MsgBorrowResponse{}, err
}

func (s msgServer) MaxBorrow(
goCtx context.Context,
msg *types.MsgMaxBorrow,
) (*types.MsgMaxBorrowResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

borrowerAddr, err := sdk.AccAddressFromBech32(msg.Borrower)
if err != nil {
return nil, err
}

currentMaxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom, false)
if err != nil {
return nil, err
}
historicMaxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom, true)
if err != nil {
return nil, err
}

maxBorrow := sdk.NewCoin(
msg.Denom,
sdk.MinInt(currentMaxBorrow.Amount, historicMaxBorrow.Amount),
)
if maxBorrow.IsZero() {
return nil, types.ErrMaxBorrowZero
}

if err := s.keeper.Borrow(ctx, borrowerAddr, maxBorrow); err != nil {
return nil, err
}

// Fail here if borrower ends up over their borrow limit under current or historic prices
err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr)
if err != nil {
return nil, err
}

// Check MaxSupplyUtilization after transaction
if err = s.keeper.checkSupplyUtilization(ctx, maxBorrow.Denom); err != nil {
return nil, err
}

// Check MinCollateralLiquidity is still satisfied after the transaction
if err = s.keeper.checkCollateralLiquidity(ctx, maxBorrow.Denom); err != nil {
return nil, err
}

s.keeper.Logger(ctx).Debug(
"assets borrowed",
"borrower", msg.Borrower,
"amount", maxBorrow.String(),
)
err = ctx.EventManager().EmitTypedEvent(&types.EventBorrow{
Borrower: msg.Borrower,
Asset: maxBorrow,
})
return &types.MsgMaxBorrowResponse{
Borrowed: maxBorrow,
}, err
}

func (s msgServer) Repay(
goCtx context.Context,
msg *types.MsgRepay,
Expand Down
132 changes: 132 additions & 0 deletions x/leverage/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,138 @@ func (s *IntegrationTestSuite) TestMsgBorrow() {
}
}

func (s *IntegrationTestSuite) TestMsgMaxBorrow() {
type testCase struct {
msg string
addr sdk.AccAddress
coin sdk.Coin
err error
}

app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require()

// create and fund a supplier which supplies 100 UMEE and 100 ATOM
supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000))
s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000))

// create a borrower which supplies and collateralizes 100 ATOM
borrower := s.newAccount(coin(atomDenom, 100_000000))
s.supply(borrower, coin(atomDenom, 100_000000))
s.collateralize(borrower, coin("u/"+atomDenom, 100_000000))

// create an additional supplier (DUMP, PUMP tokens)
surplus := s.newAccount(coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000))
s.supply(surplus, coin(pumpDenom, 100_000000))
s.supply(surplus, coin(dumpDenom, 100_000000))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should be reused, rather than copied over.


// this will be a DUMP (historic price 1.00, current price 0.50) borrower
// using PUMP (historic price 1.00, current price 2.00) collateral
dumpborrower := s.newAccount(coin(pumpDenom, 100_000000))
s.supply(dumpborrower, coin(pumpDenom, 100_000000))
s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000))
// collateral value is $200 (current) or $100 (historic)
// collateral weights are always 0.25 in testing

// this will be a PUMP (historic price 1.00, current price 2.00) borrower
// using DUMP (historic price 1.00, current price 0.50) collateral
pumpborrower := s.newAccount(coin(dumpDenom, 100_000000))
s.supply(pumpborrower, coin(dumpDenom, 100_000000))
s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000))
// collateral value is $50 (current) or $100 (historic)
// collateral weights are always 0.25 in testing

tcs := []testCase{
{
"uToken",
borrower,
coin("u/"+umeeDenom, 0),
types.ErrUToken,
},
{
"unregistered token",
borrower,
coin("abcd", 0),
types.ErrNotRegisteredToken,
},
{
"zero collateral",
supplier,
coin(atomDenom, 0),
types.ErrMaxBorrowZero,
},
{
"atom borrow",
borrower,
coin(atomDenom, 25_000000),
nil,
},
{
"already borrowed max",
borrower,
coin(atomDenom, 0),
types.ErrMaxBorrowZero,
},
{
"dump borrower",
dumpborrower,
coin(dumpDenom, 25_000000),
nil,
},
{
"pump borrower",
pumpborrower,
coin(pumpDenom, 6_250000),
nil,
},
adamewozniak marked this conversation as resolved.
Show resolved Hide resolved
}

for _, tc := range tcs {
msg := &types.MsgMaxBorrow{
Borrower: tc.addr.String(),
Denom: tc.coin.Denom,
}
adamewozniak marked this conversation as resolved.
Show resolved Hide resolved
if tc.err != nil {
_, err := srv.MaxBorrow(ctx, msg)
require.ErrorIs(err, tc.err, tc.msg)
} else {
// initial state
iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr)
iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr)
iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx)
iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom)
iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr)

// verify the output of borrow function
resp, err := srv.MaxBorrow(ctx, msg)
require.NoError(err, tc.msg)
require.Equal(&types.MsgMaxBorrowResponse{
Borrowed: tc.coin,
}, resp, tc.msg)

// final state
fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr)
fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr)
fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx)
fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom)
fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr)

// verify token balance is increased by expected amount
require.Equal(iBalance.Add(tc.coin), fBalance, tc.msg, "balances")
// verify uToken collateral unchanged
require.Equal(iCollateral, fCollateral, tc.msg, "collateral")
// verify uToken supply is unchanged
require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply")
// verify uToken exchange rate is unchanged
require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate")
// verify borrowed coins increased by expected amount
require.Equal(iBorrowed.Add(tc.coin), fBorrowed, "borrowed coins")

// check all available invariants
s.checkInvariants(tc.msg)
}
}
}

func (s *IntegrationTestSuite) TestMsgRepay() {
type testCase struct {
msg string
Expand Down
2 changes: 2 additions & 0 deletions x/leverage/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgGovUpdateRegistry{}, "umee/leverage/MsgGovUpdateRegistry", nil)
cdc.RegisterConcrete(&MsgSupplyCollateral{}, "umee/leverage/MsgSupplyCollateral", nil)
cdc.RegisterConcrete(&MsgMaxWithdraw{}, "umee/leverage/MsgMaxWithdraw", nil)
cdc.RegisterConcrete(&MsgMaxBorrow{}, "umee/leverage/MsgMaxBorrow", nil)
}

func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
Expand All @@ -52,6 +53,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgGovUpdateRegistry{},
&MsgSupplyCollateral{},
&MsgMaxWithdraw{},
&MsgMaxBorrow{},
)

registry.RegisterImplementations(
Expand Down
1 change: 1 addition & 0 deletions x/leverage/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 403, "borrower not eligible for liquidation")
ErrMaxWithdrawZero = sdkerrors.Register(ModuleName, 404, "max withdraw amount was zero")
ErrNoHistoricMedians = sdkerrors.Register(ModuleName, 405, "insufficient historic medians available")
ErrMaxBorrowZero = sdkerrors.Register(ModuleName, 406, "max borrow amount was zero")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is an error? If there is nothing more to borrow, we just return zero, rather error. Imaging smart contract just want to automatically set max borrow. It should handle zero value rather than dealing with failed transaction.

Copy link
Member Author

@toteki toteki Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any supply / withdraw / borrow / etc that is a No-Op currently fails rather then succeeding with consumed gas. This follows suit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If transaction fails, then the whole provided fee for gas is consumed anyway. So, it only makes doing composability more difficult.


// 5XX = Market Conditions
ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 500, "lending pool insufficient")
Expand Down
Loading