Skip to content

Commit ec76a9d

Browse files
authored
feat: add MsgMaxBorrow (#1690)
* changelog BEFORE feature?? * ++ * proto++ * msg_server * rm++ * Msg++ * CLI++ * CLI tests++ * msg server tests
1 parent deb4dc4 commit ec76a9d

File tree

11 files changed

+790
-87
lines changed

11 files changed

+790
-87
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
6464
- [1654](https://github.com/umee-network/umee/pull/1654) Leverage historacle integration.
6565
- [1685](https://github.com/umee-network/umee/pull/1685) Add medians param to Token registry.
6666
- [1683](https://github.com/umee-network/umee/pull/1683) Add MaxBorrow query and allow returning all denoms from MaxWithdraw.
67+
- [1690](https://github.com/umee-network/umee/pull/1690) Add MaxBorrow message type.
6768

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

proto/umee/leverage/v1/tx.proto

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ service Msg {
3636
// Borrow allows a user to borrow tokens from the module if they have sufficient collateral.
3737
rpc Borrow(MsgBorrow) returns (MsgBorrowResponse);
3838

39+
// MaxBorrow allows a user to borrow the maximum amount of tokens their collateral will allow.
40+
rpc MaxBorrow(MsgMaxBorrow) returns (MsgMaxBorrowResponse);
41+
3942
// Repay allows a user to repay previously borrowed tokens and interest.
4043
rpc Repay(MsgRepay) returns (MsgRepayResponse);
4144

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

105+
// MsgMaxBorrow represents a user's request to borrow a base asset type
106+
// from the module, using the maximum available amount.
107+
message MsgMaxBorrow {
108+
// Borrower is the account address taking a loan and the signer
109+
// of the message.
110+
string borrower = 1;
111+
string denom = 2;
112+
}
113+
102114
// MsgRepay represents a user's request to repay a borrowed base asset
103115
// type to the module.
104116
message MsgRepay {
@@ -162,6 +174,12 @@ message MsgDecollateralizeResponse {}
162174
// MsgBorrowResponse defines the Msg/Borrow response type.
163175
message MsgBorrowResponse {}
164176

177+
// MsgMaxBorrowResponse defines the Msg/MaxBorrow response type.
178+
message MsgMaxBorrowResponse {
179+
// Borrowed is the amount of tokens borrowed.
180+
cosmos.base.v1beta1.Coin borrowed = 1 [(gogoproto.nullable) = false];
181+
}
182+
165183
// MsgRepayResponse defines the Msg/Repay response type.
166184
message MsgRepayResponse {
167185
// Repaid is the amount of base tokens repaid to the module.

x/leverage/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ Users have the following actions available to them:
8080

8181
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).
8282

83+
- `MsgMaxBorrow` borrows assets by automatically calculating the maximum amount that can be borrowed.
84+
8385
- `MsgRepay` assets of a borrowed type, directly reducing the amount owed.
8486

8587
Repayments that exceed a borrower's amount owed in the selected denomination succeed at paying the reduced amount rather than failing outright.

x/leverage/client/cli/tx.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func GetTxCmd() *cobra.Command {
3030
GetCmdCollateralize(),
3131
GetCmdDecollateralize(),
3232
GetCmdBorrow(),
33+
GetCmdMaxBorrow(),
3334
GetCmdRepay(),
3435
GetCmdLiquidate(),
3536
GetCmdSupplyCollateral(),
@@ -213,6 +214,30 @@ func GetCmdBorrow() *cobra.Command {
213214
return cmd
214215
}
215216

217+
// GetCmdMaxBorrow creates a Cobra command to generate or broadcast a
218+
// transaction with a MsgBorrow message.
219+
func GetCmdMaxBorrow() *cobra.Command {
220+
cmd := &cobra.Command{
221+
Use: "max-borrow [denom]",
222+
Args: cobra.ExactArgs(1),
223+
Short: "Borrow the maximum acceptable amount of a supported asset",
224+
RunE: func(cmd *cobra.Command, args []string) error {
225+
clientCtx, err := client.GetClientTxContext(cmd)
226+
if err != nil {
227+
return err
228+
}
229+
230+
msg := types.NewMsgMaxBorrow(clientCtx.GetFromAddress(), args[0])
231+
232+
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
233+
},
234+
}
235+
236+
flags.AddTxFlagsToCmd(cmd)
237+
238+
return cmd
239+
}
240+
216241
// GetCmdRepay creates a Cobra command to generate or broadcast a
217242
// transaction with a MsgRepay message.
218243
func GetCmdRepay() *cobra.Command {

x/leverage/client/tests/tests.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,16 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
190190
"borrow",
191191
cli.GetCmdBorrow(),
192192
[]string{
193-
"249uumee", // produces a borrowed amount of 250 due to rounding
193+
"150uumee",
194+
},
195+
nil,
196+
}
197+
198+
maxborrow := testTransaction{
199+
"max-borrow",
200+
cli.GetCmdMaxBorrow(),
201+
[]string{
202+
"uumee", // should borrow up to the max of 250 uumee, which will become 251 due to rounding
194203
},
195204
nil,
196205
}
@@ -253,13 +262,13 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
253262
&types.QueryAccountBalancesResponse{},
254263
&types.QueryAccountBalancesResponse{
255264
Supplied: sdk.NewCoins(
256-
sdk.NewInt64Coin(appparams.BondDenom, 1000),
265+
sdk.NewInt64Coin(appparams.BondDenom, 1001),
257266
),
258267
Collateral: sdk.NewCoins(
259268
sdk.NewInt64Coin(types.ToUTokenDenom(appparams.BondDenom), 1000),
260269
),
261270
Borrowed: sdk.NewCoins(
262-
sdk.NewInt64Coin(appparams.BondDenom, 250),
271+
sdk.NewInt64Coin(appparams.BondDenom, 251),
263272
),
264273
},
265274
},
@@ -275,16 +284,16 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
275284
// This result is umee's oracle exchange rate from
276285
// app/test_helpers.go/IntegrationTestNetworkConfig
277286
// times the amount of umee, and then times params
278-
// (1000 / 1000000) * 34.21 = 0.03421
279-
SuppliedValue: sdk.MustNewDecFromStr("0.03421"),
280-
// (1000 / 1000000) * 34.21 = 0.03421
281-
CollateralValue: sdk.MustNewDecFromStr("0.03421"),
282-
// (250 / 1000000) * 34.21 = 0.0085525
283-
BorrowedValue: sdk.MustNewDecFromStr("0.0085525"),
284-
// (1000 / 1000000) * 34.21 * 0.25 = 0.0085525
285-
BorrowLimit: sdk.MustNewDecFromStr("0.0085525"),
286-
// (1000 / 1000000) * 0.25 * 34.21 = 0.0085525
287-
LiquidationThreshold: sdk.MustNewDecFromStr("0.0085525"),
287+
// (1001 / 1000000) * 34.21 = 0.03424421
288+
SuppliedValue: sdk.MustNewDecFromStr("0.03424421"),
289+
// (1001 / 1000000) * 34.21 = 0.03424421
290+
CollateralValue: sdk.MustNewDecFromStr("0.03424421"),
291+
// (251 / 1000000) * 34.21 = 0.00858671
292+
BorrowedValue: sdk.MustNewDecFromStr("0.00858671"),
293+
// (1001 / 1000000) * 34.21 * 0.25 = 0.0085610525
294+
BorrowLimit: sdk.MustNewDecFromStr("0.0085610525"),
295+
// (1001 / 1000000) * 0.25 * 34.21 = 0.0085610525
296+
LiquidationThreshold: sdk.MustNewDecFromStr("0.0085610525"),
288297
},
289298
},
290299
{
@@ -442,6 +451,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() {
442451
addCollateral,
443452
supplyCollateral,
444453
borrow,
454+
maxborrow,
445455
)
446456

447457
// These queries run while the supplying and borrowing is active to produce nonzero output

x/leverage/keeper/msg_server.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,68 @@ func (s msgServer) Borrow(
322322
return &types.MsgBorrowResponse{}, err
323323
}
324324

325+
func (s msgServer) MaxBorrow(
326+
goCtx context.Context,
327+
msg *types.MsgMaxBorrow,
328+
) (*types.MsgMaxBorrowResponse, error) {
329+
ctx := sdk.UnwrapSDKContext(goCtx)
330+
331+
borrowerAddr, err := sdk.AccAddressFromBech32(msg.Borrower)
332+
if err != nil {
333+
return nil, err
334+
}
335+
336+
currentMaxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom, false)
337+
if err != nil {
338+
return nil, err
339+
}
340+
historicMaxBorrow, err := s.keeper.maxBorrow(ctx, borrowerAddr, msg.Denom, true)
341+
if err != nil {
342+
return nil, err
343+
}
344+
345+
maxBorrow := sdk.NewCoin(
346+
msg.Denom,
347+
sdk.MinInt(currentMaxBorrow.Amount, historicMaxBorrow.Amount),
348+
)
349+
if maxBorrow.IsZero() {
350+
return nil, types.ErrMaxBorrowZero
351+
}
352+
353+
if err := s.keeper.Borrow(ctx, borrowerAddr, maxBorrow); err != nil {
354+
return nil, err
355+
}
356+
357+
// Fail here if borrower ends up over their borrow limit under current or historic prices
358+
err = s.keeper.assertBorrowerHealth(ctx, borrowerAddr)
359+
if err != nil {
360+
return nil, err
361+
}
362+
363+
// Check MaxSupplyUtilization after transaction
364+
if err = s.keeper.checkSupplyUtilization(ctx, maxBorrow.Denom); err != nil {
365+
return nil, err
366+
}
367+
368+
// Check MinCollateralLiquidity is still satisfied after the transaction
369+
if err = s.keeper.checkCollateralLiquidity(ctx, maxBorrow.Denom); err != nil {
370+
return nil, err
371+
}
372+
373+
s.keeper.Logger(ctx).Debug(
374+
"assets borrowed",
375+
"borrower", msg.Borrower,
376+
"amount", maxBorrow.String(),
377+
)
378+
err = ctx.EventManager().EmitTypedEvent(&types.EventBorrow{
379+
Borrower: msg.Borrower,
380+
Asset: maxBorrow,
381+
})
382+
return &types.MsgMaxBorrowResponse{
383+
Borrowed: maxBorrow,
384+
}, err
385+
}
386+
325387
func (s msgServer) Repay(
326388
goCtx context.Context,
327389
msg *types.MsgRepay,

x/leverage/keeper/msg_server_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,138 @@ func (s *IntegrationTestSuite) TestMsgBorrow() {
12481248
}
12491249
}
12501250

1251+
func (s *IntegrationTestSuite) TestMsgMaxBorrow() {
1252+
type testCase struct {
1253+
msg string
1254+
addr sdk.AccAddress
1255+
coin sdk.Coin
1256+
err error
1257+
}
1258+
1259+
app, ctx, srv, require := s.app, s.ctx, s.msgSrvr, s.Require()
1260+
1261+
// create and fund a supplier which supplies 100 UMEE and 100 ATOM
1262+
supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000))
1263+
s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000))
1264+
1265+
// create a borrower which supplies and collateralizes 100 ATOM
1266+
borrower := s.newAccount(coin(atomDenom, 100_000000))
1267+
s.supply(borrower, coin(atomDenom, 100_000000))
1268+
s.collateralize(borrower, coin("u/"+atomDenom, 100_000000))
1269+
1270+
// create an additional supplier (DUMP, PUMP tokens)
1271+
surplus := s.newAccount(coin(dumpDenom, 100_000000), coin(pumpDenom, 100_000000))
1272+
s.supply(surplus, coin(pumpDenom, 100_000000))
1273+
s.supply(surplus, coin(dumpDenom, 100_000000))
1274+
1275+
// this will be a DUMP (historic price 1.00, current price 0.50) borrower
1276+
// using PUMP (historic price 1.00, current price 2.00) collateral
1277+
dumpborrower := s.newAccount(coin(pumpDenom, 100_000000))
1278+
s.supply(dumpborrower, coin(pumpDenom, 100_000000))
1279+
s.collateralize(dumpborrower, coin("u/"+pumpDenom, 100_000000))
1280+
// collateral value is $200 (current) or $100 (historic)
1281+
// collateral weights are always 0.25 in testing
1282+
1283+
// this will be a PUMP (historic price 1.00, current price 2.00) borrower
1284+
// using DUMP (historic price 1.00, current price 0.50) collateral
1285+
pumpborrower := s.newAccount(coin(dumpDenom, 100_000000))
1286+
s.supply(pumpborrower, coin(dumpDenom, 100_000000))
1287+
s.collateralize(pumpborrower, coin("u/"+dumpDenom, 100_000000))
1288+
// collateral value is $50 (current) or $100 (historic)
1289+
// collateral weights are always 0.25 in testing
1290+
1291+
tcs := []testCase{
1292+
{
1293+
"uToken",
1294+
borrower,
1295+
coin("u/"+umeeDenom, 0),
1296+
types.ErrUToken,
1297+
},
1298+
{
1299+
"unregistered token",
1300+
borrower,
1301+
coin("abcd", 0),
1302+
types.ErrNotRegisteredToken,
1303+
},
1304+
{
1305+
"zero collateral",
1306+
supplier,
1307+
coin(atomDenom, 0),
1308+
types.ErrMaxBorrowZero,
1309+
},
1310+
{
1311+
"atom borrow",
1312+
borrower,
1313+
coin(atomDenom, 25_000000),
1314+
nil,
1315+
},
1316+
{
1317+
"already borrowed max",
1318+
borrower,
1319+
coin(atomDenom, 0),
1320+
types.ErrMaxBorrowZero,
1321+
},
1322+
{
1323+
"dump borrower",
1324+
dumpborrower,
1325+
coin(dumpDenom, 25_000000),
1326+
nil,
1327+
},
1328+
{
1329+
"pump borrower",
1330+
pumpborrower,
1331+
coin(pumpDenom, 6_250000),
1332+
nil,
1333+
},
1334+
}
1335+
1336+
for _, tc := range tcs {
1337+
msg := &types.MsgMaxBorrow{
1338+
Borrower: tc.addr.String(),
1339+
Denom: tc.coin.Denom,
1340+
}
1341+
if tc.err != nil {
1342+
_, err := srv.MaxBorrow(ctx, msg)
1343+
require.ErrorIs(err, tc.err, tc.msg)
1344+
} else {
1345+
// initial state
1346+
iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr)
1347+
iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr)
1348+
iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx)
1349+
iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom)
1350+
iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr)
1351+
1352+
// verify the output of borrow function
1353+
resp, err := srv.MaxBorrow(ctx, msg)
1354+
require.NoError(err, tc.msg)
1355+
require.Equal(&types.MsgMaxBorrowResponse{
1356+
Borrowed: tc.coin,
1357+
}, resp, tc.msg)
1358+
1359+
// final state
1360+
fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr)
1361+
fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr)
1362+
fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx)
1363+
fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom)
1364+
fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr)
1365+
1366+
// verify token balance is increased by expected amount
1367+
require.Equal(iBalance.Add(tc.coin), fBalance, tc.msg, "balances")
1368+
// verify uToken collateral unchanged
1369+
require.Equal(iCollateral, fCollateral, tc.msg, "collateral")
1370+
// verify uToken supply is unchanged
1371+
require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply")
1372+
// verify uToken exchange rate is unchanged
1373+
require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate")
1374+
// verify borrowed coins increased by expected amount
1375+
require.Equal(iBorrowed.Add(tc.coin), fBorrowed, "borrowed coins")
1376+
1377+
// check all available invariants
1378+
s.checkInvariants(tc.msg)
1379+
}
1380+
}
1381+
}
1382+
12511383
func (s *IntegrationTestSuite) TestMsgRepay() {
12521384
type testCase struct {
12531385
msg string

x/leverage/types/codec.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
3737
cdc.RegisterConcrete(&MsgGovUpdateRegistry{}, "umee/leverage/MsgGovUpdateRegistry", nil)
3838
cdc.RegisterConcrete(&MsgSupplyCollateral{}, "umee/leverage/MsgSupplyCollateral", nil)
3939
cdc.RegisterConcrete(&MsgMaxWithdraw{}, "umee/leverage/MsgMaxWithdraw", nil)
40+
cdc.RegisterConcrete(&MsgMaxBorrow{}, "umee/leverage/MsgMaxBorrow", nil)
4041
}
4142

4243
func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
@@ -52,6 +53,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
5253
&MsgGovUpdateRegistry{},
5354
&MsgSupplyCollateral{},
5455
&MsgMaxWithdraw{},
56+
&MsgMaxBorrow{},
5557
)
5658

5759
registry.RegisterImplementations(

x/leverage/types/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var (
3737
ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 403, "borrower not eligible for liquidation")
3838
ErrMaxWithdrawZero = sdkerrors.Register(ModuleName, 404, "max withdraw amount was zero")
3939
ErrNoHistoricMedians = sdkerrors.Register(ModuleName, 405, "insufficient historic medians available")
40+
ErrMaxBorrowZero = sdkerrors.Register(ModuleName, 406, "max borrow amount was zero")
4041

4142
// 5XX = Market Conditions
4243
ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 500, "lending pool insufficient")

0 commit comments

Comments
 (0)