From 4e641c52122c3cad859e09be56b31dab0ae3f047 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Mon, 7 Dec 2020 22:51:35 +0100 Subject: [PATCH] Hard: repay borrowed coins (#725) * add msg borrow test * repay types * register msg repay on codec * repay keeper functionality * repay cli * repay keeper test * Hard: repay insufficient balance error (#726) * repay error: insufficient balance * isolate coin type in error msg * add multi-coin repay example * CalculatePaymentAmount, repay > SyncBorrowInterest * remove todo: index updated by sync * update tests * add back in test --- x/harvest/client/cli/tx.go | 29 +++++ x/harvest/handler.go | 20 +++ x/harvest/keeper/keeper.go | 8 +- x/harvest/keeper/repay.go | 77 +++++++++++ x/harvest/keeper/repay_test.go | 225 +++++++++++++++++++++++++++++++++ x/harvest/types/codec.go | 1 + x/harvest/types/errors.go | 8 +- x/harvest/types/events.go | 3 + x/harvest/types/msg.go | 54 +++++++- x/harvest/types/msg_test.go | 76 +++++++++++ 10 files changed, 493 insertions(+), 8 deletions(-) create mode 100644 x/harvest/keeper/repay.go create mode 100644 x/harvest/keeper/repay_test.go diff --git a/x/harvest/client/cli/tx.go b/x/harvest/client/cli/tx.go index cacd621e..340d0bb3 100644 --- a/x/harvest/client/cli/tx.go +++ b/x/harvest/client/cli/tx.go @@ -34,6 +34,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { getCmdWithdraw(cdc), getCmdClaimReward(cdc), getCmdBorrow(cdc), + getCmdRepay(cdc), )...) return harvestTxCmd @@ -144,3 +145,31 @@ func getCmdBorrow(cdc *codec.Codec) *cobra.Command { }, } } + +func getCmdRepay(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "repay [1000000000ukava]", + Short: "repay tokens to the harvest protocol", + Long: strings.TrimSpace(`repay tokens to the harvest protocol`), + Args: cobra.ExactArgs(1), + Example: fmt.Sprintf( + `%s tx %s repay 1000000000ukava,25000000000bnb --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.NewMsgRepay(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 f84ef16a..bb83c62d 100644 --- a/x/harvest/handler.go +++ b/x/harvest/handler.go @@ -23,6 +23,8 @@ func NewHandler(k Keeper) sdk.Handler { return handleMsgWithdraw(ctx, k, msg) case types.MsgBorrow: return handleMsgBorrow(ctx, k, msg) + case types.MsgRepay: + return handleMsgRepay(ctx, k, msg) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } @@ -100,3 +102,21 @@ func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sd Events: ctx.EventManager().Events(), }, nil } + +func handleMsgRepay(ctx sdk.Context, k keeper.Keeper, msg types.MsgRepay) (*sdk.Result, error) { + err := k.Repay(ctx, msg.Sender, 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.Sender.String()), + ), + ) + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} diff --git a/x/harvest/keeper/keeper.go b/x/harvest/keeper/keeper.go index d7341dc5..b2e29948 100644 --- a/x/harvest/keeper/keeper.go +++ b/x/harvest/keeper/keeper.go @@ -240,8 +240,12 @@ func (k Keeper) IterateBorrows(ctx sdk.Context, cb func(borrow types.Borrow) (st // SetBorrowedCoins sets the total amount of coins currently borrowed in the store func (k Keeper) SetBorrowedCoins(ctx sdk.Context, borrowedCoins sdk.Coins) { store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowedCoinsPrefix) - bz := k.cdc.MustMarshalBinaryBare(borrowedCoins) - store.Set([]byte{}, bz) + if borrowedCoins.Empty() { + store.Set([]byte{}, []byte{}) + } else { + bz := k.cdc.MustMarshalBinaryBare(borrowedCoins) + store.Set([]byte{}, bz) + } } // GetBorrowedCoins returns an sdk.Coins object from the store representing all currently borrowed coins diff --git a/x/harvest/keeper/repay.go b/x/harvest/keeper/repay.go new file mode 100644 index 00000000..e7f88fea --- /dev/null +++ b/x/harvest/keeper/repay.go @@ -0,0 +1,77 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/kava-labs/kava/x/harvest/types" +) + +// Repay borrowed funds +func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { + // Sync interest so loan is up-to-date + k.SyncBorrowInterest(ctx, sender, coins) + + // Validate requested repay + err := k.ValidateRepay(ctx, sender, coins) + if err != nil { + return err + } + + // Check borrow exists here to avoid duplicating store read in ValidateRepay + borrow, found := k.GetBorrow(ctx, sender) + if !found { + return types.ErrBorrowNotFound + } + + payment := k.CalculatePaymentAmount(borrow.Amount, coins) + + // Sends coins from user to Harvest module account + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment) + if err != nil { + return err + } + + // Update user's borrow in store + borrow.Amount = borrow.Amount.Sub(payment) + k.SetBorrow(ctx, borrow) + + // Update total borrowed amount + k.DecrementBorrowedCoins(ctx, payment) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeHarvestRepay, + sdk.NewAttribute(types.AttributeKeySender, sender.String()), + sdk.NewAttribute(types.AttributeKeyRepayCoins, payment.String()), + ), + ) + + return nil +} + +// ValidateRepay validates a requested loan repay +func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { + senderAcc := k.accountKeeper.GetAccount(ctx, sender) + senderCoins := senderAcc.GetCoins() + for _, coin := range coins { + if senderCoins.AmountOf(coin.Denom).LT(coin.Amount) { + return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(coin.Denom), coin.Denom) + } + } + + return nil +} + +// CalculatePaymentAmount prevents overpayment when repaying borrowed coins +func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Coins { + repayment := sdk.Coins{} + for _, coin := range payment { + if coin.Amount.GT(owed.AmountOf(coin.Denom)) { + repayment = append(repayment, sdk.NewCoin(coin.Denom, owed.AmountOf(coin.Denom))) + } else { + repayment = append(repayment, coin) + } + } + return repayment +} diff --git a/x/harvest/keeper/repay_test.go b/x/harvest/keeper/repay_test.go new file mode 100644 index 00000000..1fce8a9d --- /dev/null +++ b/x/harvest/keeper/repay_test.go @@ -0,0 +1,225 @@ +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" + "github.com/kava-labs/kava/x/pricefeed" +) + +func (suite *KeeperTestSuite) TestRepay() { + type args struct { + borrower sdk.AccAddress + initialBorrowerCoins sdk.Coins + initialModuleCoins sdk.Coins + depositCoins []sdk.Coin + borrowCoins sdk.Coins + repayCoins 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: partial repay", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: repay in full", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "valid: overpayment is adjusted", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))}, // Deposit less so user still has some KAVA + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(60*KAVA_CF))), // Exceeds borrowed coins but not user's balance + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "invalid: insufficent balance for repay", + args{ + borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))), + initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), + initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))), + depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))}, + borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))), + repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(51*KAVA_CF))), // Exceeds user's KAVA balance + }, + errArgs{ + expectPass: false, + contains: "account can only repay up to", + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Auth module genesis state + authGS := app.NewAuthGenState( + []sdk.AccAddress{tc.args.borrower}, + []sdk.Coins{tc.args.initialBorrowerCoins}) + + // Harvest module genesis state + 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", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("0.8")), "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")), + }, + ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) + + // Pricefeed module genesis state + pricefeedGS := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + {MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + { + MarketID: "usdx:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("1.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + { + MarketID: "kava:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + + // Initialize test application + tApp.InitializeFromGenesisStates(authGS, + app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}, + app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}, + ) + + // Mint coins to Harvest module account + supplyKeeper := tApp.GetSupplyKeeper() + supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins) + + keeper := tApp.GetHarvestKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + + var err error + + // Deposit coins to harvest + depositedCoins := sdk.NewCoins() + for _, depositCoin := range tc.args.depositCoins { + err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, depositCoin) + suite.Require().NoError(err) + depositedCoins.Add(depositCoin) + } + + // Borrow coins from harvest + err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins) + suite.Require().NoError(err) + + err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.repayCoins) + if tc.errArgs.expectPass { + suite.Require().NoError(err) + // If we overpaid expect an adjustment + repaymentCoins := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins) + + // Check borrower balance + expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins) + acc := suite.getAccount(tc.args.borrower) + suite.Require().Equal(expectedBorrowerCoins, acc.GetCoins()) + + // Check module account balance + expectedModuleCoins := tc.args.initialModuleCoins.Add(tc.args.depositCoins...).Sub(tc.args.borrowCoins).Add(repaymentCoins...) + mAcc := suite.getModuleAccount(types.ModuleAccountName) + suite.Require().Equal(expectedModuleCoins, mAcc.GetCoins()) + + // Check user's borrow object + borrow, _ := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower) + expectedBorrowCoins := tc.args.borrowCoins.Sub(repaymentCoins) + suite.Require().Equal(expectedBorrowCoins, borrow.Amount) + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + + // Check borrower balance (no repay coins) + expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...) + acc := suite.getAccount(tc.args.borrower) + suite.Require().Equal(expectedBorrowerCoins, acc.GetCoins()) + + // Check module account balance (no repay coins) + expectedModuleCoins := tc.args.initialModuleCoins.Add(tc.args.depositCoins...).Sub(tc.args.borrowCoins) + mAcc := suite.getModuleAccount(types.ModuleAccountName) + suite.Require().Equal(expectedModuleCoins, mAcc.GetCoins()) + + // Check user's borrow object (no repay coins) + borrow, _ := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower) + suite.Require().Equal(tc.args.borrowCoins, borrow.Amount) + } + }) + } +} diff --git a/x/harvest/types/codec.go b/x/harvest/types/codec.go index aa722cf9..035bd0a4 100644 --- a/x/harvest/types/codec.go +++ b/x/harvest/types/codec.go @@ -18,5 +18,6 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil) cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil) cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil) + cdc.RegisterConcrete(MsgRepay{}, "harvest/MsgRepay", nil) cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil) } diff --git a/x/harvest/types/errors.go b/x/harvest/types/errors.go index ec37e74f..5906ed2f 100644 --- a/x/harvest/types/errors.go +++ b/x/harvest/types/errors.go @@ -55,8 +55,10 @@ var ( ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation") // ErrBorrowEmptyCoins error for when you cannot borrow empty coins ErrBorrowEmptyCoins = sdkerrors.Register(ModuleName, 25, "cannot borrow zero coins") + // ErrBorrowNotFound error for when a user's borrow is not found in the store + ErrBorrowNotFound = sdkerrors.Register(ModuleName, 26, "borrow not found") // ErrPreviousAccrualTimeNotFound error for no previous accrual time found in store - ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 26, "no previous accrual time found") - // ErrBorrowNotFound error for when borrow not found in store - ErrBorrowNotFound = sdkerrors.Register(ModuleName, 27, "no borrow found") + ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 27, "no previous accrual time found") + // ErrInsufficientBalanceForRepay error for when requested repay exceeds user's balance + ErrInsufficientBalanceForRepay = sdkerrors.Register(ModuleName, 29, "insufficient balance") ) diff --git a/x/harvest/types/events.go b/x/harvest/types/events.go index 7fd31c90..6dcac400 100644 --- a/x/harvest/types/events.go +++ b/x/harvest/types/events.go @@ -9,6 +9,7 @@ const ( EventTypeHarvestWithdrawal = "harvest_withdrawal" EventTypeClaimHarvestReward = "claim_harvest_reward" EventTypeHarvestBorrow = "harvest_borrow" + EventTypeHarvestRepay = "harvest_repay" AttributeValueCategory = ModuleName AttributeKeyBlockHeight = "block_height" AttributeKeyRewardsDistribution = "rewards_distributed" @@ -22,4 +23,6 @@ const ( AttributeKeyBorrow = "borrow" AttributeKeyBorrower = "borrower" AttributeKeyBorrowCoins = "borrow_coins" + AttributeKeySender = "sender" + AttributeKeyRepayCoins = "repay_coins" ) diff --git a/x/harvest/types/msg.go b/x/harvest/types/msg.go index 17131980..cad95154 100644 --- a/x/harvest/types/msg.go +++ b/x/harvest/types/msg.go @@ -209,8 +209,6 @@ 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"` @@ -229,7 +227,7 @@ func NewMsgBorrow(borrower sdk.AccAddress, amount sdk.Coins) MsgBorrow { 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' +func (msg MsgBorrow) Type() string { return "harvest_borrow" } // ValidateBasic does a simple validation check that doesn't require access to any other information. func (msg MsgBorrow) ValidateBasic() error { @@ -260,3 +258,53 @@ func (msg MsgBorrow) String() string { Amount: %s `, msg.Borrower, msg.Amount) } + +// MsgRepay repays funds to the harvest module. +type MsgRepay struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// NewMsgRepay returns a new MsgRepay +func NewMsgRepay(sender sdk.AccAddress, amount sdk.Coins) MsgRepay { + return MsgRepay{ + Sender: sender, + Amount: amount, + } +} + +// Route return the message type used for routing the message. +func (msg MsgRepay) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgRepay) Type() string { return "harvest_repay" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgRepay) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if !msg.Amount.IsValid() || msg.Amount.IsZero() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "repay amount %s", msg.Amount) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgRepay) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgRepay) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +// String implements the Stringer interface +func (msg MsgRepay) String() string { + return fmt.Sprintf(`Repay Message: + Sender: %s + Amount: %s +`, msg.Sender, msg.Amount) +} diff --git a/x/harvest/types/msg_test.go b/x/harvest/types/msg_test.go index 99e27bf2..598dc7d6 100644 --- a/x/harvest/types/msg_test.go +++ b/x/harvest/types/msg_test.go @@ -192,6 +192,82 @@ func (suite *MsgTestSuite) TestMsgClaim() { } } +func (suite *MsgTestSuite) TestMsgBorrow() { + type args struct { + borrower sdk.AccAddress + amount sdk.Coins + } + addrs := []sdk.AccAddress{ + sdk.AccAddress("test1"), + } + testCases := []struct { + name string + args args + expectPass bool + expectedErr string + }{ + { + name: "valid", + args: args{ + borrower: addrs[0], + amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))), + }, + expectPass: true, + expectedErr: "", + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := types.NewMsgBorrow(tc.args.borrower, tc.args.amount) + err := msg.ValidateBasic() + if tc.expectPass { + suite.NoError(err) + } else { + suite.Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.expectedErr)) + } + }) + } +} + +func (suite *MsgTestSuite) TestMsgRepay() { + type args struct { + sender sdk.AccAddress + amount sdk.Coins + } + addrs := []sdk.AccAddress{ + sdk.AccAddress("test1"), + } + testCases := []struct { + name string + args args + expectPass bool + expectedErr string + }{ + { + name: "valid", + args: args{ + sender: addrs[0], + amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))), + }, + expectPass: true, + expectedErr: "", + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + msg := types.NewMsgRepay(tc.args.sender, tc.args.amount) + err := msg.ValidateBasic() + if tc.expectPass { + suite.NoError(err) + } else { + suite.Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.expectedErr)) + } + }) + } +} + func TestMsgTestSuite(t *testing.T) { suite.Run(t, new(MsgTestSuite)) }