From 1442deb3dc1862480ad40b0877882672ffbc3a6f Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Fri, 30 Oct 2020 10:59:47 +0100 Subject: [PATCH] Harvest basic borrow functionality (#702) * basic borrow types * borrow keeper scaffolding * borrow limits param * integrate pricefeed keeper * msg handling and querier * borrow user validation * update migration scripts for compile * borrows querier, fixes * add money market param * add spot market ID to params, refactor pricefeed * working bnb -> ukava borrows * refactor to getAssetPrice * conversion_factor param, refactor validateBorrow() * address misc revisions * remove validation code * add borrow test * update test params * single borrow with sdk.Coins per user * fix harvest test * removed legacy commented out code * address minor revisions --- app/app.go | 13 ++-- migrate/v0_11/migrate.go | 53 ++++++++------- x/harvest/alias.go | 5 +- x/harvest/client/cli/query.go | 58 ++++++++++++++++ x/harvest/client/cli/tx.go | 29 ++++++++ x/harvest/handler.go | 20 ++++++ x/harvest/keeper/borrow.go | 34 ++++++++++ x/harvest/keeper/borrow_test.go | 101 ++++++++++++++++++++++++++++ x/harvest/keeper/claim_test.go | 5 ++ x/harvest/keeper/deposit_test.go | 10 +++ x/harvest/keeper/keeper.go | 81 ++++++++++++++++++---- x/harvest/keeper/params.go | 13 ++++ x/harvest/keeper/querier.go | 35 ++++++++++ x/harvest/keeper/rewards_test.go | 10 +++ x/harvest/keeper/timelock_test.go | 5 ++ x/harvest/module.go | 14 ++-- x/harvest/types/borrow.go | 19 ++++++ x/harvest/types/codec.go | 1 + x/harvest/types/errors.go | 10 +++ x/harvest/types/events.go | 4 ++ x/harvest/types/expected_keepers.go | 7 ++ x/harvest/types/keys.go | 1 + x/harvest/types/msg.go | 53 +++++++++++++++ x/harvest/types/params.go | 93 ++++++++++++++++++++++++- x/harvest/types/querier.go | 19 ++++++ x/pricefeed/keeper/params.go | 1 - x/pricefeed/types/market.go | 1 - 27 files changed, 640 insertions(+), 55 deletions(-) create mode 100644 x/harvest/keeper/borrow.go create mode 100644 x/harvest/keeper/borrow_test.go create mode 100644 x/harvest/types/borrow.go diff --git a/app/app.go b/app/app.go index 64db9531..7c0a1c33 100644 --- a/app/app.go +++ b/app/app.go @@ -102,9 +102,9 @@ var ( bep3.ModuleName: {supply.Minter, supply.Burner}, kavadist.ModuleName: {supply.Minter}, issuance.ModuleAccountName: {supply.Minter, supply.Burner}, - harvest.LPAccount: {supply.Minter, supply.Burner}, - harvest.DelegatorAccount: {supply.Minter, supply.Burner}, - harvest.ModuleAccountName: {supply.Minter, supply.Burner}, + harvest.LPAccount: {supply.Minter, supply.Burner}, + harvest.DelegatorAccount: {supply.Minter, supply.Burner}, + harvest.ModuleAccountName: {supply.Minter, supply.Burner}, } // module accounts that are allowed to receive tokens @@ -376,7 +376,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, harvestSubspace, app.accountKeeper, app.supplyKeeper, - &stakingKeeper) + &stakingKeeper, + app.pricefeedKeeper) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -407,7 +408,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), committee.NewAppModule(app.committeeKeeper, app.accountKeeper), issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper), - harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper), + harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper, app.pricefeedKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -460,7 +461,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), committee.NewAppModule(app.committeeKeeper, app.accountKeeper), issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper), - harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper), + harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper, app.pricefeedKeeper), ) app.sm.RegisterStoreDecoders() diff --git a/migrate/v0_11/migrate.go b/migrate/v0_11/migrate.go index 7ed3f2fc..951799ff 100644 --- a/migrate/v0_11/migrate.go +++ b/migrate/v0_11/migrate.go @@ -129,7 +129,7 @@ func MigrateAppState(v0_9AppState v39_genutil.AppMap) v39_genutil.AppMap { delete(v0_9AppState, v0_9pricefeed.ModuleName) v0_11AppState[v0_9pricefeed.ModuleName] = v0_11Codec.MustMarshalJSON(MigratePricefeed(pricefeedGenState)) } - v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest()) + // v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest()) v0_11AppState[v0_11issuance.ModuleName] = v0_11Codec.MustMarshalJSON(v0_11issuance.DefaultGenesisState()) return v0_11AppState } @@ -633,29 +633,34 @@ func MigrateGov(oldGenState v39_1gov.GenesisState) v39_1gov.GenesisState { return oldGenState } -// MigrateHarvest initializes the harvest genesis state for kava-4 -func MigrateHarvest() v0_11harvest.GenesisState { - // total HARD per second for lps (week one): 633761 - // HARD per second for delegators (week one): 1267522 - incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC) - incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC) - claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC) - harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams( - true, - v0_11harvest.DistributionSchedules{ - v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), - v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), - v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), - v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), - }, - v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule( - v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), - time.Hour*24, - ), - }, - ), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes) - return harvestGS -} +// // MigrateHarvest initializes the harvest genesis state for kava-4 +// func MigrateHarvest() v0_11harvest.GenesisState { +// // total HARD per second for lps (week one): 633761 +// // HARD per second for delegators (week one): 1267522 +// incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC) +// incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC) +// claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC) +// harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams( +// true, +// v0_11harvest.DistributionSchedules{ +// v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), +// v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), +// v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), +// v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), +// }, +// v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule( +// v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), +// time.Hour*24, +// ), +// }, +// v0_11harvest.BlockLimits{ +// v0_11harvest.NewBlockLimit("usdx", sdk.Dec(0.9)), +// v0_11harvest.NewBlockLimit("ukava", sdk.Dec(0.6)), +// v0_11harvest.NewBlockLimit("bnb", sdk.Dec(0.9)), +// }, +// ), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes) +// return harvestGS +// } // MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState { diff --git a/x/harvest/alias.go b/x/harvest/alias.go index 70846d5a..ce57566a 100644 --- a/x/harvest/alias.go +++ b/x/harvest/alias.go @@ -74,6 +74,7 @@ var ( RegisterCodec = types.RegisterCodec // variable aliases + BorrowsKeyPrefix = types.BorrowsKeyPrefix ClaimsKeyPrefix = types.ClaimsKeyPrefix DefaultActive = types.DefaultActive DefaultDelegatorSchedules = types.DefaultDelegatorSchedules @@ -109,7 +110,9 @@ var ( type ( Keeper = keeper.Keeper AccountKeeper = types.AccountKeeper - Claim = types.Claim + Borrow = types.Borrow + MoneyMarket = types.MoneyMarket + MoneyMarkets = types.MoneyMarkets DelegatorDistributionSchedule = types.DelegatorDistributionSchedule DelegatorDistributionSchedules = types.DelegatorDistributionSchedules Deposit = types.Deposit diff --git a/x/harvest/client/cli/query.go b/x/harvest/client/cli/query.go index 67645626..adb7c8f4 100644 --- a/x/harvest/client/cli/query.go +++ b/x/harvest/client/cli/query.go @@ -40,6 +40,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { queryModAccountsCmd(queryRoute, cdc), queryDepositsCmd(queryRoute, cdc), queryClaimsCmd(queryRoute, cdc), + queryBorrowsCmd(queryRoute, cdc), )...) return harvestQueryCmd @@ -250,3 +251,60 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { cmd.Flags().String(flagDepositType, "", "(optional) filter for claims by type (lp or staking)") return cmd } + +func queryBorrowsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "borrows", + Short: "query harvest module borrows with optional filters", + Long: strings.TrimSpace(`query for all harvest module borrows or a specific borrow using flags: + + Example: + $ kvcli q harvest borrows + $ kvcli q harvest borrows --borrower kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny + $ kvcli q harvest borrows --borrow-denom bnb`, + ), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + var owner sdk.AccAddress + + ownerBech := viper.GetString(flagOwner) + depositDenom := viper.GetString(flagDepositDenom) + + if len(ownerBech) != 0 { + borrowOwner, err := sdk.AccAddressFromBech32(ownerBech) + if err != nil { + return err + } + owner = borrowOwner + } + + page := viper.GetInt(flags.FlagPage) + limit := viper.GetInt(flags.FlagLimit) + + params := types.NewQueryBorrowParams(page, limit, owner, depositDenom) + bz, err := cdc.MarshalJSON(params) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetBorrows) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + + var borrows []types.Borrow + if err := cdc.UnmarshalJSON(res, &borrows); err != nil { + return fmt.Errorf("failed to unmarshal borrows: %w", err) + } + return cliCtx.PrintOutput(borrows) + }, + } + cmd.Flags().Int(flags.FlagPage, 1, "pagination page to query for") + cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit (max 100)") + cmd.Flags().String(flagOwner, "", "(optional) filter for borrows by owner address") + return cmd +} diff --git a/x/harvest/client/cli/tx.go b/x/harvest/client/cli/tx.go index 525530b2..b681ebde 100644 --- a/x/harvest/client/cli/tx.go +++ b/x/harvest/client/cli/tx.go @@ -33,6 +33,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { getCmdDeposit(cdc), getCmdWithdraw(cdc), getCmdClaimReward(cdc), + getCmdBorrow(cdc), )...) return harvestTxCmd @@ -116,3 +117,31 @@ func getCmdClaimReward(cdc *codec.Codec) *cobra.Command { }, } } + +func getCmdBorrow(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "borrow [1000000000ukava]", + Short: "borrow tokens from the harvest protocol", + Long: strings.TrimSpace(`borrows tokens from the harvest protocol`), + Args: cobra.ExactArgs(1), + Example: fmt.Sprintf( + `%s tx %s borrow 1000000000ukava --from `, version.ClientName, types.ModuleName, + ), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + + coins, err := sdk.ParseCoins(args[0]) + if err != nil { + return err + } + + msg := types.NewMsgBorrow(cliCtx.GetFromAddress(), coins) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/harvest/handler.go b/x/harvest/handler.go index 196a5fc5..f97d16d0 100644 --- a/x/harvest/handler.go +++ b/x/harvest/handler.go @@ -21,6 +21,8 @@ func NewHandler(k Keeper) sdk.Handler { return handleMsgDeposit(ctx, k, msg) case types.MsgWithdraw: return handleMsgWithdraw(ctx, k, msg) + case types.MsgBorrow: + return handleMsgBorrow(ctx, k, msg) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } @@ -80,3 +82,21 @@ func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) Events: ctx.EventManager().Events(), }, nil } + +func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sdk.Result, error) { + err := k.Borrow(ctx, msg.Borrower, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Borrower.String()), + ), + ) + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} diff --git a/x/harvest/keeper/borrow.go b/x/harvest/keeper/borrow.go new file mode 100644 index 00000000..0e2015bc --- /dev/null +++ b/x/harvest/keeper/borrow.go @@ -0,0 +1,34 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/harvest/types" +) + +// Borrow funds +func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, borrower, coins) + if err != nil { + return err + } + + borrow, found := k.GetBorrow(ctx, borrower) + if !found { + borrow = types.NewBorrow(borrower, coins) + } else { + borrow.Amount = borrow.Amount.Add(coins...) + } + + k.SetBorrow(ctx, borrow) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHarvestBorrow, + sdk.NewAttribute(types.AttributeKeyBorrower, borrow.Borrower.String()), + sdk.NewAttribute(types.AttributeKeyBorrowCoins, coins.String()), + ), + ) + + return nil +} diff --git a/x/harvest/keeper/borrow_test.go b/x/harvest/keeper/borrow_test.go new file mode 100644 index 00000000..76247c17 --- /dev/null +++ b/x/harvest/keeper/borrow_test.go @@ -0,0 +1,101 @@ +package keeper_test + +import ( + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/harvest/types" +) + +func (suite *KeeperTestSuite) TestBorrow() { + type args struct { + borrower sdk.AccAddress + coins sdk.Coins + expectedAccountBalance sdk.Coins + expectedModAccountBalance sdk.Coins + } + type errArgs struct { + expectPass bool + contains string + } + type borrowTest struct { + name string + args args + errArgs errArgs + } + testCases := []borrowTest{ + { + "valid", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + coins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50))), + expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(150))), + expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(950))), + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + // create new app with one funded account + + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + authGS := app.NewAuthGenState( + []sdk.AccAddress{tc.args.borrower}, + []sdk.Coins{sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100)))}) + loanToValue := sdk.MustNewDecFromStr("0.6") + harvestGS := types.NewGenesisState(types.NewParams( + true, + types.DistributionSchedules{ + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + }, + types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule( + types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}), + time.Hour*24, + ), + }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, + ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) + tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) + keeper := tApp.GetHarvestKeeper() + supplyKeeper := tApp.GetSupplyKeeper() + supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000)))) + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + + // run the test + var err error + err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.coins) + + // verify results + if tc.errArgs.expectPass { + suite.Require().NoError(err) + acc := suite.getAccount(tc.args.borrower) + suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins()) + mAcc := suite.getModuleAccount(types.ModuleAccountName) + suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins()) + _, f := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower) + suite.Require().True(f) + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + } + }) + } +} diff --git a/x/harvest/keeper/claim_test.go b/x/harvest/keeper/claim_test.go index e28bb936..4510cdd0 100644 --- a/x/harvest/keeper/claim_test.go +++ b/x/harvest/keeper/claim_test.go @@ -253,6 +253,7 @@ func (suite *KeeperTestSuite) TestClaim() { sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), }) + loanToValue := sdk.MustNewDecFromStr("0.6") harvestGS := types.NewGenesisState(types.NewParams( true, types.DistributionSchedules{ @@ -263,6 +264,10 @@ func (suite *KeeperTestSuite) TestClaim() { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) if tc.args.validatorVesting { diff --git a/x/harvest/keeper/deposit_test.go b/x/harvest/keeper/deposit_test.go index 22b5fcde..2f1ce448 100644 --- a/x/harvest/keeper/deposit_test.go +++ b/x/harvest/keeper/deposit_test.go @@ -116,6 +116,7 @@ func (suite *KeeperTestSuite) TestDeposit() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))}) + loanToValue, _ := sdk.NewDecFromStr("0.6") harvestGS := types.NewGenesisState(types.NewParams( true, types.DistributionSchedules{ @@ -126,6 +127,10 @@ func (suite *KeeperTestSuite) TestDeposit() { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) keeper := tApp.GetHarvestKeeper() @@ -283,6 +288,7 @@ func (suite *KeeperTestSuite) TestWithdraw() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))}) + loanToValue := sdk.MustNewDecFromStr("0.6") harvestGS := types.NewGenesisState(types.NewParams( true, types.DistributionSchedules{ @@ -293,6 +299,10 @@ func (suite *KeeperTestSuite) TestWithdraw() { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) keeper := tApp.GetHarvestKeeper() diff --git a/x/harvest/keeper/keeper.go b/x/harvest/keeper/keeper.go index 3f6fa219..7221fa55 100644 --- a/x/harvest/keeper/keeper.go +++ b/x/harvest/keeper/keeper.go @@ -13,27 +13,31 @@ import ( // Keeper keeper for the harvest module type Keeper struct { - key sdk.StoreKey - cdc *codec.Codec - paramSubspace subspace.Subspace - accountKeeper types.AccountKeeper - supplyKeeper types.SupplyKeeper - stakingKeeper types.StakingKeeper + key sdk.StoreKey + cdc *codec.Codec + paramSubspace subspace.Subspace + accountKeeper types.AccountKeeper + supplyKeeper types.SupplyKeeper + stakingKeeper types.StakingKeeper + pricefeedKeeper types.PricefeedKeeper } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper { +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, + ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper, + pfk types.PricefeedKeeper) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } return Keeper{ - key: key, - cdc: cdc, - paramSubspace: paramstore, - accountKeeper: ak, - supplyKeeper: sk, - stakingKeeper: stk, + key: key, + cdc: cdc, + paramSubspace: paramstore, + accountKeeper: ak, + supplyKeeper: sk, + stakingKeeper: stk, + pricefeedKeeper: pfk, } } @@ -178,7 +182,58 @@ func (k Keeper) IterateClaimsByTypeAndDenom(ctx sdk.Context, depositType types.D } } +// GetDepositsByUser gets all deposits for an individual user +func (k Keeper) GetDepositsByUser(ctx sdk.Context, user sdk.AccAddress) []types.Deposit { + var deposits []types.Deposit + k.IterateDeposits(ctx, func(deposit types.Deposit) (stop bool) { + if deposit.Depositor.Equals(user) { + deposits = append(deposits, deposit) + } + return false + }) + return deposits +} + // BondDenom returns the bond denom from the staking keeper func (k Keeper) BondDenom(ctx sdk.Context) string { return k.stakingKeeper.BondDenom(ctx) } + +// GetBorrow returns a Borrow from the store for a particular borrower address and borrow denom +func (k Keeper) GetBorrow(ctx sdk.Context, borrower sdk.AccAddress) (types.Borrow, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix) + bz := store.Get(borrower) + if bz == nil { + return types.Borrow{}, false + } + var borrow types.Borrow + k.cdc.MustUnmarshalBinaryBare(bz, &borrow) + return borrow, true +} + +// SetBorrow sets the input borrow in the store, prefixed by the borrower address and borrow denom +func (k Keeper) SetBorrow(ctx sdk.Context, borrow types.Borrow) { + store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix) + bz := k.cdc.MustMarshalBinaryBare(borrow) + store.Set(borrow.Borrower, bz) +} + +// DeleteBorrow deletes a borrow from the store +func (k Keeper) DeleteBorrow(ctx sdk.Context, borrow types.Borrow) { + store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix) + store.Delete(borrow.Borrower) +} + +// IterateBorrows iterates over all borrow objects in the store and performs a callback function +func (k Keeper) IterateBorrows(ctx sdk.Context, cb func(borrow types.Borrow) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var borrow types.Borrow + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &borrow) + if cb(borrow) { + break + } + } +} diff --git a/x/harvest/keeper/params.go b/x/harvest/keeper/params.go index 7a04ae42..c4b22075 100644 --- a/x/harvest/keeper/params.go +++ b/x/harvest/keeper/params.go @@ -18,6 +18,7 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramSubspace.SetParamSet(ctx, ¶ms) } +// GetLPSchedule gets the LP's schedule func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.DistributionSchedule, bool) { params := k.GetParams(ctx) for _, lps := range params.LiquidityProviderSchedules { @@ -28,6 +29,7 @@ func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.Distribution return types.DistributionSchedule{}, false } +// GetDelegatorSchedule gets the Delgator's schedule func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.DelegatorDistributionSchedule, bool) { params := k.GetParams(ctx) for _, dds := range params.DelegatorDistributionSchedules { @@ -37,3 +39,14 @@ func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.Deleg } return types.DelegatorDistributionSchedule{}, false } + +// GetMoneyMarket returns the corresponding Money Market param for a specific denom +func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) { + params := k.GetParams(ctx) + for _, mm := range params.MoneyMarkets { + if mm.Denom == denom { + return mm, true + } + } + return types.MoneyMarket{}, false +} diff --git a/x/harvest/keeper/querier.go b/x/harvest/keeper/querier.go index 688d87ef..756bc0bf 100644 --- a/x/harvest/keeper/querier.go +++ b/x/harvest/keeper/querier.go @@ -24,6 +24,8 @@ func NewQuerier(k Keeper) sdk.Querier { return queryGetDeposits(ctx, req, k) case types.QueryGetClaims: return queryGetClaims(ctx, req, k) + case types.QueryGetBorrows: + return queryGetBorrows(ctx, req, k) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) } @@ -260,3 +262,36 @@ func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e return bz, nil } + +func queryGetBorrows(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { + + var params types.QueryBorrowParams + err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + // TODO: filter query results + // depositDenom := len(params.BorrowDenom) > 0 + // owner := len(params.Owner) > 0 + + var borrows []types.Borrow + k.IterateBorrows(ctx, func(borrow types.Borrow) (stop bool) { + borrows = append(borrows, borrow) + return false + }) + + start, end := client.Paginate(len(borrows), params.Page, params.Limit, 100) + if start < 0 || end < 0 { + borrows = []types.Borrow{} + } else { + borrows = borrows[start:end] + } + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, borrows) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} diff --git a/x/harvest/keeper/rewards_test.go b/x/harvest/keeper/rewards_test.go index becfd722..215fdea5 100644 --- a/x/harvest/keeper/rewards_test.go +++ b/x/harvest/keeper/rewards_test.go @@ -63,6 +63,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() { // Initialize test app and set context tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime}) + loanToValue, _ := sdk.NewDecFromStr("0.6") harvestGS := types.NewGenesisState(types.NewParams( true, types.DistributionSchedules{ @@ -73,6 +74,10 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), tc.args.previousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) supplyKeeper := tApp.GetSupplyKeeper() @@ -400,6 +405,7 @@ func (suite *DelegatorRewardsTestSuite) kavaClaimExists(ctx sdk.Context, owner s } func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState { + loanToValue := sdk.MustNewDecFromStr("0.6") genState := types.NewGenesisState( types.NewParams( true, @@ -436,6 +442,10 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes, diff --git a/x/harvest/keeper/timelock_test.go b/x/harvest/keeper/timelock_test.go index 7b335e21..312464ca 100644 --- a/x/harvest/keeper/timelock_test.go +++ b/x/harvest/keeper/timelock_test.go @@ -279,6 +279,7 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins}) + loanToValue := sdk.MustNewDecFromStr("0.6") harvestGS := types.NewGenesisState(types.NewParams( true, types.DistributionSchedules{ @@ -289,6 +290,10 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() { time.Hour*24, ), }, + types.MoneyMarkets{ + types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)), + types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)), + }, ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) if tc.args.accArgs.vestingAccountBefore { diff --git a/x/harvest/module.go b/x/harvest/module.go index 9fa94d0a..13ee93a5 100644 --- a/x/harvest/module.go +++ b/x/harvest/module.go @@ -77,16 +77,18 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { type AppModule struct { AppModuleBasic - keeper Keeper - supplyKeeper types.SupplyKeeper + keeper Keeper + supplyKeeper types.SupplyKeeper + pricefeedKeeper types.PricefeedKeeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule { +func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper, pricefeedKeeper types.PricefeedKeeper) AppModule { return AppModule{ - AppModuleBasic: AppModuleBasic{}, - keeper: keeper, - supplyKeeper: supplyKeeper, + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + supplyKeeper: supplyKeeper, + pricefeedKeeper: pricefeedKeeper, } } diff --git a/x/harvest/types/borrow.go b/x/harvest/types/borrow.go new file mode 100644 index 00000000..7217312f --- /dev/null +++ b/x/harvest/types/borrow.go @@ -0,0 +1,19 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Borrow defines an amount of coins borrowed from a harvest module account +type Borrow struct { + Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// NewBorrow returns a new Borrow instance +func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins) Borrow { + return Borrow{ + Borrower: borrower, + Amount: amount, + } +} diff --git a/x/harvest/types/codec.go b/x/harvest/types/codec.go index 87858be8..aa722cf9 100644 --- a/x/harvest/types/codec.go +++ b/x/harvest/types/codec.go @@ -17,5 +17,6 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgClaimReward{}, "harvest/MsgClaimReward", nil) cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil) cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil) + cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil) cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil) } diff --git a/x/harvest/types/errors.go b/x/harvest/types/errors.go index c55cad57..55ea0d83 100644 --- a/x/harvest/types/errors.go +++ b/x/harvest/types/errors.go @@ -35,4 +35,14 @@ var ( ErrClaimExpired = sdkerrors.Register(ModuleName, 14, "claim period expired") // ErrInvalidReceiver error for when sending and receiving accounts don't match ErrInvalidReceiver = sdkerrors.Register(ModuleName, 15, "receiver account must match sender account") + // ErrMoneyMarketNotFound error for money market param not found + ErrMoneyMarketNotFound = sdkerrors.Register(ModuleName, 16, "no money market found") + // ErrDepositsNotFound error for no deposits found + ErrDepositsNotFound = sdkerrors.Register(ModuleName, 17, "no deposits found") + // ErrInsufficientLoanToValue error for when an attempted borrow exceeds maximum loan-to-value + ErrInsufficientLoanToValue = sdkerrors.Register(ModuleName, 18, "total deposited value is insufficient for borrow request") + // ErrMarketNotFound error for when a market for the input denom is not found + ErrMarketNotFound = sdkerrors.Register(ModuleName, 19, "no market found for denom") + // ErrPriceNotFound error for when a price for the input market is not found + ErrPriceNotFound = sdkerrors.Register(ModuleName, 20, "no price found for market") ) diff --git a/x/harvest/types/events.go b/x/harvest/types/events.go index bb7840ee..e46c1744 100644 --- a/x/harvest/types/events.go +++ b/x/harvest/types/events.go @@ -8,6 +8,7 @@ const ( EventTypeDeleteHarvestDeposit = "delete_harvest_deposit" EventTypeHarvestWithdrawal = "harvest_withdrawal" EventTypeClaimHarvestReward = "claim_harvest_reward" + EventTypeHarvestBorrow = "harvest_borrow" AttributeValueCategory = ModuleName AttributeKeyBlockHeight = "block_height" AttributeKeyRewardsDistribution = "rewards_distributed" @@ -18,4 +19,7 @@ const ( AttributeKeyClaimHolder = "claim_holder" AttributeKeyClaimAmount = "claim_amount" AttributeKeyClaimMultiplier = "claim_multiplier" + AttributeKeyBorrow = "borrow" + AttributeKeyBorrower = "borrower" + AttributeKeyBorrowCoins = "borrow_coins" ) diff --git a/x/harvest/types/expected_keepers.go b/x/harvest/types/expected_keepers.go index 3960dd2d..7b4d66e5 100644 --- a/x/harvest/types/expected_keepers.go +++ b/x/harvest/types/expected_keepers.go @@ -6,6 +6,8 @@ import ( stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/cosmos-sdk/x/supply/exported" + + pftypes "github.com/kava-labs/kava/x/pricefeed/types" ) // SupplyKeeper defines the expected supply keeper @@ -33,3 +35,8 @@ type StakingKeeper interface { GetBondedPool(ctx sdk.Context) (bondedPool exported.ModuleAccountI) BondDenom(ctx sdk.Context) (res string) } + +// PricefeedKeeper defines the expected interface for the pricefeed +type PricefeedKeeper interface { + GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, error) +} diff --git a/x/harvest/types/keys.go b/x/harvest/types/keys.go index 8fc26c44..d1d5cf3c 100644 --- a/x/harvest/types/keys.go +++ b/x/harvest/types/keys.go @@ -35,6 +35,7 @@ var ( PreviousDelegationDistributionKey = []byte{0x02} DepositsKeyPrefix = []byte{0x03} ClaimsKeyPrefix = []byte{0x04} + BorrowsKeyPrefix = []byte{0x05} sep = []byte(":") ) diff --git a/x/harvest/types/msg.go b/x/harvest/types/msg.go index ae0f3aba..5dfcd5b5 100644 --- a/x/harvest/types/msg.go +++ b/x/harvest/types/msg.go @@ -53,6 +53,7 @@ var ( _ sdk.Msg = &MsgClaimReward{} _ sdk.Msg = &MsgDeposit{} _ sdk.Msg = &MsgWithdraw{} + _ sdk.Msg = &MsgBorrow{} ) // MsgDeposit deposit collateral to the harvest module. @@ -214,3 +215,55 @@ func (msg MsgClaimReward) GetSignBytes() []byte { func (msg MsgClaimReward) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } + +// --------------------------------------- + +// MsgBorrow borrows funds from the harvest module. +type MsgBorrow struct { + Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// NewMsgBorrow returns a new MsgBorrow +func NewMsgBorrow(borrower sdk.AccAddress, amount sdk.Coins) MsgBorrow { + return MsgBorrow{ + Borrower: borrower, + Amount: amount, + } +} + +// Route return the message type used for routing the message. +func (msg MsgBorrow) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgBorrow) Type() string { return "harvest_borrow" } // TODO: or just 'borrow' + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgBorrow) ValidateBasic() error { + if msg.Borrower.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if !msg.Amount.IsValid() || msg.Amount.IsZero() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "borrow amount %s", msg.Amount) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgBorrow) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgBorrow) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Borrower} +} + +// String implements the Stringer interface +func (msg MsgBorrow) String() string { + return fmt.Sprintf(`Borrow Message: + Borrower: %s + Amount: %s +`, msg.Borrower, msg.Amount) +} diff --git a/x/harvest/types/params.go b/x/harvest/types/params.go index 44cc9fb9..f65d00df 100644 --- a/x/harvest/types/params.go +++ b/x/harvest/types/params.go @@ -16,10 +16,12 @@ var ( KeyActive = []byte("Active") KeyLPSchedules = []byte("LPSchedules") KeyDelegatorSchedule = []byte("DelegatorSchedule") + KeyMoneyMarkets = []byte("MoneyMarkets") DefaultActive = true DefaultGovSchedules = DistributionSchedules{} DefaultLPSchedules = DistributionSchedules{} DefaultDelegatorSchedules = DelegatorDistributionSchedules{} + DefaultMoneyMarkets = MoneyMarkets{} GovDenom = cdptypes.DefaultGovDenom ) @@ -28,6 +30,7 @@ type Params struct { Active bool `json:"active" yaml:"active"` LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"` DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"` + MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"` } // DistributionSchedule distribution schedule for liquidity providers @@ -218,18 +221,91 @@ func (ds DistributionSchedule) GetMultiplier(name MultiplierName) (Multiplier, b // Multipliers slice of Multiplier type Multipliers []Multiplier +// BorrowLimit enforces restrictions on a money market +type BorrowLimit struct { + MaximumLimit sdk.Int `json:"maximum_limit" yaml:"maximum_limit"` + LoanToValue sdk.Dec `json:"loan_to_value" yaml:"loan_to_value"` +} + +// NewBorrowLimit returns a new BorrowLimit +func NewBorrowLimit(maximumLimit sdk.Int, loanToValue sdk.Dec) BorrowLimit { + return BorrowLimit{ + MaximumLimit: maximumLimit, + LoanToValue: loanToValue, + } +} + +// Validate BorrowLimit +func (bl BorrowLimit) Validate() error { + if bl.MaximumLimit.IsNegative() { + return fmt.Errorf("maximum limit cannot be negative: %s", bl.MaximumLimit) + } + if !bl.LoanToValue.IsPositive() { + return fmt.Errorf("loan-to-value must be a positive integer: %s", bl.LoanToValue) + } + if bl.LoanToValue.GT(sdk.OneDec()) { + return fmt.Errorf("loan-to-value cannot be greater than 1.0: %s", bl.LoanToValue) + } + return nil +} + +// MoneyMarket is a money market for an individual asset +type MoneyMarket struct { + Denom string `json:"denom" yaml:"denom"` + BorrowLimit BorrowLimit `json:"borrow_limit" yaml:"borrow_limit"` + SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` +} + +// NewMoneyMarket returns a new MoneyMarket +func NewMoneyMarket(denom string, maximumLimit sdk.Int, loanToValue sdk.Dec, + spotMarketID string, conversionFactor sdk.Int) MoneyMarket { + return MoneyMarket{ + Denom: denom, + BorrowLimit: NewBorrowLimit(maximumLimit, loanToValue), + SpotMarketID: spotMarketID, + ConversionFactor: conversionFactor, + } +} + +// Validate MoneyMarket param +func (mm MoneyMarket) Validate() error { + if err := sdk.ValidateDenom(mm.Denom); err != nil { + return err + } + + if err := mm.BorrowLimit.Validate(); err != nil { + return err + } + return nil +} + +// MoneyMarkets slice of MoneyMarket +type MoneyMarkets []MoneyMarket + +// Validate borrow limits +func (mms MoneyMarkets) Validate() error { + for _, moneyMarket := range mms { + if err := moneyMarket.Validate(); err != nil { + return err + } + } + return nil +} + // NewParams returns a new params object -func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules) Params { +func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules, moneyMarkets MoneyMarkets) Params { return Params{ Active: active, LiquidityProviderSchedules: lps, DelegatorDistributionSchedules: dds, + MoneyMarkets: moneyMarkets, } } // DefaultParams returns default params for harvest module func DefaultParams() Params { - return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules) + return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules, DefaultMoneyMarkets) } // String implements fmt.Stringer @@ -237,7 +313,8 @@ func (p Params) String() string { return fmt.Sprintf(`Params: Active: %t Liquidity Provider Distribution Schedules %s - Delegator Distribution Schedule %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules) + Delegator Distribution Schedule %s + Money Markets %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, p.MoneyMarkets) } // ParamKeyTable Key declaration for parameters @@ -251,6 +328,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam), params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams), params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams), + params.NewParamSetPair(KeyMoneyMarkets, &p.MoneyMarkets, validateMoneyMarketParams), } } @@ -300,3 +378,12 @@ func validateDelegatorParams(i interface{}) error { return dds.Validate() } + +func validateMoneyMarketParams(i interface{}) error { + mm, ok := i.(MoneyMarkets) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + return mm.Validate() +} diff --git a/x/harvest/types/querier.go b/x/harvest/types/querier.go index 25ab3d9f..65675cb6 100644 --- a/x/harvest/types/querier.go +++ b/x/harvest/types/querier.go @@ -10,6 +10,7 @@ const ( QueryGetModuleAccounts = "accounts" QueryGetDeposits = "deposits" QueryGetClaims = "claims" + QueryGetBorrows = "borrows" ) // QueryDepositParams is the params for a filtered deposit query @@ -67,3 +68,21 @@ func NewQueryAccountParams(page, limit int, name string) QueryAccountParams { Name: name, } } + +// QueryBorrowParams is the params for a filtered borrow query +type QueryBorrowParams struct { + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + BorrowDenom string `json:"borrow_denom" yaml:"borrow_denom"` +} + +// NewQueryBorrowParams creates a new QueryBorrowParams +func NewQueryBorrowParams(page, limit int, owner sdk.AccAddress, depositDenom string) QueryBorrowParams { + return QueryBorrowParams{ + Page: page, + Limit: limit, + Owner: owner, + BorrowDenom: depositDenom, + } +} diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go index 16836359..b4f4658f 100644 --- a/x/pricefeed/keeper/params.go +++ b/x/pricefeed/keeper/params.go @@ -58,5 +58,4 @@ func (k Keeper) GetMarket(ctx sdk.Context, marketID string) (types.Market, bool) } } return types.Market{}, false - } diff --git a/x/pricefeed/types/market.go b/x/pricefeed/types/market.go index 4a22da43..413eff89 100644 --- a/x/pricefeed/types/market.go +++ b/x/pricefeed/types/market.go @@ -11,7 +11,6 @@ import ( // Market an asset in the pricefeed type Market struct { - // TODO: rename to ID MarketID string `json:"market_id" yaml:"market_id"` BaseAsset string `json:"base_asset" yaml:"base_asset"` QuoteAsset string `json:"quote_asset" yaml:"quote_asset"`