diff --git a/x/incentive/alias.go b/x/incentive/alias.go index f916f791..99e7578d 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -26,6 +26,7 @@ const ( EventTypeRewardPeriod = types.EventTypeRewardPeriod HardLiquidityProviderClaimType = types.HardLiquidityProviderClaimType Large = types.Large + MaxDenomsToClaim = types.MaxDenomsToClaim Medium = types.Medium ModuleName = types.ModuleName QuerierRoute = types.QuerierRoute @@ -53,7 +54,9 @@ var ( NewQuerier = keeper.NewQuerier DefaultGenesisState = types.DefaultGenesisState DefaultParams = types.DefaultParams + FilterCoins = types.FilterCoins GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength + MultiplyCoins = types.MultiplyCoins NewAccumulator = types.NewAccumulator NewDelegatorClaim = types.NewDelegatorClaim NewGenesisAccumulationTime = types.NewGenesisAccumulationTime @@ -63,6 +66,8 @@ var ( NewMsgClaimDelegatorRewardVVesting = types.NewMsgClaimDelegatorRewardVVesting NewMsgClaimHardReward = types.NewMsgClaimHardReward NewMsgClaimHardRewardVVesting = types.NewMsgClaimHardRewardVVesting + NewMsgClaimSwapReward = types.NewMsgClaimSwapReward + NewMsgClaimSwapRewardVVesting = types.NewMsgClaimSwapRewardVVesting NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward NewMsgClaimUSDXMintingRewardVVesting = types.NewMsgClaimUSDXMintingRewardVVesting NewMultiRewardIndex = types.NewMultiRewardIndex @@ -98,6 +103,7 @@ var ( ErrDecreasingRewardFactor = types.ErrDecreasingRewardFactor ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance ErrInvalidAccountType = types.ErrInvalidAccountType + ErrInvalidClaimDenoms = types.ErrInvalidClaimDenoms ErrInvalidClaimOwner = types.ErrInvalidClaimOwner ErrInvalidClaimType = types.ErrInvalidClaimType ErrInvalidMultiplier = types.ErrInvalidMultiplier @@ -107,7 +113,6 @@ var ( GovDenom = types.GovDenom HardBorrowRewardIndexesKeyPrefix = types.HardBorrowRewardIndexesKeyPrefix HardLiquidityClaimKeyPrefix = types.HardLiquidityClaimKeyPrefix - HardLiquidityRewardDenom = types.HardLiquidityRewardDenom HardSupplyRewardIndexesKeyPrefix = types.HardSupplyRewardIndexesKeyPrefix IncentiveMacc = types.IncentiveMacc KeyClaimEnd = types.KeyClaimEnd @@ -155,6 +160,8 @@ type ( MsgClaimDelegatorRewardVVesting = types.MsgClaimDelegatorRewardVVesting MsgClaimHardReward = types.MsgClaimHardReward MsgClaimHardRewardVVesting = types.MsgClaimHardRewardVVesting + MsgClaimSwapReward = types.MsgClaimSwapReward + MsgClaimSwapRewardVVesting = types.MsgClaimSwapRewardVVesting MsgClaimUSDXMintingReward = types.MsgClaimUSDXMintingReward MsgClaimUSDXMintingRewardVVesting = types.MsgClaimUSDXMintingRewardVVesting MultiRewardIndex = types.MultiRewardIndex diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go index 9c414097..211bb269 100644 --- a/x/incentive/client/cli/tx.go +++ b/x/incentive/client/cli/tx.go @@ -32,23 +32,21 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { getCmdClaimHardVVesting(cdc), getCmdClaimDelegator(cdc), getCmdClaimDelegatorVVesting(cdc), + getCmdClaimSwap(cdc), + getCmdClaimSwapVVesting(cdc), )...) return incentiveTxCmd } func getCmdClaimCdp(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-cdp [multiplier]", - Short: "claim CDP rewards using a given multiplier", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding CDP rewards using a given multiplier - Example: - $ %s tx %s claim-cdp large - `, version.ClientName, types.ModuleName), - ), - Args: cobra.ExactArgs(1), + cmd := &cobra.Command{ + Use: "claim-cdp [multiplier]", + Short: "claim USDX minting rewards using a given multiplier", + Long: `Claim sender's outstanding USDX minting rewards using a given multiplier.`, + Example: fmt.Sprintf(` $ %s tx %s claim-cdp large`, version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -58,27 +56,25 @@ func getCmdClaimCdp(cdc *codec.Codec) *cobra.Command { multiplier := args[0] msg := types.NewMsgClaimUSDXMintingReward(sender, multiplier) - err := msg.ValidateBasic() - if err != nil { + if err := msg.ValidateBasic(); err != nil { return err } return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } + + return cmd } func getCmdClaimCdpVVesting(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-cdp-vesting [multiplier] [receiver]", - Short: "claim CDP rewards using a given multiplier on behalf of a validator vesting account", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding CDP rewards on behalf of a validator vesting using a given multiplier - Example: - $ %s tx %s claim-cdp-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw - `, version.ClientName, types.ModuleName), - ), - Args: cobra.ExactArgs(2), + cmd := &cobra.Command{ + Use: "claim-cdp-vesting [multiplier] [receiver]", + Short: "claim USDX minting rewards using a given multiplier on behalf of a validator vesting account", + Long: `Claim sender's outstanding USDX minting rewards on behalf of a validator vesting using a given multiplier. +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens.`, + Example: fmt.Sprintf(` $ %s tx %s claim-cdp-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw`, version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -93,62 +89,25 @@ func getCmdClaimCdpVVesting(cdc *codec.Codec) *cobra.Command { } msg := types.NewMsgClaimUSDXMintingRewardVVesting(sender, receiver, multiplier) - err = msg.ValidateBasic() - if err != nil { + if err := msg.ValidateBasic(); err != nil { return err } return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } -} -func getCmdClaimHardVVesting(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-hard-vesting [multiplier] [receiver]", - Short: "claim Hard module rewards on behalf of a validator vesting account using a given multiplier", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding Hard rewards on behalf of a validator vesting account for deposit/borrow/delegate using given multiplier - - Example: - $ %s tx %s claim-hard-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw - `, version.ClientName, types.ModuleName), - ), - Args: cobra.ExactArgs(2), - 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)) - - sender := cliCtx.GetFromAddress() - multiplier := args[0] - receiverStr := args[1] - receiver, err := sdk.AccAddressFromBech32(receiverStr) - if err != nil { - return err - } - - msg := types.NewMsgClaimHardRewardVVesting(sender, receiver, multiplier) - err = msg.ValidateBasic() - if err != nil { - return err - } - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) - }, - } + return cmd } func getCmdClaimHard(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-hard [multiplier]", - Short: "claim sender's Hard module rewards using a given multiplier", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding Hard rewards for deposit/borrow/delegate using given multiplier + var denomsToClaim []string - Example: - $ %s tx %s claim-hard large - `, version.ClientName, types.ModuleName), - ), - Args: cobra.ExactArgs(1), + cmd := &cobra.Command{ + Use: "claim-hard [multiplier]", + Short: "claim sender's Hard module rewards using a given multiplier", + Long: `Claim sender's outstanding Hard rewards for deposit/borrow using given multiplier`, + Example: fmt.Sprintf(` $ %s tx %s claim-hard large`, version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -157,57 +116,31 @@ func getCmdClaimHard(cdc *codec.Codec) *cobra.Command { sender := cliCtx.GetFromAddress() multiplier := args[0] - msg := types.NewMsgClaimHardReward(sender, multiplier) - err := msg.ValidateBasic() - if err != nil { + msg := types.NewMsgClaimHardReward(sender, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { return err } return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd } -func getCmdClaimDelegator(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-delegator [multiplier]", - Short: "claim sender's delegator rewards using a given multiplier", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding delegator rewards using given multiplier +func getCmdClaimHardVVesting(cdc *codec.Codec) *cobra.Command { + var denomsToClaim []string - Example: - $ %s tx %s claim-delegator large - `, version.ClientName, types.ModuleName), - ), - Args: cobra.ExactArgs(1), - 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)) - - sender := cliCtx.GetFromAddress() - multiplier := args[0] - - msg := types.NewMsgClaimDelegatorReward(sender, multiplier) - err := msg.ValidateBasic() - if err != nil { - return err - } - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) - }, - } -} - -func getCmdClaimDelegatorVVesting(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "claim-delegator-vesting [multiplier] [receiver]", - Short: "claim delegator rewards on behalf of a validator vesting account using a given multiplier", - Long: strings.TrimSpace( - fmt.Sprintf(`Claim sender's outstanding delegator rewards on behalf of a validator vesting account using given multiplier - - Example: - $ %s tx %s claim-delegator-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw - `, version.ClientName, types.ModuleName), - ), + cmd := &cobra.Command{ + Use: "claim-hard-vesting [multiplier] [receiver]", + Short: "claim Hard module rewards on behalf of a validator vesting account using a given multiplier", + Long: `Claim sender's outstanding hard supply/borrow rewards on behalf of a validator vesting account using given multiplier +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens. +Optionally claim only certain denoms from the rewards. Specifying none will claim all of them.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-hard-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", version.ClientName, types.ModuleName), + fmt.Sprintf(" $ %s tx %s claim-hard-vesting small kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --claim-only swp,hard", version.ClientName, types.ModuleName), + }, "\n"), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) @@ -222,12 +155,158 @@ func getCmdClaimDelegatorVVesting(cdc *codec.Codec) *cobra.Command { return err } - msg := types.NewMsgClaimDelegatorRewardVVesting(sender, receiver, multiplier) - err = msg.ValidateBasic() - if err != nil { + msg := types.NewMsgClaimHardRewardVVesting(sender, receiver, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { return err } return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd +} + +func getCmdClaimDelegator(cdc *codec.Codec) *cobra.Command { + var denomsToClaim []string + + cmd := &cobra.Command{ + Use: "claim-delegator [multiplier]", + Short: "claim sender's delegator rewards using a given multiplier", + Long: `Claim sender's outstanding delegator rewards using given multiplier. +Optionally claim only certain denoms from the rewards. Specifying none will claim all of them.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-delegator large", version.ClientName, types.ModuleName), + fmt.Sprintf(" $ %s tx %s claim-delegator large --claim-only swp,hard", version.ClientName, types.ModuleName), + }, "\n"), + Args: cobra.ExactArgs(1), + 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)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + + msg := types.NewMsgClaimDelegatorReward(sender, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd +} + +func getCmdClaimDelegatorVVesting(cdc *codec.Codec) *cobra.Command { + var denomsToClaim []string + + cmd := &cobra.Command{ + Use: "claim-delegator-vesting [multiplier] [receiver]", + Short: "claim delegator rewards on behalf of a validator vesting account using a given multiplier", + Long: `Claim sender's outstanding delegator rewards on behalf of a validator vesting account using given multiplier +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens. +Optionally claim only certain denoms from the rewards. Specifying none will claim all of them.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-delegator-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", version.ClientName, types.ModuleName), + fmt.Sprintf(" $ %s tx %s claim-delegator-vesting small kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --claim-only swp,hard", version.ClientName, types.ModuleName), + }, "\n"), + Args: cobra.ExactArgs(2), + 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)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + receiverStr := args[1] + receiver, err := sdk.AccAddressFromBech32(receiverStr) + if err != nil { + return err + } + + msg := types.NewMsgClaimDelegatorRewardVVesting(sender, receiver, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd +} + +func getCmdClaimSwap(cdc *codec.Codec) *cobra.Command { + var denomsToClaim []string + + cmd := &cobra.Command{ + Use: "claim-swap [multiplier]", + Short: "claim sender's swap rewards using a given multiplier", + Long: `Claim sender's outstanding swap rewards using given multiplier. +Optionally claim only certain denoms from the rewards. Specifying none will claim all of them.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-swap large", version.ClientName, types.ModuleName), + fmt.Sprintf(" $ %s tx %s claim-swap large --claim-only swp,hard", version.ClientName, types.ModuleName), + }, "\n"), + Args: cobra.ExactArgs(1), + 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)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + + msg := types.NewMsgClaimSwapReward(sender, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd +} + +func getCmdClaimSwapVVesting(cdc *codec.Codec) *cobra.Command { + var denomsToClaim []string + + cmd := &cobra.Command{ + Use: "claim-swap-vesting [multiplier] [receiver]", + Short: "claim swap rewards on behalf of a validator vesting account using a given multiplier", + Long: `Claim sender's outstanding swap rewards on behalf of a validator vesting account using given multiplier +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens. +Optionally claim only certain denoms from the rewards. Specifying none will claim all of them.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-swap-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", version.ClientName, types.ModuleName), + fmt.Sprintf(" $ %s tx %s claim-swap-vesting small kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --claim-only swp,hard", version.ClientName, types.ModuleName), + }, "\n"), + Args: cobra.ExactArgs(2), + 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)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + receiverStr := args[1] + receiver, err := sdk.AccAddressFromBech32(receiverStr) + if err != nil { + return err + } + + msg := types.NewMsgClaimSwapRewardVVesting(sender, receiver, multiplier, denomsToClaim) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringSliceVar(&denomsToClaim, "claim-only", nil, "claim only these denoms, otherwise claim all denoms") + + return cmd } diff --git a/x/incentive/client/rest/rest.go b/x/incentive/client/rest/rest.go index d17af7bc..35fa2d04 100644 --- a/x/incentive/client/rest/rest.go +++ b/x/incentive/client/rest/rest.go @@ -25,6 +25,7 @@ type PostClaimReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` Sender sdk.AccAddress `json:"sender" yaml:"sender"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // PostClaimReq defines the properties of claim transaction's request body. @@ -33,4 +34,5 @@ type PostClaimVVestingReq struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } diff --git a/x/incentive/client/rest/tx.go b/x/incentive/client/rest/tx.go index 884b1c18..ee00b276 100644 --- a/x/incentive/client/rest/tx.go +++ b/x/incentive/client/rest/tx.go @@ -16,15 +16,33 @@ import ( ) func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc("/incentive/claim-cdp", postClaimCdpHandlerFn(cliCtx)).Methods("POST") - r.HandleFunc("/incentive/claim-cdp-vesting", postClaimCdpVVestingHandlerFn(cliCtx)).Methods("POST") - r.HandleFunc("/incentive/claim-hard", postClaimHardHandlerFn(cliCtx)).Methods("POST") - r.HandleFunc("/incentive/claim-hard-vesting", postClaimHardVVestingHandlerFn(cliCtx)).Methods("POST") - r.HandleFunc("/incentive/claim-delegator", postClaimDelegatorHandlerFn(cliCtx)).Methods("POST") - r.HandleFunc("/incentive/claim-delegator-vesting", postClaimDelegatorVVestingHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/incentive/claim-cdp", postClaimHandlerFn(cliCtx, usdxMintingGenerator)).Methods("POST") + r.HandleFunc("/incentive/claim-cdp-vesting", postClaimVVestingHandlerFn(cliCtx, usdxMintingVVGenerator)).Methods("POST") + + r.HandleFunc("/incentive/claim-hard", postClaimHandlerFn(cliCtx, hardGenerator)).Methods("POST") + r.HandleFunc("/incentive/claim-hard-vesting", postClaimVVestingHandlerFn(cliCtx, hardVVGenerator)).Methods("POST") + + r.HandleFunc("/incentive/claim-delegator", postClaimHandlerFn(cliCtx, delegatorGenerator)).Methods("POST") + r.HandleFunc("/incentive/claim-delegator-vesting", postClaimVVestingHandlerFn(cliCtx, delegatorVVGenerator)).Methods("POST") + + r.HandleFunc("/incentive/claim-swap", postClaimHandlerFn(cliCtx, swapGenerator)).Methods("POST") + r.HandleFunc("/incentive/claim-swap-vesting", postClaimVVestingHandlerFn(cliCtx, swapVVGenerator)).Methods("POST") } -func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func usdxMintingGenerator(req PostClaimReq) sdk.Msg { + return types.NewMsgClaimUSDXMintingReward(req.Sender, req.MultiplierName) +} +func hardGenerator(req PostClaimReq) sdk.Msg { + return types.NewMsgClaimHardReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +} +func delegatorGenerator(req PostClaimReq) sdk.Msg { + return types.NewMsgClaimDelegatorReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +} +func swapGenerator(req PostClaimReq) sdk.Msg { + return types.NewMsgClaimSwapReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +} + +func postClaimHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimReq) sdk.Msg) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody PostClaimReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { @@ -47,7 +65,7 @@ func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgClaimUSDXMintingReward(requestBody.Sender, requestBody.MultiplierName) + msg := msgGenerator(requestBody) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return @@ -57,7 +75,20 @@ func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -func postClaimCdpVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func usdxMintingVVGenerator(req PostClaimVVestingReq) sdk.Msg { + return types.NewMsgClaimUSDXMintingRewardVVesting(req.Sender, req.Receiver, req.MultiplierName) +} +func hardVVGenerator(req PostClaimVVestingReq) sdk.Msg { + return types.NewMsgClaimHardRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +} +func delegatorVVGenerator(req PostClaimVVestingReq) sdk.Msg { + return types.NewMsgClaimDelegatorRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +} +func swapVVGenerator(req PostClaimVVestingReq) sdk.Msg { + return types.NewMsgClaimSwapRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +} + +func postClaimVVestingHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimVVestingReq) sdk.Msg) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody PostClaimVVestingReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { @@ -80,139 +111,7 @@ func postClaimCdpVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgClaimUSDXMintingRewardVVesting(requestBody.Sender, requestBody.Receiver, requestBody.MultiplierName) - if err := msg.ValidateBasic(); err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) - } -} - -func postClaimHardHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var requestBody PostClaimReq - if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { - return - } - - requestBody.BaseReq = requestBody.BaseReq.Sanitize() - if !requestBody.BaseReq.ValidateBasic(w) { - return - } - - fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - if !bytes.Equal(fromAddr, requestBody.Sender) { - rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) - return - } - - msg := types.NewMsgClaimHardReward(requestBody.Sender, requestBody.MultiplierName) - if err := msg.ValidateBasic(); err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) - } -} - -func postClaimHardVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var requestBody PostClaimVVestingReq - if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { - return - } - - requestBody.BaseReq = requestBody.BaseReq.Sanitize() - if !requestBody.BaseReq.ValidateBasic(w) { - return - } - - fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - if !bytes.Equal(fromAddr, requestBody.Sender) { - rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) - return - } - - msg := types.NewMsgClaimHardRewardVVesting(requestBody.Sender, requestBody.Receiver, requestBody.MultiplierName) - if err := msg.ValidateBasic(); err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) - } -} - -func postClaimDelegatorHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var requestBody PostClaimReq - if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { - return - } - - requestBody.BaseReq = requestBody.BaseReq.Sanitize() - if !requestBody.BaseReq.ValidateBasic(w) { - return - } - - fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - if !bytes.Equal(fromAddr, requestBody.Sender) { - rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) - return - } - - msg := types.NewMsgClaimDelegatorReward(requestBody.Sender, requestBody.MultiplierName) - if err := msg.ValidateBasic(); err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) - } -} - -func postClaimDelegatorVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var requestBody PostClaimVVestingReq - if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { - return - } - - requestBody.BaseReq = requestBody.BaseReq.Sanitize() - if !requestBody.BaseReq.ValidateBasic(w) { - return - } - - fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - if !bytes.Equal(fromAddr, requestBody.Sender) { - rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) - return - } - - msg := types.NewMsgClaimDelegatorRewardVVesting(requestBody.Sender, requestBody.Receiver, requestBody.MultiplierName) + msg := msgGenerator(requestBody) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/incentive/genesis_test.go b/x/incentive/genesis_test.go index 74c63428..336c8cb4 100644 --- a/x/incentive/genesis_test.go +++ b/x/incentive/genesis_test.go @@ -85,7 +85,7 @@ func (suite *GenesisTestSuite) SetupTest() { app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(incentiveGS)}, app.GenesisState{hard.ModuleName: hard.ModuleCdc.MustMarshalJSON(hardGS)}, NewCDPGenStateMulti(), - NewPricefeedGenStateMulti(), + NewPricefeedGenStateMultiFromTime(suite.genesisTime), ) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) @@ -115,7 +115,7 @@ func (suite *GenesisTestSuite) TestPaidOutClaimsPassValidateGenesis() { suite.Require().NoError(err) incentiveHandler := incentive.NewHandler(suite.keeper) - _, err = incentiveHandler(suite.ctx, incentive.NewMsgClaimHardReward(suite.addrs[0], string(incentive.Large))) + _, err = incentiveHandler(suite.ctx, incentive.NewMsgClaimHardReward(suite.addrs[0], string(incentive.Large), nil)) suite.Require().NoError(err) genState := incentive.ExportGenesis(suite.ctx, suite.keeper) diff --git a/x/incentive/handler.go b/x/incentive/handler.go index a5c02ebf..be44db0e 100644 --- a/x/incentive/handler.go +++ b/x/incentive/handler.go @@ -25,6 +25,10 @@ func NewHandler(k keeper.Keeper) sdk.Handler { return handleMsgClaimDelegatorReward(ctx, k, msg) case types.MsgClaimDelegatorRewardVVesting: return handleMsgClaimDelegatorRewardVVesting(ctx, k, msg) + case types.MsgClaimSwapReward: + return handleMsgClaimSwapReward(ctx, k, msg) + case types.MsgClaimSwapRewardVVesting: + return handleMsgClaimSwapRewardVVesting(ctx, k, msg) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } @@ -32,7 +36,7 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingReward) (*sdk.Result, error) { - err := k.ClaimUSDXMintingReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimUSDXMintingReward(ctx, msg.Sender, msg.Sender, types.MultiplierName(msg.MultiplierName)) if err != nil { return nil, err } @@ -43,7 +47,10 @@ func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types func handleMsgClaimUSDXMintingRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingRewardVVesting) (*sdk.Result, error) { - err := k.ClaimUSDXMintingRewardVVesting(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + if err := k.ValidateIsValidatorVestingAccount(ctx, msg.Sender); err != nil { + return nil, err + } + err := k.ClaimUSDXMintingReward(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) if err != nil { return nil, err } @@ -54,7 +61,7 @@ func handleMsgClaimUSDXMintingRewardVVesting(ctx sdk.Context, k keeper.Keeper, m func handleMsgClaimHardReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardReward) (*sdk.Result, error) { - err := k.ClaimHardReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimHardReward(ctx, msg.Sender, msg.Sender, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) if err != nil { return nil, err } @@ -65,7 +72,10 @@ func handleMsgClaimHardReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgCla func handleMsgClaimHardRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardRewardVVesting) (*sdk.Result, error) { - err := k.ClaimHardRewardVVesting(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + if err := k.ValidateIsValidatorVestingAccount(ctx, msg.Sender); err != nil { + return nil, err + } + err := k.ClaimHardReward(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) if err != nil { return nil, err } @@ -76,7 +86,7 @@ func handleMsgClaimHardRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg type func handleMsgClaimDelegatorReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimDelegatorReward) (*sdk.Result, error) { - err := k.ClaimDelegatorReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimDelegatorReward(ctx, msg.Sender, msg.Sender, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) if err != nil { return nil, err } @@ -87,7 +97,35 @@ func handleMsgClaimDelegatorReward(ctx sdk.Context, k keeper.Keeper, msg types.M func handleMsgClaimDelegatorRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimDelegatorRewardVVesting) (*sdk.Result, error) { - err := k.ClaimDelegatorRewardVVesting(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + if err := k.ValidateIsValidatorVestingAccount(ctx, msg.Sender); err != nil { + return nil, err + } + err := k.ClaimDelegatorReward(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) + if err != nil { + return nil, err + } + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} + +func handleMsgClaimSwapReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimSwapReward) (*sdk.Result, error) { + + err := k.ClaimSwapReward(ctx, msg.Sender, msg.Sender, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) + if err != nil { + return nil, err + } + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} + +func handleMsgClaimSwapRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimSwapRewardVVesting) (*sdk.Result, error) { + + if err := k.ValidateIsValidatorVestingAccount(ctx, msg.Sender); err != nil { + return nil, err + } + err := k.ClaimSwapReward(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) if err != nil { return nil, err } diff --git a/x/incentive/handler_delegator_test.go b/x/incentive/handler_delegator_test.go new file mode 100644 index 00000000..2929c9ab --- /dev/null +++ b/x/incentive/handler_delegator_test.go @@ -0,0 +1,201 @@ +package incentive_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *HandlerTestSuite) TestPayoutDelegatorClaim() { + userAddr := suite.addrs[0] + receiverAddr := suite.addrs[1] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("ukava", 1e12))). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a delegation (need to create a validator first, which will have a self delegation) + suite.NoError( + suite.DeliverMsgCreateValidator(sdk.ValAddress(userAddr), c("ukava", 1e9)), + ) + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + // Now the delegation is bonded, accumulate some delegator rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorRewardVVesting(userAddr, receiverAddr, "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim a single denom + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorReward(userAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := cs(c("hard", 2*7*1e6)) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004786, Amount: expectedRewards}, + }) + // Check that claimed coins have been removed from a claim's reward + suite.DelegatorRewardEquals(userAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutDelegatorClaimSingleDenom() { + userAddr := suite.addrs[0] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("ukava", 1e12))) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a delegation (need to create a validator first, which will have a self delegation) + suite.NoError( + suite.DeliverMsgCreateValidator(sdk.ValAddress(userAddr), c("ukava", 1e9)), + ) + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + // Now the delegation is bonded, accumulate some delegator rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorRewardVVesting(userAddr, suite.addrs[1], "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorReward(userAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 2*7*1e6) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004786, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.DelegatorRewardEquals(userAddr, cs(c("hard", 2*7*1e6))) +} + +func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVesting() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a delegation (need to create a validator first, which will have a self delegation) + suite.NoError( + suite.DeliverMsgCreateValidator(sdk.ValAddress(valAddr), c("ukava", 1e9)), + ) + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + // Now the delegation is bonded, accumulate some delegator rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorReward(valAddr, "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("hard", 2*7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004786, Amount: cs(expectedRewards)}, + }) + + // Check that each claim reward coin's amount has been reset to 0 + suite.DelegatorRewardEquals(valAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVestingSingleDenom() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a delegation (need to create a validator first, which will have a self delegation) + suite.NoError( + suite.DeliverMsgCreateValidator(sdk.ValAddress(valAddr), c("ukava", 1e9)), + ) + // new block required to bond validator + suite.NextBlockAfter(7 * time.Second) + // Now the delegation is bonded, accumulate some delegator rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorReward(valAddr, "large", []string{"swap"}), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 2*7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004786, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.DelegatorRewardEquals(valAddr, cs(c("hard", 2*7*1e6))) +} diff --git a/x/incentive/handler_hard_test.go b/x/incentive/handler_hard_test.go new file mode 100644 index 00000000..a431b810 --- /dev/null +++ b/x/incentive/handler_hard_test.go @@ -0,0 +1,195 @@ +package incentive_test + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *HandlerTestSuite) TestPayoutHardClaim() { + userAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("bnb", 1e12))). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a deposit and borrow + suite.NoError(suite.DeliverHardMsgDeposit(userAddr, cs(c("bnb", 1e11)))) + suite.NoError(suite.DeliverHardMsgBorrow(userAddr, cs(c("bnb", 1e10)))) + + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimHardRewardVVesting(userAddr, receiverAddr, "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim a single denom + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimHardReward(userAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := cs(c("hard", 2*7*1e6)) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004793, Amount: expectedRewards}, + }) + // Check that claimed coins have been removed from a claim's reward + suite.HardRewardEquals(userAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutHardClaimSingleDenom() { + userAddr := suite.addrs[0] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("bnb", 1e12))) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a deposit and borrow + suite.NoError(suite.DeliverHardMsgDeposit(userAddr, cs(c("bnb", 1e11)))) + suite.NoError(suite.DeliverHardMsgBorrow(userAddr, cs(c("bnb", 1e10)))) + + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimHardRewardVVesting(userAddr, suite.addrs[1], "large", []string{"swap"}), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimHardReward(userAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 2*7*1e6) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.HardRewardEquals(userAddr, cs(c("hard", 2*7*1e6))) +} + +func (suite *HandlerTestSuite) TestPayoutHardClaimVVesting() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("bnb", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a deposit and borrow + suite.NoError(suite.DeliverHardMsgDeposit(valAddr, cs(c("bnb", 1e11)))) + suite.NoError(suite.DeliverHardMsgBorrow(valAddr, cs(c("bnb", 1e10)))) + + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimHardReward(valAddr, "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("hard", 2*7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that each claim reward coin's amount has been reset to 0 + suite.HardRewardEquals(valAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutHardClaimVVestingSingleDenom() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("bnb", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // create a deposit and borrow + suite.NoError(suite.DeliverHardMsgDeposit(valAddr, cs(c("bnb", 1e11)))) + suite.NoError(suite.DeliverHardMsgBorrow(valAddr, cs(c("bnb", 1e10)))) + + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimHardReward(valAddr, "large", []string{"swap"}), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 2*7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.HardRewardEquals(valAddr, cs(c("hard", 2*7*1e6))) +} diff --git a/x/incentive/handler_swap_test.go b/x/incentive/handler_swap_test.go new file mode 100644 index 00000000..d40df7de --- /dev/null +++ b/x/incentive/handler_swap_test.go @@ -0,0 +1,298 @@ +package incentive_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper" + hardkeeper "github.com/kava-labs/kava/x/hard/keeper" + "github.com/kava-labs/kava/x/incentive" + "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" + "github.com/kava-labs/kava/x/incentive/types" + "github.com/kava-labs/kava/x/kavadist" + validatorvesting "github.com/kava-labs/kava/x/validator-vesting" +) + +// Test suite used for all keeper tests +type HandlerTestSuite struct { + testutil.IntegrationTester + + // TODO remove these + keeper keeper.Keeper + hardKeeper hardkeeper.Keeper + cdpKeeper cdpkeeper.Keeper + handler sdk.Handler + + genesisTime time.Time + addrs []sdk.AccAddress +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} + +// SetupTest is run automatically before each suite test +func (suite *HandlerTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + _, suite.addrs = app.GeneratePrivKeyAddressPairs(5) + + suite.genesisTime = time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC) +} + +func (suite *HandlerTestSuite) SetupApp() { + suite.App = app.NewTestApp() + + suite.keeper = suite.App.GetIncentiveKeeper() + suite.hardKeeper = suite.App.GetHardKeeper() + suite.cdpKeeper = suite.App.GetCDPKeeper() + suite.handler = incentive.NewHandler(suite.keeper) + + suite.Ctx = suite.App.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) +} + +type genesisBuilder interface { + BuildMarshalled() app.GenesisState +} + +func (suite *HandlerTestSuite) SetupWithGenState(builders ...genesisBuilder) { + suite.SetupApp() + + builtGenStates := []app.GenesisState{ + NewStakingGenesisState(), + NewPricefeedGenStateMultiFromTime(suite.genesisTime), + NewCDPGenStateMulti(), + NewHardGenStateMulti(suite.genesisTime).BuildMarshalled(), + NewSwapGenesisState(), + } + for _, builder := range builders { + builtGenStates = append(builtGenStates, builder.BuildMarshalled()) + } + + suite.App.InitializeFromGenesisStatesWithTime( + suite.genesisTime, + builtGenStates..., + ) +} + +// for the purposes of incentive module. A validator vesting account only needs to exist, and have enough balance to delegate/or supply. +func (suite *HandlerTestSuite) NewValidatorVestingAccountWithBalance(address sdk.AccAddress, spendableBalance sdk.Coins) *validatorvesting.ValidatorVestingAccount { + bacc := auth.NewBaseAccount(address, spendableBalance, nil, 0, 0) + // vesting coins set to nil and vesting end time set to genesis full base account balance should be spendable + bva, err := vesting.NewBaseVestingAccount(bacc, nil, suite.genesisTime.Unix()) + if err != nil { + panic(err.Error()) + } + // vesting start time set to genesis and no vesting periods + return validatorvesting.NewValidatorVestingAccountRaw(bva, suite.genesisTime.Unix(), nil, sdk.ConsAddress{}, nil, 90) +} + +// authBuilder returns a new auth genesis builder with a full kavadist module account. +func (suite *HandlerTestSuite) authBuilder() app.AuthGenesisBuilder { + return app.NewAuthGenesisBuilder(). + WithSimpleModuleAccount(kavadist.ModuleName, cs(c(types.USDXMintingRewardDenom, 1e18), c("hard", 1e18), c("swap", 1e18))) +} + +// incentiveBuilder returns a new incentive genesis builder with a genesis time and multipliers set +func (suite *HandlerTestSuite) incentiveBuilder() testutil.IncentiveGenesisBuilder { + return testutil.NewIncentiveGenesisBuilder(). + WithGenesisTime(suite.genesisTime). + WithMultipliers(types.Multipliers{ + types.NewMultiplier(types.MultiplierName("small"), 1, d("0.2")), + types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0")), + }) +} + +func (suite *HandlerTestSuite) TestPayoutSwapClaim() { + userAddr := suite.addrs[0] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("ukava", 1e12), c("busd", 1e12))) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSwapRewardPeriod("busd/ukava", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // deposit into a swap pool + suite.NoError( + suite.DeliverSwapMsgDeposit(userAddr, c("ukava", 1e9), c("busd", 1e9), d("1.0")), + ) + // accumulate some swap rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapReward(userAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := cs(c("swap", 7*1e6), c("hard", 7*1e6)) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004793, Amount: expectedRewards}, + }) + + // Check that each claim reward coin's amount has been reset to 0 + suite.SwapRewardEquals(userAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutSwapClaimSingleDenom() { + userAddr := suite.addrs[0] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("ukava", 1e12), c("busd", 1e12))) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSwapRewardPeriod("busd/ukava", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // deposit into a swap pool + suite.NoError( + suite.DeliverSwapMsgDeposit(userAddr, c("ukava", 1e9), c("busd", 1e9), d("1.0")), + ) + + // accumulate some swap rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapReward(userAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 7*1e6) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.SwapRewardEquals(userAddr, cs(c("hard", 7*1e6))) +} + +func (suite *HandlerTestSuite) TestPayoutSwapClaimVVesting() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12), c("busd", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSwapRewardPeriod("busd/ukava", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // deposit into a swap pool + suite.NoError( + suite.DeliverSwapMsgDeposit(valAddr, c("ukava", 1e9), c("busd", 1e9), d("1.0")), + ) + + // accumulate some swap rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapReward(valAddr, "large", nil), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, "large", nil), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := cs(c("hard", 7*1e6), c("swap", 7*1e6)) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards...)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004793, Amount: expectedRewards}, + }) + + // Check that each claim reward coin's amount has been reset to 0 + suite.SwapRewardEquals(valAddr, nil) +} + +func (suite *HandlerTestSuite) TestPayoutSwapClaimVVestingSingleDenom() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12), c("busd", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleSwapRewardPeriod("busd/ukava", cs(c("hard", 1e6), c("swap", 1e6))) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // deposit into a swap pool + suite.NoError( + suite.DeliverSwapMsgDeposit(valAddr, c("ukava", 1e9), c("busd", 1e9), d("1.0")), + ) + + // accumulate some swap rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapReward(valAddr, "large", []string{"swap"}), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c("swap", 7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that claimed coins have been removed from a claim's reward + suite.SwapRewardEquals(valAddr, cs(c("hard", 7*1e6))) +} diff --git a/x/incentive/handler_test.go b/x/incentive/handler_test.go deleted file mode 100644 index bfa21348..00000000 --- a/x/incentive/handler_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package incentive_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/suite" - - sdk "github.com/cosmos/cosmos-sdk/types" - - abci "github.com/tendermint/tendermint/abci/types" - tmtime "github.com/tendermint/tendermint/types/time" - - "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/incentive" - "github.com/kava-labs/kava/x/incentive/types" - "github.com/kava-labs/kava/x/kavadist" -) - -func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } -func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } - -type HandlerTestSuite struct { - suite.Suite - - ctx sdk.Context - app app.TestApp - handler sdk.Handler - keeper incentive.Keeper - addrs []sdk.AccAddress -} - -func (suite *HandlerTestSuite) SetupTest() { - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) - keeper := tApp.GetIncentiveKeeper() - - // Set up genesis state and initialize - _, addrs := app.GeneratePrivKeyAddressPairs(3) - coins := []sdk.Coins{} - for j := 0; j < 3; j++ { - coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000))) - } - authGS := app.NewAuthGenState(addrs, coins) - incentiveGS := incentive.NewGenesisState( - incentive.NewParams( - incentive.RewardPeriods{incentive.NewRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), c("ukava", 122354))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.Multipliers{incentive.NewMultiplier(incentive.MultiplierName("small"), 1, d("0.25")), incentive.NewMultiplier(incentive.MultiplierName("large"), 12, d("1.0"))}, - time.Date(2025, 12, 15, 14, 0, 0, 0, time.UTC), - ), - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultUSDXClaims, - incentive.DefaultHardClaims, - incentive.DefaultDelegatorClaims, - incentive.DefaultSwapClaims, - ) - tApp.InitializeFromGenesisStates(authGS, app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(incentiveGS)}, NewCDPGenStateMulti(), NewPricefeedGenStateMulti()) - - suite.addrs = addrs - suite.handler = incentive.NewHandler(keeper) - suite.keeper = keeper - suite.app = tApp - suite.ctx = ctx -} - -func (suite *HandlerTestSuite) TestMsgUSDXMintingClaimReward() { - suite.addUSDXMintingClaim() - msg := incentive.NewMsgClaimUSDXMintingReward(suite.addrs[0], "small") - res, err := suite.handler(suite.ctx, msg) - suite.NoError(err) - suite.Require().NotNil(res) -} - -func (suite *HandlerTestSuite) TestMsgHardClaimReward() { - suite.addHardLiquidityProviderClaim() - msg := incentive.NewMsgClaimHardReward(suite.addrs[0], "small") - res, err := suite.handler(suite.ctx, msg) - suite.NoError(err) - suite.Require().NotNil(res) -} - -func (suite *HandlerTestSuite) addHardLiquidityProviderClaim() { - sk := suite.app.GetSupplyKeeper() - err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000))) - suite.Require().NoError(err) - rewardPeriod := types.RewardIndexes{types.NewRewardIndex("bnb-s", sdk.ZeroDec())} - multiRewardIndex := types.NewMultiRewardIndex("bnb-s", rewardPeriod) - multiRewardIndexes := types.MultiRewardIndexes{multiRewardIndex} - c1 := incentive.NewHardLiquidityProviderClaim(suite.addrs[0], cs(c("ukava", 1000000)), multiRewardIndexes, multiRewardIndexes) - suite.NotPanics(func() { - suite.keeper.SetHardLiquidityProviderClaim(suite.ctx, c1) - }) -} - -func (suite *HandlerTestSuite) addUSDXMintingClaim() { - sk := suite.app.GetSupplyKeeper() - err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000))) - suite.Require().NoError(err) - c1 := incentive.NewUSDXMintingClaim(suite.addrs[0], c("ukava", 1000000), types.RewardIndexes{types.NewRewardIndex("bnb-s", sdk.ZeroDec())}) - suite.NotPanics(func() { - suite.keeper.SetUSDXMintingClaim(suite.ctx, c1) - }) -} - -// TODO: add tests - -func TestHandlerTestSuite(t *testing.T) { - suite.Run(t, new(HandlerTestSuite)) -} - -// Avoid cluttering test cases with long function names -func i(in int64) sdk.Int { return sdk.NewInt(in) } -func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } diff --git a/x/incentive/handler_usdx_test.go b/x/incentive/handler_usdx_test.go new file mode 100644 index 00000000..9edc8b75 --- /dev/null +++ b/x/incentive/handler_usdx_test.go @@ -0,0 +1,96 @@ +package incentive_test + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + + "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *HandlerTestSuite) TestPayoutUSDXClaim() { + userAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + authBulder := suite.authBuilder(). + WithSimpleAccount(userAddr, cs(c("bnb", 1e12))). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // mint some usdx + suite.DeliverMsgCreateCDP(userAddr, c("bnb", 1e9), c("usdx", 1e7), "bnb-a") + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(userAddr) + + // Check rewards cannot be claimed by vvesting claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimUSDXMintingRewardVVesting(userAddr, receiverAddr, "large"), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim a single denom + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimUSDXMintingReward(userAddr, "large"), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := cs(c(types.USDXMintingRewardDenom, 7*1e6)) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + + suite.VestingPeriodsEqual(userAddr, vesting.Periods{ + {Length: 33004793, Amount: expectedRewards}, + }) + // Check that claimed coins have been removed from a claim's reward + suite.USDXRewardEquals(userAddr, c(types.USDXMintingRewardDenom, 0)) +} + +func (suite *HandlerTestSuite) TestPayoutUSDXClaimVVesting() { + valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] + + vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("bnb", 1e12))) + + authBulder := suite.authBuilder(). + WithAccounts(vva). + WithSimpleAccount(receiverAddr, nil) + + incentBuilder := suite.incentiveBuilder(). + WithSimpleUSDXRewardPeriod("bnb-a", c(types.USDXMintingRewardDenom, 1e6)) + + suite.SetupWithGenState(authBulder, incentBuilder) + + // mint some usdx + suite.DeliverMsgCreateCDP(valAddr, c("bnb", 1e9), c("usdx", 1e7), "bnb-a") + // accumulate some rewards + suite.NextBlockAfter(7 * time.Second) + + preClaimBal := suite.GetBalance(receiverAddr) + + // Check rewards cannot be claimed by normal claim msgs + err := suite.DeliverIncentiveMsg( + types.NewMsgClaimUSDXMintingReward(valAddr, "large"), + ) + suite.ErrorIs(err, types.ErrInvalidAccountType) + + // Claim rewards + err = suite.DeliverIncentiveMsg( + types.NewMsgClaimUSDXMintingRewardVVesting(valAddr, receiverAddr, "large"), + ) + suite.NoError(err) + + // Check rewards were paid out + expectedRewards := c(types.USDXMintingRewardDenom, 7*1e6) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + + suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ + {Length: 33004793, Amount: cs(expectedRewards)}, + }) + + // Check that each claim reward coin's amount has been reset to 0 + suite.USDXRewardEquals(valAddr, c(types.USDXMintingRewardDenom, 0)) +} diff --git a/x/incentive/integration_test.go b/x/incentive/integration_test.go index 4bdb9302..6de252d1 100644 --- a/x/incentive/integration_test.go +++ b/x/incentive/integration_test.go @@ -4,12 +4,21 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/pricefeed" + "github.com/kava-labs/kava/x/swap" ) +// Avoid cluttering test cases with long function names +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } + func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ @@ -99,120 +108,92 @@ func NewCDPGenStateMulti() app.GenesisState { return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } -func NewPricefeedGenStateMulti() app.GenesisState { +func NewPricefeedGenStateMultiFromTime(t time.Time) app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ + {MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, {MarketID: "busd:usd", BaseAsset: "busd", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + {MarketID: "zzz:usd", BaseAsset: "zzz", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ + { + MarketID: "kava:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: t.Add(1 * time.Hour), + }, { MarketID: "btc:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("8000.00"), - Expiry: time.Now().Add(1 * time.Hour), + Expiry: t.Add(1 * time.Hour), }, { MarketID: "xrp:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("0.25"), - Expiry: time.Now().Add(1 * time.Hour), + Expiry: t.Add(1 * time.Hour), }, { MarketID: "bnb:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("17.25"), - Expiry: time.Now().Add(1 * time.Hour), + Expiry: t.Add(1 * time.Hour), }, { MarketID: "busd:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.OneDec(), - Expiry: time.Now().Add(1 * time.Hour), + Expiry: t.Add(1 * time.Hour), + }, + { + MarketID: "zzz:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("2.00"), + Expiry: t.Add(1 * time.Hour), }, }, } return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} } -// func NewIncentiveGenState(previousAccumTime, endTime time.Time, rewardPeriods ...incentive.RewardPeriod) app.GenesisState { -// var accumulationTimes incentive.GenesisAccumulationTimes -// for _, rp := range rewardPeriods { -// accumulationTimes = append( -// accumulationTimes, -// incentive.NewGenesisAccumulationTime( -// rp.CollateralType, -// previousAccumTime, -// ), -// ) -// } -// genesis := incentive.NewGenesisState( -// incentive.NewParams( -// rewardPeriods, -// types.MultiRewardPeriods{}, -// types.MultiRewardPeriods{}, -// types.MultiRewardPeriods{}, -// types.MultiRewardPeriods{}, -// incentive.Multipliers{ -// incentive.NewMultiplier(incentive.Small, 1, d("0.25")), -// incentive.NewMultiplier(incentive.Large, 12, d("1.0")), -// }, -// endTime, -// ), -// accumulationTimes, -// accumulationTimes, -// accumulationTimes, -// incentive.DefaultGenesisAccumulationTimes, -// incentive.DefaultUSDXClaims, -// incentive.DefaultHardClaims, -// ) -// return app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(genesis)} -// } +func NewHardGenStateMulti(genTime time.Time) testutil.HardGenesisBuilder { + kavaMM := testutil.NewStandardMoneyMarket("ukava") + kavaMM.SpotMarketID = "kava:usd" + btcMM := testutil.NewStandardMoneyMarket("btcb") + btcMM.SpotMarketID = "btc:usd" -func NewCDPGenStateHighInterest() app.GenesisState { - cdpGenesis := cdp.GenesisState{ - Params: cdp.Params{ - GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - SurplusAuctionLot: cdp.DefaultSurplusLot, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, - DebtAuctionLot: cdp.DefaultDebtLot, - CollateralParams: cdp.CollateralParams{ - { - Denom: "bnb", - Type: "bnb-a", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), - StabilityFee: sdk.MustNewDecFromStr("1.000000051034942716"), // 500% APR - LiquidationPenalty: d("0.05"), - AuctionSize: i(50000000000), - Prefix: 0x22, - SpotMarketID: "bnb:usd", - LiquidationMarketID: "bnb:usd", - ConversionFactor: i(8), - }, - }, - DebtParam: cdp.DebtParam{ - Denom: "usdx", - ReferenceAsset: "usd", - ConversionFactor: i(6), - DebtFloor: i(10000000), - }, - }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{ - cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec()), - }, - TotalPrincipals: cdp.GenesisTotalPrincipals{ - cdp.NewGenesisTotalPrincipal("bnb-a", sdk.ZeroInt()), - }, - } - return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} + builder := testutil.NewHardGenesisBuilder().WithGenesisTime(genTime). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("usdx")). + WithInitializedMoneyMarket(kavaMM). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("bnb")). + WithInitializedMoneyMarket(btcMM). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("xrp")). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("zzz")) + return builder +} + +func NewStakingGenesisState() app.GenesisState { + genState := staking.DefaultGenesisState() + genState.Params.BondDenom = "ukava" + return app.GenesisState{ + staking.ModuleName: staking.ModuleCdc.MustMarshalJSON(genState), + } +} + +func NewSwapGenesisState() app.GenesisState { + genesis := swap.NewGenesisState( + swap.NewParams( + swap.NewAllowedPools(swap.NewAllowedPool("busd", "ukava")), + d("0.0"), + ), + ) + return app.GenesisState{ + swap.ModuleName: swap.ModuleCdc.MustMarshalJSON(genesis), + } } diff --git a/x/incentive/keeper/cdp_test.go b/x/incentive/keeper/cdp_test.go index dc1d8203..ff7453c9 100644 --- a/x/incentive/keeper/cdp_test.go +++ b/x/incentive/keeper/cdp_test.go @@ -8,6 +8,7 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" ) @@ -23,7 +24,7 @@ func TestRiskyCDPsAccumulateRewards(t *testing.T) { collateralType := "bnb-a" rewardsPerSecond := c(types.USDXMintingRewardDenom, 1_000_000) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(genesisTime). WithSimpleUSDXRewardPeriod(collateralType, rewardsPerSecond) diff --git a/x/incentive/keeper/claim.go b/x/incentive/keeper/claim.go new file mode 100644 index 00000000..85a9612c --- /dev/null +++ b/x/incentive/keeper/claim.go @@ -0,0 +1,239 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/kava-labs/kava/x/incentive/types" + validatorvesting "github.com/kava-labs/kava/x/validator-vesting" +) + +// ClaimUSDXMintingReward pays out funds from a claim to a receiver account. +// Rewards are removed from a claim and paid out according to the multiplier, which reduces the reward amount in exchange for shorter vesting times. +func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { + claim, found := k.GetUSDXMintingClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) + if err != nil { + return err + } + + rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt() + if rewardAmount.IsZero() { + return types.ErrZeroClaim + } + rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, sdk.NewCoins(rewardCoin), length) + if err != nil { + return err + } + + k.ZeroUSDXMintingClaim(ctx, claim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, owner.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()), + ), + ) + return nil +} + +// ClaimHardReward pays out funds from a claim to a receiver account. +// Rewards are removed from a claim and paid out according to the multiplier, which reduces the reward amount in exchange for shorter vesting times. +func (k Keeper) ClaimHardReward(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName, denomsToClaim []string) error { + _, found := k.GetHardLiquidityProviderClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + k.SynchronizeHardLiquidityProviderClaim(ctx, owner) + + syncedClaim, found := k.GetHardLiquidityProviderClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) + rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + if rewardCoins.IsZero() { + return types.ErrZeroClaim + } + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) + if err != nil { + return err + } + + // remove claimed coins (NOT reward coins) + syncedClaim.Reward = syncedClaim.Reward.Sub(claimingCoins) + k.SetHardLiquidityProviderClaim(ctx, syncedClaim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, owner.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claimingCoins.String()), + sdk.NewAttribute(types.AttributeKeyClaimType, syncedClaim.GetType()), + ), + ) + return nil +} + +// ClaimDelegatorReward pays out funds from a claim to a receiver account. +// Rewards are removed from a claim and paid out according to the multiplier, which reduces the reward amount in exchange for shorter vesting times. +func (k Keeper) ClaimDelegatorReward(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName, denomsToClaim []string) error { + claim, found := k.GetDelegatorClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + syncedClaim, err := k.SynchronizeDelegatorClaim(ctx, claim) + if err != nil { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) + rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + if rewardCoins.IsZero() { + return types.ErrZeroClaim + } + + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) + if err != nil { + return err + } + + // remove claimed coins (NOT reward coins) + syncedClaim.Reward = syncedClaim.Reward.Sub(claimingCoins) + k.SetDelegatorClaim(ctx, syncedClaim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, owner.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claimingCoins.String()), + sdk.NewAttribute(types.AttributeKeyClaimType, syncedClaim.GetType()), + ), + ) + return nil +} + +// ClaimSwapReward pays out funds from a claim to a receiver account. +// Rewards are removed from a claim and paid out according to the multiplier, which reduces the reward amount in exchange for shorter vesting times. +func (k Keeper) ClaimSwapReward(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName, denomsToClaim []string) error { + _, found := k.GetSwapClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + syncedClaim, found := k.GetSynchronizedSwapClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) + rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + if rewardCoins.IsZero() { + return types.ErrZeroClaim + } + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) + if err != nil { + return err + } + + // remove claimed coins (NOT reward coins) + syncedClaim.Reward = syncedClaim.Reward.Sub(claimingCoins) + k.SetSwapClaim(ctx, syncedClaim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, owner.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claimingCoins.String()), + sdk.NewAttribute(types.AttributeKeyClaimType, syncedClaim.GetType()), + ), + ) + return nil +} + +func (k Keeper) ValidateIsValidatorVestingAccount(ctx sdk.Context, address sdk.AccAddress) error { + acc := k.accountKeeper.GetAccount(ctx, address) + if acc == nil { + return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", address) + } + _, ok := acc.(*validatorvesting.ValidatorVestingAccount) + if !ok { + return sdkerrors.Wrapf(types.ErrInvalidAccountType, "account is not validator vesting account, %s", address) + } + return nil +} + diff --git a/x/incentive/keeper/claim_test.go b/x/incentive/keeper/claim_test.go new file mode 100644 index 00000000..6aafba18 --- /dev/null +++ b/x/incentive/keeper/claim_test.go @@ -0,0 +1,81 @@ +package keeper_test + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/x/incentive/types" +) + +// ClaimTests runs unit tests for the keeper Claim methods +type ClaimTests struct { + unitTester +} + +func TestClaim(t *testing.T) { + suite.Run(t, new(ClaimTests)) +} + +func (suite *ClaimTests) ErrorIs(err, target error) bool { + return suite.Truef(errors.Is(err, target), "err didn't match: %s, it was: %s", target, err) +} + +func (suite *ClaimTests) TestCannotClaimWhenMultiplierNotRecognised() { + subspace := &fakeParamSubspace{ + params: types.Params{ + ClaimMultipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, d("0.2")), + }, + }, + } + suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil, nil) + + claim := types.DelegatorClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + }, + } + suite.storeDelegatorClaim(claim) + + // multiplier not in params + err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, types.Large, nil) + suite.ErrorIs(err, types.ErrInvalidMultiplier) + + // invalid multiplier name + err = suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, "", nil) + suite.ErrorIs(err, types.ErrInvalidMultiplier) + + // invalid multiplier name + const zeroWidthSpace = "​" + err = suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, types.Small+zeroWidthSpace, nil) + suite.ErrorIs(err, types.ErrInvalidMultiplier) +} + +func (suite *ClaimTests) TestCannotClaimAfterEndTime() { + endTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + + subspace := &fakeParamSubspace{ + params: types.Params{ + ClaimMultipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, d("0.2")), + }, + ClaimEnd: endTime, + }, + } + suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil, nil) + + suite.ctx = suite.ctx.WithBlockTime(endTime.Add(time.Nanosecond)) + + claim := types.DelegatorClaim{ + BaseMultiClaim: types.BaseMultiClaim{ + Owner: arbitraryAddress(), + }, + } + suite.storeDelegatorClaim(claim) + + err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, types.Small, nil) + suite.ErrorIs(err, types.ErrClaimExpired) +} diff --git a/x/incentive/keeper/integration_test.go b/x/incentive/keeper/integration_test.go index 2c2c5734..18912b42 100644 --- a/x/incentive/keeper/integration_test.go +++ b/x/incentive/keeper/integration_test.go @@ -9,16 +9,10 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" committeetypes "github.com/kava-labs/kava/x/committee/types" - "github.com/kava-labs/kava/x/hard" - hardtypes "github.com/kava-labs/kava/x/hard/types" - "github.com/kava-labs/kava/x/incentive/types" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/pricefeed" ) -const ( - oneYear time.Duration = time.Hour * 24 * 365 -) - // Avoid cluttering test cases with long function names func i(in int64) sdk.Int { return sdk.NewInt(in) } func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } @@ -168,19 +162,19 @@ func NewPricefeedGenStateMultiFromTime(t time.Time) app.GenesisState { return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} } -func NewHardGenStateMulti(genTime time.Time) HardGenesisBuilder { - kavaMM := NewStandardMoneyMarket("ukava") +func NewHardGenStateMulti(genTime time.Time) testutil.HardGenesisBuilder { + kavaMM := testutil.NewStandardMoneyMarket("ukava") kavaMM.SpotMarketID = "kava:usd" - btcMM := NewStandardMoneyMarket("btcb") + btcMM := testutil.NewStandardMoneyMarket("btcb") btcMM.SpotMarketID = "btc:usd" - builder := NewHardGenesisBuilder().WithGenesisTime(genTime). - WithInitializedMoneyMarket(NewStandardMoneyMarket("usdx")). + builder := testutil.NewHardGenesisBuilder().WithGenesisTime(genTime). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("usdx")). WithInitializedMoneyMarket(kavaMM). - WithInitializedMoneyMarket(NewStandardMoneyMarket("bnb")). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("bnb")). WithInitializedMoneyMarket(btcMM). - WithInitializedMoneyMarket(NewStandardMoneyMarket("xrp")). - WithInitializedMoneyMarket(NewStandardMoneyMarket("zzz")) + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("xrp")). + WithInitializedMoneyMarket(testutil.NewStandardMoneyMarket("zzz")) return builder } @@ -212,162 +206,3 @@ func NewCommitteeGenesisState(members []sdk.AccAddress) app.GenesisState { committeetypes.ModuleName: committeetypes.ModuleCdc.MustMarshalJSON(genState), } } - -// IncentiveGenesisBuilder is a tool for creating an incentive genesis state. -// Helper methods add values onto a default genesis state. -// All methods are immutable and return updated copies of the builder. -type IncentiveGenesisBuilder struct { - types.GenesisState - genesisTime time.Time -} - -func NewIncentiveGenesisBuilder() IncentiveGenesisBuilder { - return IncentiveGenesisBuilder{ - GenesisState: types.DefaultGenesisState(), - genesisTime: time.Time{}, - } -} - -func (builder IncentiveGenesisBuilder) Build() types.GenesisState { - return builder.GenesisState -} - -func (builder IncentiveGenesisBuilder) BuildMarshalled() app.GenesisState { - return app.GenesisState{ - types.ModuleName: types.ModuleCdc.MustMarshalJSON(builder.Build()), - } -} - -func (builder IncentiveGenesisBuilder) WithGenesisTime(time time.Time) IncentiveGenesisBuilder { - builder.genesisTime = time - builder.Params.ClaimEnd = time.Add(5 * oneYear) - return builder -} - -func (builder IncentiveGenesisBuilder) WithInitializedBorrowRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { - builder.Params.HardBorrowRewardPeriods = append(builder.Params.HardBorrowRewardPeriods, period) - - accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) - builder.HardBorrowAccumulationTimes = append(builder.HardBorrowAccumulationTimes, accumulationTimeForPeriod) - return builder -} - -func (builder IncentiveGenesisBuilder) WithSimpleBorrowRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { - return builder.WithInitializedBorrowRewardPeriod(types.NewMultiRewardPeriod( - true, - ctype, - builder.genesisTime, - builder.genesisTime.Add(4*oneYear), - rewardsPerSecond, - )) -} -func (builder IncentiveGenesisBuilder) WithInitializedSupplyRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { - builder.Params.HardSupplyRewardPeriods = append(builder.Params.HardSupplyRewardPeriods, period) - - accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) - builder.HardSupplyAccumulationTimes = append(builder.HardSupplyAccumulationTimes, accumulationTimeForPeriod) - return builder -} - -func (builder IncentiveGenesisBuilder) WithSimpleSupplyRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { - return builder.WithInitializedSupplyRewardPeriod(types.NewMultiRewardPeriod( - true, - ctype, - builder.genesisTime, - builder.genesisTime.Add(4*oneYear), - rewardsPerSecond, - )) -} -func (builder IncentiveGenesisBuilder) WithInitializedDelegatorRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { - builder.Params.DelegatorRewardPeriods = append(builder.Params.DelegatorRewardPeriods, period) - - accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) - builder.DelegatorAccumulationTimes = append(builder.DelegatorAccumulationTimes, accumulationTimeForPeriod) - return builder -} - -func (builder IncentiveGenesisBuilder) WithSimpleDelegatorRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { - return builder.WithInitializedDelegatorRewardPeriod(types.NewMultiRewardPeriod( - true, - ctype, - builder.genesisTime, - builder.genesisTime.Add(4*oneYear), - rewardsPerSecond, - )) -} -func (builder IncentiveGenesisBuilder) WithInitializedUSDXRewardPeriod(period types.RewardPeriod) IncentiveGenesisBuilder { - builder.Params.USDXMintingRewardPeriods = append(builder.Params.USDXMintingRewardPeriods, period) - - accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) - builder.USDXAccumulationTimes = append(builder.USDXAccumulationTimes, accumulationTimeForPeriod) - return builder -} - -func (builder IncentiveGenesisBuilder) WithSimpleUSDXRewardPeriod(ctype string, rewardsPerSecond sdk.Coin) IncentiveGenesisBuilder { - return builder.WithInitializedUSDXRewardPeriod(types.NewRewardPeriod( - true, - ctype, - builder.genesisTime, - builder.genesisTime.Add(4*oneYear), - rewardsPerSecond, - )) -} - -func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.Multipliers) IncentiveGenesisBuilder { - builder.Params.ClaimMultipliers = multipliers - return builder -} - -// HardGenesisBuilder is a tool for creating a hard genesis state. -// Helper methods add values onto a default genesis state. -// All methods are immutable and return updated copies of the builder. -type HardGenesisBuilder struct { - hardtypes.GenesisState - genesisTime time.Time -} - -func NewHardGenesisBuilder() HardGenesisBuilder { - return HardGenesisBuilder{ - GenesisState: hardtypes.DefaultGenesisState(), - } -} -func (builder HardGenesisBuilder) Build() hardtypes.GenesisState { - return builder.GenesisState -} -func (builder HardGenesisBuilder) BuildMarshalled() app.GenesisState { - return app.GenesisState{ - hardtypes.ModuleName: hardtypes.ModuleCdc.MustMarshalJSON(builder.Build()), - } -} -func (builder HardGenesisBuilder) WithGenesisTime(genTime time.Time) HardGenesisBuilder { - builder.genesisTime = genTime - return builder -} -func (builder HardGenesisBuilder) WithInitializedMoneyMarket(market hard.MoneyMarket) HardGenesisBuilder { - builder.Params.MoneyMarkets = append(builder.Params.MoneyMarkets, market) - - builder.PreviousAccumulationTimes = append( - builder.PreviousAccumulationTimes, - hardtypes.NewGenesisAccumulationTime(market.Denom, builder.genesisTime, sdk.OneDec(), sdk.OneDec()), - ) - return builder -} -func (builder HardGenesisBuilder) WithMinBorrow(minUSDValue sdk.Dec) HardGenesisBuilder { - builder.Params.MinimumBorrowUSDValue = minUSDValue - return builder -} -func NewStandardMoneyMarket(denom string) hardtypes.MoneyMarket { - return hardtypes.NewMoneyMarket( - denom, - hard.NewBorrowLimit( - false, - sdk.NewDec(1e15), - d("0.6"), - ), - denom+":usd", - i(1e6), - hard.NewInterestRateModel(d("0.05"), d("2"), d("0.8"), d("10")), - d("0.05"), - sdk.ZeroDec(), - ) -} diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index f0195f07..496d9ff4 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -23,367 +23,6 @@ const ( PaymentHour = 14 ) -// ClaimUSDXMintingReward sends the reward amount to the input address and zero's out the claim in the store -func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { - claim, found := k.GetUSDXMintingClaim(ctx, addr) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) - if err != nil { - return err - } - - rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - return types.ErrZeroClaim - } - rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), length) - if err != nil { - return err - } - - k.ZeroUSDXMintingClaim(ctx, claim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()), - ), - ) - return nil -} - -// ClaimUSDXMintingReward sends the reward amount to the input receiver address and zero's out the claim in the store -func (k Keeper) ClaimUSDXMintingRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { - claim, found := k.GetUSDXMintingClaim(ctx, owner) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) - } - - acc := k.accountKeeper.GetAccount(ctx, owner) - if acc == nil { - return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner) - } - _, ok := acc.(*validatorvesting.ValidatorVestingAccount) - if !ok { - return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) - if err != nil { - return err - } - - rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - return types.ErrZeroClaim - } - rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, sdk.NewCoins(rewardCoin), length) - if err != nil { - return err - } - - k.ZeroUSDXMintingClaim(ctx, claim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()), - ), - ) - return nil -} - -// ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store -func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { - _, found := k.GetHardLiquidityProviderClaim(ctx, addr) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - k.SynchronizeHardLiquidityProviderClaim(ctx, addr) - - claim, found := k.GetHardLiquidityProviderClaim(ctx, addr) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) - } - - var rewardCoins sdk.Coins - for _, coin := range claim.Reward { - rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - continue - } - rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) - } - if rewardCoins.IsZero() { - return types.ErrZeroClaim - } - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, rewardCoins, length) - if err != nil { - return err - } - - k.ZeroHardLiquidityProviderClaim(ctx, claim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimType, claim.GetType()), - ), - ) - return nil -} - -// ClaimHardRewardVVesting sends the reward amount to the input address and zero's out the claim in the store -func (k Keeper) ClaimHardRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { - _, found := k.GetHardLiquidityProviderClaim(ctx, owner) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) - } - - acc := k.accountKeeper.GetAccount(ctx, owner) - if acc == nil { - return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner) - } - _, ok := acc.(*validatorvesting.ValidatorVestingAccount) - if !ok { - return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - k.SynchronizeHardLiquidityProviderClaim(ctx, owner) - - claim, found := k.GetHardLiquidityProviderClaim(ctx, owner) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) - } - - var rewardCoins sdk.Coins - for _, coin := range claim.Reward { - rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - continue - } - rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) - } - if rewardCoins.IsZero() { - return types.ErrZeroClaim - } - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) - if err != nil { - return err - } - - k.ZeroHardLiquidityProviderClaim(ctx, claim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimType, claim.GetType()), - ), - ) - return nil -} - -// ClaimDelegatorReward sends the reward amount to the input address and zero's out the delegator claim in the store -func (k Keeper) ClaimDelegatorReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { - claim, found := k.GetDelegatorClaim(ctx, addr) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - syncedClaim, err := k.SynchronizeDelegatorClaim(ctx, claim) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) - } - - var rewardCoins sdk.Coins - for _, coin := range syncedClaim.Reward { - rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - continue - } - rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) - } - if rewardCoins.IsZero() { - return types.ErrZeroClaim - } - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, rewardCoins, length) - if err != nil { - return err - } - - k.ZeroDelegatorClaim(ctx, syncedClaim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, syncedClaim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, syncedClaim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimType, syncedClaim.GetType()), - ), - ) - return nil -} - -// ClaimDelegatorRewardVVesting sends the reward amount to the input address and zero's out the claim in the store -func (k Keeper) ClaimDelegatorRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { - claim, found := k.GetDelegatorClaim(ctx, owner) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) - } - - acc := k.accountKeeper.GetAccount(ctx, owner) - if acc == nil { - return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner) - } - _, ok := acc.(*validatorvesting.ValidatorVestingAccount) - if !ok { - return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner) - } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) - if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) - } - - claimEnd := k.GetClaimEnd(ctx) - - if ctx.BlockTime().After(claimEnd) { - return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) - } - - syncedClaim, err := k.SynchronizeDelegatorClaim(ctx, claim) - if !found { - return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) - } - - var rewardCoins sdk.Coins - for _, coin := range syncedClaim.Reward { - rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt() - if rewardAmount.IsZero() { - continue - } - rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) - } - if rewardCoins.IsZero() { - return types.ErrZeroClaim - } - length, err := k.GetPeriodLength(ctx, multiplier) - if err != nil { - return err - } - - err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) - if err != nil { - return err - } - - k.ZeroDelegatorClaim(ctx, syncedClaim) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, syncedClaim.GetOwner().String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, syncedClaim.GetReward().String()), - sdk.NewAttribute(types.AttributeKeyClaimType, syncedClaim.GetType()), - ), - ) - return nil -} - // SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account and the input length is greater than zero, the recipient account is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length. func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error { macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule) diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index 4b848627..9ea0c6cd 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -7,7 +7,6 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" "github.com/cosmos/cosmos-sdk/x/auth/vesting" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" @@ -18,12 +17,11 @@ import ( "github.com/kava-labs/kava/app" cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper" cdptypes "github.com/kava-labs/kava/x/cdp/types" - "github.com/kava-labs/kava/x/hard" hardkeeper "github.com/kava-labs/kava/x/hard/keeper" "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/kavadist" - validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) // Test suite used for all keeper tests @@ -61,7 +59,7 @@ func (suite *PayoutTestSuite) SetupApp() { suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) } -func (suite *PayoutTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder IncentiveGenesisBuilder, hardBuilder HardGenesisBuilder) { +func (suite *PayoutTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder, hardBuilder testutil.HardGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( @@ -84,674 +82,6 @@ func (suite *PayoutTestSuite) getModuleAccount(name string) supplyexported.Modul return sk.GetModuleAccount(suite.ctx, name) } -func (suite *PayoutTestSuite) TestPayoutUSDXMintingClaim() { - type args struct { - ctype string - rewardsPerSecond sdk.Coin - initialCollateral sdk.Coin - initialPrincipal sdk.Coin - multipliers types.Multipliers - multiplier types.MultiplierName - timeElapsed int - expectedBalance sdk.Coins - expectedPeriods vesting.Periods - isPeriodicVestingAccount bool - } - type errArgs struct { - expectPass bool - contains string - } - type test struct { - name string - args args - errArgs errArgs - } - testCases := []test{ - { - "valid 1 day", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedBalance: cs(c("usdx", 10000000000), c("ukava", 10571385600)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("ukava", 10571385600))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "invalid zero rewards", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 0), - initialCollateral: c("bnb", 1000000000000), - initialPrincipal: c("usdx", 10000000000), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedBalance: cs(c("usdx", 10000000000)), - expectedPeriods: vesting.Periods{}, - isPeriodicVestingAccount: false, - }, - errArgs{ - expectPass: false, - contains: "claim amount rounds to zero", - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - userAddr := suite.addrs[0] - authBulder := app.NewAuthGenesisBuilder(). - WithSimpleAccount(userAddr, cs(tc.args.initialCollateral)). - WithSimpleModuleAccount(kavadist.ModuleName, cs(c("ukava", 1000000000000))) - - incentBuilder := NewIncentiveGenesisBuilder(). - WithGenesisTime(suite.genesisTime). - WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond). - WithMultipliers(tc.args.multipliers) - - suite.SetupWithGenState(authBulder, incentBuilder, NewHardGenStateMulti(suite.genesisTime)) - - // setup cdp state - err := suite.cdpKeeper.AddCdp(suite.ctx, userAddr, tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) - suite.Require().NoError(err) - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) - - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) - suite.Require().True(found) - err = suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) - suite.Require().NoError(err) - - err = suite.keeper.ClaimUSDXMintingReward(suite.ctx, userAddr, tc.args.multiplier) - - if tc.errArgs.expectPass { - suite.Require().NoError(err) - - acc := suite.getAccount(userAddr) - suite.Require().Equal(tc.args.expectedBalance, acc.GetCoins()) - - if tc.args.isPeriodicVestingAccount { - vacc, ok := acc.(*vesting.PeriodicVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) - } - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, userAddr) - suite.Require().True(found) - suite.Require().Equal(c("ukava", 0), claim.Reward) - } else { - suite.Require().Error(err) - suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) - } - }) - } -} - -func (suite *PayoutTestSuite) TestPayoutUSDXMintingClaimVVesting() { - type args struct { - ctype string - rewardsPerSecond sdk.Coin - initialCollateral sdk.Coin - initialPrincipal sdk.Coin - multipliers types.Multipliers - multiplier types.MultiplierName - timeElapsed int - expectedBalance sdk.Coins - expectedPeriods vesting.Periods - } - type errArgs struct { - expectPass bool - contains string - } - type test struct { - name string - args args - errArgs errArgs - } - testCases := []test{ - { - "valid 1 day", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 122354), - initialCollateral: c("bnb", 1e12), - initialPrincipal: c("usdx", 1e10), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedBalance: cs(c("ukava", 10571385600)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("ukava", 10571385600))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "invalid zero rewards", - args{ - ctype: "bnb-a", - rewardsPerSecond: c("ukava", 0), - initialCollateral: c("bnb", 1e12), - initialPrincipal: c("usdx", 1e10), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedBalance: cs(), - expectedPeriods: vesting.Periods{}, - }, - errArgs{ - expectPass: false, - contains: "claim amount rounds to zero", - }, - }, - } - for _, tc := range testCases { - suite.Run(tc.name, func() { - - bacc := auth.NewBaseAccount(suite.addrs[2], cs(tc.args.initialCollateral, c("ukava", 400)), nil, 0, 0) - bva, err := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), suite.genesisTime.Unix()+16) - suite.Require().NoError(err) - periods := vesting.Periods{ - vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))}, - } - vva := validatorvesting.NewValidatorVestingAccountRaw(bva, suite.genesisTime.Unix(), periods, sdk.ConsAddress{}, nil, 90) - - authBulder := app.NewAuthGenesisBuilder(). - WithAccounts(vva). - WithSimpleModuleAccount(kavadist.ModuleName, cs(c("ukava", 1e18))). - WithSimpleAccount(suite.addrs[0], cs()) // the recipient address needs to be a instantiated account // TODO change? - - incentBuilder := NewIncentiveGenesisBuilder(). - WithGenesisTime(suite.genesisTime). - WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond). - WithMultipliers(tc.args.multipliers) - - suite.SetupWithGenState(authBulder, incentBuilder, NewHardGenStateMulti(suite.genesisTime)) - - // setup cdp state - cdpKeeper := suite.app.GetCDPKeeper() - err = cdpKeeper.AddCdp(suite.ctx, suite.addrs[2], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) - suite.Require().NoError(err) - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[2]) - suite.Require().True(found) - suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) - - // accumulate some usdx rewards - updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) - suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) - rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) - suite.Require().True(found) - err = suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) - suite.Require().NoError(err) - - err = suite.keeper.ClaimUSDXMintingRewardVVesting(suite.ctx, suite.addrs[2], suite.addrs[0], tc.args.multiplier) - - if tc.errArgs.expectPass { - suite.Require().NoError(err) - ak := suite.app.GetAccountKeeper() - acc := ak.GetAccount(suite.ctx, suite.addrs[0]) - suite.Require().Equal(tc.args.expectedBalance, acc.GetCoins()) - - vacc, ok := acc.(*vesting.PeriodicVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) - - claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[2]) - suite.Require().True(found) - suite.Require().Equal(c("ukava", 0), claim.Reward) - } else { - suite.Require().Error(err) - suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) - } - }) - } -} - -func (suite *PayoutTestSuite) TestPayoutHardLiquidityProviderClaim() { - type args struct { - deposit sdk.Coins - borrow sdk.Coins - rewardsPerSecond sdk.Coins - multipliers types.Multipliers - multiplier types.MultiplierName - timeElapsed int64 - expectedRewards sdk.Coins - expectedPeriods vesting.Periods - isPeriodicVestingAccount bool - } - type errArgs struct { - expectPass bool - contains string - } - type test struct { - name string - args args - errArgs errArgs - } - testCases := []test{ - { - "single reward denom: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771200)), // 10571385600 (deposit reward) + 10571385600 (borrow reward) - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "single reward denom: valid 10 days", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 864000, - expectedRewards: cs(c("hard", 211427712000)), // 105713856000 (deposit reward) + 105713856000 (borrow reward) - expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712000))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771200), c("ukava", 21142771200)), // 10571385600 (deposit reward) + 10571385600 (borrow reward) - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200), c("ukava", 21142771200))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms: valid 10 days", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 864000, - expectedRewards: cs(c("hard", 211427712000), c("ukava", 211427712000)), // 105713856000 (deposit reward) + 105713856000 (borrow reward) - expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712000), c("ukava", 211427712000))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms with different rewards per second: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 222222)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771200), c("ukava", 38399961600)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200), c("ukava", 38399961600))}}, - isPeriodicVestingAccount: true, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - userAddr := suite.addrs[3] - authBulder := app.NewAuthGenesisBuilder(). - WithSimpleAccount(userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15))). - WithSimpleModuleAccount(kavadist.ModuleName, cs(c("hard", 1000000000000000000), c("ukava", 1000000000000000000))) - - incentBuilder := NewIncentiveGenesisBuilder(). - WithGenesisTime(suite.genesisTime). - WithMultipliers(tc.args.multipliers) - for _, c := range tc.args.deposit { - incentBuilder = incentBuilder.WithSimpleSupplyRewardPeriod(c.Denom, tc.args.rewardsPerSecond) - } - for _, c := range tc.args.borrow { - incentBuilder = incentBuilder.WithSimpleBorrowRewardPeriod(c.Denom, tc.args.rewardsPerSecond) - } - - suite.SetupWithGenState(authBulder, incentBuilder, NewHardGenStateMulti(suite.genesisTime)) - - // User deposits and borrows - err := suite.hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) - suite.Require().NoError(err) - err = suite.hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim that has 0 rewards - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - for _, coin := range tc.args.deposit { - suite.Require().Equal(sdk.ZeroInt(), claim.Reward.AmountOf(coin.Denom)) - } - - // Set up future runtime context - runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.timeElapsed), 0) - runCtx := suite.ctx.WithBlockTime(runAtTime) - - // Accumulate supply rewards for each deposit denom - for _, coin := range tc.args.deposit { - rewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, coin.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardSupplyRewards(runCtx, rewardPeriod) - suite.Require().NoError(err) - } - - // Accumulate borrow rewards for each deposit denom - for _, coin := range tc.args.borrow { - rewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, coin.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardBorrowRewards(runCtx, rewardPeriod) - suite.Require().NoError(err) - } - - // Sync hard supply rewards - deposit, found := suite.hardKeeper.GetDeposit(suite.ctx, userAddr) - suite.Require().True(found) - suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) - - // Sync hard borrow rewards - borrow, found := suite.hardKeeper.GetBorrow(suite.ctx, userAddr) - suite.Require().True(found) - suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) - - // Fetch pre-claim balances - ak := suite.app.GetAccountKeeper() - preClaimAcc := ak.GetAccount(runCtx, userAddr) - - err = suite.keeper.ClaimHardReward(runCtx, userAddr, tc.args.multiplier) - if tc.errArgs.expectPass { - suite.Require().NoError(err) - - // Check that user's balance has increased by expected reward amount - postClaimAcc := ak.GetAccount(suite.ctx, userAddr) - suite.Require().Equal(preClaimAcc.GetCoins().Add(tc.args.expectedRewards...), postClaimAcc.GetCoins()) - - if tc.args.isPeriodicVestingAccount { - vacc, ok := postClaimAcc.(*vesting.PeriodicVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) - } - - // Check that each claim reward coin's amount has been reset to 0 - claim, found := suite.keeper.GetHardLiquidityProviderClaim(runCtx, userAddr) - suite.Require().True(found) - for _, claimRewardCoin := range claim.Reward { - suite.Require().Equal(c(claimRewardCoin.Denom, 0), claimRewardCoin) - } - } else { - suite.Require().Error(err) - suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) - } - }) - } -} - -func (suite *PayoutTestSuite) TestPayoutHardLiquidityProviderClaimVVesting() { - type args struct { - deposit sdk.Coins - borrow sdk.Coins - rewardsPerSecond sdk.Coins - multipliers types.Multipliers - multiplier types.MultiplierName - timeElapsed int64 - expectedRewards sdk.Coins - expectedPeriods vesting.Periods - } - type errArgs struct { - expectPass bool - contains string - } - type test struct { - name string - args args - errArgs errArgs - } - testCases := []test{ - { - "single reward denom: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771202)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771202))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "single reward denom: valid 10 days", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 864000, - expectedRewards: cs(c("hard", 211427712008)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712008))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771202), c("ukava", 21142771202)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771202), c("ukava", 21142771202))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms: valid 10 days", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 864000, - expectedRewards: cs(c("hard", 211427712008), c("ukava", 211427712008)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712008), c("ukava", 211427712008))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - { - "multiple reward denoms with different rewards per second: valid 1 day", - args{ - deposit: cs(c("bnb", 10000000000)), - borrow: cs(c("bnb", 5000000000)), - rewardsPerSecond: cs(c("hard", 122354), c("ukava", 222222)), - multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, - multiplier: types.MultiplierName("large"), - timeElapsed: 86400, - expectedRewards: cs(c("hard", 21142771202), c("ukava", 38399961603)), - expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771202), c("ukava", 38399961603))}}, - }, - errArgs{ - expectPass: true, - contains: "", - }, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - - userAddr := suite.addrs[3] - - bacc := auth.NewBaseAccount(userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), nil, 0, 0) - bva, err := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), suite.genesisTime.Unix()+16) - suite.Require().NoError(err) - periods := vesting.Periods{ - vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))}, - vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))}, - } - vva := validatorvesting.NewValidatorVestingAccountRaw(bva, suite.genesisTime.Unix(), periods, sdk.ConsAddress{}, nil, 90) - - authBulder := app.NewAuthGenesisBuilder(). - WithAccounts(vva). - WithSimpleAccount(suite.addrs[2], cs()). - WithSimpleModuleAccount(kavadist.ModuleName, cs(c("hard", 1000000000000000000), c("ukava", 1000000000000000000))) - - incentBuilder := NewIncentiveGenesisBuilder(). - WithGenesisTime(suite.genesisTime). - WithMultipliers(tc.args.multipliers) - for _, c := range tc.args.deposit { - incentBuilder = incentBuilder.WithSimpleSupplyRewardPeriod(c.Denom, tc.args.rewardsPerSecond) - } - for _, c := range tc.args.borrow { - incentBuilder = incentBuilder.WithSimpleBorrowRewardPeriod(c.Denom, tc.args.rewardsPerSecond) - } - - suite.SetupWithGenState(authBulder, incentBuilder, NewHardGenStateMulti(suite.genesisTime)) - - ak := suite.app.GetAccountKeeper() - hardKeeper := suite.app.GetHardKeeper() - - // User deposits and borrows - err = hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) - suite.Require().NoError(err) - err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) - suite.Require().NoError(err) - - // Check that Hard hooks initialized a HardLiquidityProviderClaim that has 0 rewards - claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, userAddr) - suite.Require().True(found) - for _, coin := range tc.args.deposit { - suite.Require().Equal(sdk.ZeroInt(), claim.Reward.AmountOf(coin.Denom)) - } - - // Set up future runtime context - runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.timeElapsed), 0) - runCtx := suite.ctx.WithBlockTime(runAtTime) - - // Run Hard begin blocker - hard.BeginBlocker(runCtx, suite.hardKeeper) - - // Accumulate supply rewards for each deposit denom - for _, coin := range tc.args.deposit { - rewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, coin.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardSupplyRewards(runCtx, rewardPeriod) - suite.Require().NoError(err) - } - - // Accumulate borrow rewards for each deposit denom - for _, coin := range tc.args.borrow { - rewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, coin.Denom) - suite.Require().True(found) - err = suite.keeper.AccumulateHardBorrowRewards(runCtx, rewardPeriod) - suite.Require().NoError(err) - } - - // Sync hard supply rewards - deposit, found := suite.hardKeeper.GetDeposit(suite.ctx, userAddr) - suite.Require().True(found) - suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) - - // Sync hard borrow rewards - borrow, found := suite.hardKeeper.GetBorrow(suite.ctx, userAddr) - suite.Require().True(found) - suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) - - // Fetch pre-claim balances - preClaimAcc := ak.GetAccount(runCtx, suite.addrs[2]) - - err = suite.keeper.ClaimHardRewardVVesting(runCtx, userAddr, suite.addrs[2], tc.args.multiplier) - if tc.errArgs.expectPass { - suite.Require().NoError(err) - - // Check that user's balance has increased by expected reward amount - postClaimAcc := ak.GetAccount(suite.ctx, suite.addrs[2]) - suite.Require().Equal(preClaimAcc.GetCoins().Add(tc.args.expectedRewards...), postClaimAcc.GetCoins()) - - vacc, ok := postClaimAcc.(*vesting.PeriodicVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) - - // Check that each claim reward coin's amount has been reset to 0 - claim, found := suite.keeper.GetHardLiquidityProviderClaim(runCtx, suite.addrs[3]) - suite.Require().True(found) - for _, claimRewardCoin := range claim.Reward { - suite.Require().Equal(c(claimRewardCoin.Denom, 0), claimRewardCoin) - } - } else { - suite.Require().Error(err) - suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) - } - }) - } -} - func (suite *PayoutTestSuite) TestSendCoinsToPeriodicVestingAccount() { type accountArgs struct { periods vesting.Periods diff --git a/x/incentive/keeper/rewards_borrow_test.go b/x/incentive/keeper/rewards_borrow_test.go index 335dcf90..e15bba18 100644 --- a/x/incentive/keeper/rewards_borrow_test.go +++ b/x/incentive/keeper/rewards_borrow_test.go @@ -15,6 +15,7 @@ import ( "github.com/kava-labs/kava/x/hard" hardkeeper "github.com/kava-labs/kava/x/hard/keeper" "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" ) @@ -53,7 +54,7 @@ func (suite *BorrowRewardsTestSuite) SetupApp() { suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) } -func (suite *BorrowRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder IncentiveGenesisBuilder, hardBuilder HardGenesisBuilder) { +func (suite *BorrowRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder, hardBuilder testutil.HardGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( @@ -162,7 +163,7 @@ func (suite *BorrowRewardsTestSuite) TestAccumulateHardBorrowRewards() { cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleBorrowRewardPeriod(tc.args.borrow.Denom, tc.args.rewardsPerSecond) @@ -317,7 +318,7 @@ func (suite *BorrowRewardsTestSuite) TestInitializeHardBorrowRewards() { cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) for moneyMarketDenom, rewardsPerSecond := range tc.args.moneyMarketRewardDenoms { incentBuilder = incentBuilder.WithSimpleBorrowRewardPeriod(moneyMarketDenom, rewardsPerSecond) } @@ -506,7 +507,7 @@ func (suite *BorrowRewardsTestSuite) TestSynchronizeHardBorrowReward() { WithSimpleAccount(suite.addrs[2], cs(c("ukava", 1e9))). WithSimpleAccount(userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15))) - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) if tc.args.rewardsPerSecond != nil { incentBuilder = incentBuilder.WithSimpleBorrowRewardPeriod(tc.args.incentiveBorrowRewardDenom, tc.args.rewardsPerSecond) } @@ -833,7 +834,7 @@ func (suite *BorrowRewardsTestSuite) TestUpdateHardBorrowIndexDenoms() { cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleBorrowRewardPeriod("bnb", tc.args.rewardsPerSecond). WithSimpleBorrowRewardPeriod("ukava", tc.args.rewardsPerSecond). @@ -935,7 +936,7 @@ func (suite *BorrowRewardsTestSuite) TestSimulateHardBorrowRewardSynchronization userAddr := suite.addrs[3] authBuilder := app.NewAuthGenesisBuilder().WithSimpleAccount(userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15))) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleBorrowRewardPeriod(tc.args.borrow.Denom, tc.args.rewardsPerSecond) diff --git a/x/incentive/keeper/rewards_delegator_sync_test.go b/x/incentive/keeper/rewards_delegator_sync_test.go index d35d780a..6f6afe61 100644 --- a/x/incentive/keeper/rewards_delegator_sync_test.go +++ b/x/incentive/keeper/rewards_delegator_sync_test.go @@ -172,10 +172,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestRewardIsIncreasedWhenNewReward suite.Equal(newGlobalIndexes, syncedClaim.RewardIndexes) suite.Equal( - cs( - c(types.HardLiquidityRewardDenom, 100), - c("swp", 200), - ).Add(claim.Reward...), + cs(c("hard", 100), c("swp", 200)).Add(claim.Reward...), syncedClaim.Reward, ) } @@ -237,10 +234,7 @@ func (suite *SynchronizeDelegatorRewardTests) TestRewardIsIncreasedWhenGlobalFac syncedClaim, _ := suite.keeper.GetDelegatorClaim(suite.ctx, claim.Owner) suite.Equal( - cs( - c(types.HardLiquidityRewardDenom, 100), - c("swp", 200), - ).Add(claim.Reward...), + cs(c("hard", 100), c("swp", 200)).Add(claim.Reward...), syncedClaim.Reward, ) } diff --git a/x/incentive/keeper/rewards_delegator_test.go b/x/incentive/keeper/rewards_delegator_test.go index a4d577ba..00027d6a 100644 --- a/x/incentive/keeper/rewards_delegator_test.go +++ b/x/incentive/keeper/rewards_delegator_test.go @@ -13,6 +13,7 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" ) @@ -53,7 +54,7 @@ func (suite *DelegatorRewardsTestSuite) SetupApp() { suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) } -func (suite *DelegatorRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder IncentiveGenesisBuilder) { +func (suite *DelegatorRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( @@ -128,7 +129,7 @@ func (suite *DelegatorRewardsTestSuite) TestAccumulateDelegatorRewards() { WithSimpleAccount(suite.addrs[0], cs(c("ukava", 1e9))). WithSimpleAccount(sdk.AccAddress(suite.validatorAddrs[0]), cs(c("ukava", 1e9))) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(tc.args.delegation.Denom, tc.args.rewardsPerSecond) @@ -226,7 +227,7 @@ func (suite *DelegatorRewardsTestSuite) TestSynchronizeDelegatorReward() { WithSimpleAccount(suite.addrs[0], cs(c("ukava", 1e9))). WithSimpleAccount(sdk.AccAddress(suite.validatorAddrs[0]), cs(c("ukava", 1e9))) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(tc.args.delegation.Denom, tc.args.rewardsPerSecond) @@ -351,7 +352,7 @@ func (suite *DelegatorRewardsTestSuite) TestSimulateDelegatorRewardSynchronizati WithSimpleAccount(suite.addrs[0], cs(c("ukava", 1e9))). WithSimpleAccount(sdk.AccAddress(suite.validatorAddrs[0]), cs(c("ukava", 1e9))) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(tc.args.delegation.Denom, tc.args.rewardsPerSecond) @@ -458,7 +459,7 @@ func (suite *DelegatorRewardsTestSuite) TestUnbondingValidatorSyncsClaim() { rewardsPerSecond := cs(c("hard", 122354)) bondDenom := "ukava" - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(bondDenom, rewardsPerSecond) @@ -552,7 +553,7 @@ func (suite *DelegatorRewardsTestSuite) TestBondingValidatorSyncsClaim() { rewardsPerSecond := cs(c("hard", 122354)) bondDenom := "ukava" - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(bondDenom, rewardsPerSecond) @@ -644,7 +645,7 @@ func (suite *DelegatorRewardsTestSuite) TestSlashingValidatorSyncsClaim() { rewardsPerSecond := cs(c("hard", 122354)) bondDenom := "ukava" - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(bondDenom, rewardsPerSecond) @@ -725,7 +726,7 @@ func (suite *DelegatorRewardsTestSuite) TestRedelegationSyncsClaim() { rewardsPerSecond := cs(c("hard", 122354)) bondDenom := "ukava" - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleDelegatorRewardPeriod(bondDenom, rewardsPerSecond) diff --git a/x/incentive/keeper/rewards_supply_test.go b/x/incentive/keeper/rewards_supply_test.go index 57e6b550..266ba6dc 100644 --- a/x/incentive/keeper/rewards_supply_test.go +++ b/x/incentive/keeper/rewards_supply_test.go @@ -15,6 +15,7 @@ import ( "github.com/kava-labs/kava/x/hard" hardkeeper "github.com/kava-labs/kava/x/hard/keeper" "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" "github.com/kava-labs/kava/x/incentive/types" ) @@ -53,7 +54,7 @@ func (suite *SupplyRewardsTestSuite) SetupApp() { suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) } -func (suite *SupplyRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder IncentiveGenesisBuilder, hardBuilder HardGenesisBuilder) { +func (suite *SupplyRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder, hardBuilder testutil.HardGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( @@ -163,7 +164,7 @@ func (suite *SupplyRewardsTestSuite) TestAccumulateHardSupplyRewards() { cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) // suite.SetupWithGenState(authBuilder) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime) if tc.args.rewardsPerSecond != nil { incentBuilder = incentBuilder.WithSimpleSupplyRewardPeriod(tc.args.deposit.Denom, tc.args.rewardsPerSecond) @@ -317,7 +318,7 @@ func (suite *SupplyRewardsTestSuite) TestInitializeHardSupplyRewards() { cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime) for moneyMarketDenom, rewardsPerSecond := range tc.args.moneyMarketRewardDenoms { incentBuilder = incentBuilder.WithSimpleSupplyRewardPeriod(moneyMarketDenom, rewardsPerSecond) } @@ -506,7 +507,7 @@ func (suite *SupplyRewardsTestSuite) TestSynchronizeHardSupplyReward() { WithSimpleAccount(suite.addrs[2], cs(c("ukava", 1e9))). WithSimpleAccount(userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15))) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime) if tc.args.rewardsPerSecond != nil { incentBuilder = incentBuilder.WithSimpleSupplyRewardPeriod(tc.args.incentiveSupplyRewardDenom, tc.args.rewardsPerSecond) @@ -809,7 +810,7 @@ func (suite *SupplyRewardsTestSuite) TestUpdateHardSupplyIndexDenoms() { userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleSupplyRewardPeriod("bnb", tc.args.rewardsPerSecond). WithSimpleSupplyRewardPeriod("ukava", tc.args.rewardsPerSecond). @@ -893,7 +894,7 @@ func (suite *SupplyRewardsTestSuite) TestSimulateHardSupplyRewardSynchronization userAddr, cs(c("bnb", 1e15), c("ukava", 1e15), c("btcb", 1e15), c("xrp", 1e15), c("zzz", 1e15)), ) - incentBuilder := NewIncentiveGenesisBuilder(). + incentBuilder := testutil.NewIncentiveGenesisBuilder(). WithGenesisTime(suite.genesisTime). WithSimpleSupplyRewardPeriod(tc.args.deposit.Denom, tc.args.rewardsPerSecond) diff --git a/x/incentive/keeper/rewards_swap.go b/x/incentive/keeper/rewards_swap.go index 3c20f3f0..bd68e020 100644 --- a/x/incentive/keeper/rewards_swap.go +++ b/x/incentive/keeper/rewards_swap.go @@ -110,7 +110,7 @@ func (k Keeper) GetSynchronizedSwapClaim(ctx sdk.Context, owner sdk.AccAddress) for _, indexes := range claim.RewardIndexes { poolID := indexes.CollateralType - shares, found := k.swapKeeper.GetDepositorSharesInPool(ctx, owner, poolID) + shares, found := k.swapKeeper.GetDepositorSharesAmount(ctx, owner, poolID) if !found { shares = sdk.ZeroInt() } diff --git a/x/incentive/keeper/rewards_swap_test.go b/x/incentive/keeper/rewards_swap_test.go index ab97ff44..f368cab5 100644 --- a/x/incentive/keeper/rewards_swap_test.go +++ b/x/incentive/keeper/rewards_swap_test.go @@ -258,7 +258,7 @@ type fakeSwapKeeper struct { func (k fakeSwapKeeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Int, bool) { return k.poolShares, true } -func (k fakeSwapKeeper) GetDepositorSharesInPool(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) { +func (k fakeSwapKeeper) GetDepositorSharesAmount(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) { // This is just to implement the swap keeper interface. return sdk.Int{}, false } diff --git a/x/incentive/keeper/rewards_usdx_test.go b/x/incentive/keeper/rewards_usdx_test.go index 82b0bc44..73fdd230 100644 --- a/x/incentive/keeper/rewards_usdx_test.go +++ b/x/incentive/keeper/rewards_usdx_test.go @@ -12,6 +12,7 @@ import ( cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper" cdptypes "github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/testutil" ) // Test suite used for all keeper tests @@ -47,7 +48,7 @@ func (suite *USDXRewardsTestSuite) SetupApp() { suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: suite.genesisTime}) } -func (suite *USDXRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder IncentiveGenesisBuilder) { +func (suite *USDXRewardsTestSuite) SetupWithGenState(authBuilder app.AuthGenesisBuilder, incentBuilder testutil.IncentiveGenesisBuilder) { suite.SetupApp() suite.app.InitializeFromGenesisStatesWithTime( @@ -105,7 +106,7 @@ func (suite *USDXRewardsTestSuite) TestAccumulateUSDXMintingRewards() { } for _, tc := range testCases { suite.Run(tc.name, func() { - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(app.NewAuthGenesisBuilder(), incentBuilder) @@ -169,7 +170,7 @@ func (suite *USDXRewardsTestSuite) TestSynchronizeUSDXMintingReward() { for _, tc := range testCases { suite.Run(tc.name, func() { authBuilder := app.NewAuthGenesisBuilder().WithSimpleAccount(suite.addrs[0], cs(tc.args.initialCollateral)) - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(authBuilder, incentBuilder) @@ -256,7 +257,7 @@ func (suite *USDXRewardsTestSuite) TestSimulateUSDXMintingRewardSynchronization( for _, tc := range testCases { suite.Run(tc.name, func() { authBuilder := app.NewAuthGenesisBuilder().WithSimpleAccount(suite.addrs[0], cs(tc.args.initialCollateral)) - incentBuilder := NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) + incentBuilder := testutil.NewIncentiveGenesisBuilder().WithGenesisTime(suite.genesisTime).WithSimpleUSDXRewardPeriod(tc.args.ctype, tc.args.rewardsPerSecond) suite.SetupWithGenState(authBuilder, incentBuilder) diff --git a/x/incentive/testutil/builder.go b/x/incentive/testutil/builder.go new file mode 100644 index 00000000..7fd8b136 --- /dev/null +++ b/x/incentive/testutil/builder.go @@ -0,0 +1,201 @@ +package testutil + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/hard" + hardtypes "github.com/kava-labs/kava/x/hard/types" + "github.com/kava-labs/kava/x/incentive/types" +) + +const ( + oneYear time.Duration = time.Hour * 24 * 365 +) + +// IncentiveGenesisBuilder is a tool for creating an incentive genesis state. +// Helper methods add values onto a default genesis state. +// All methods are immutable and return updated copies of the builder. +type IncentiveGenesisBuilder struct { + types.GenesisState + genesisTime time.Time +} + +func NewIncentiveGenesisBuilder() IncentiveGenesisBuilder { + return IncentiveGenesisBuilder{ + GenesisState: types.DefaultGenesisState(), + genesisTime: time.Time{}, + } +} + +func (builder IncentiveGenesisBuilder) Build() types.GenesisState { + return builder.GenesisState +} + +func (builder IncentiveGenesisBuilder) BuildMarshalled() app.GenesisState { + return app.GenesisState{ + types.ModuleName: types.ModuleCdc.MustMarshalJSON(builder.Build()), + } +} + +func (builder IncentiveGenesisBuilder) WithGenesisTime(time time.Time) IncentiveGenesisBuilder { + builder.genesisTime = time + builder.Params.ClaimEnd = time.Add(5 * oneYear) + return builder +} + +func (builder IncentiveGenesisBuilder) WithInitializedBorrowRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.HardBorrowRewardPeriods = append(builder.Params.HardBorrowRewardPeriods, period) + + accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) + builder.HardBorrowAccumulationTimes = append(builder.HardBorrowAccumulationTimes, accumulationTimeForPeriod) + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleBorrowRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedBorrowRewardPeriod(types.NewMultiRewardPeriod( + true, + ctype, + builder.genesisTime, + builder.genesisTime.Add(4*oneYear), + rewardsPerSecond, + )) +} + +func (builder IncentiveGenesisBuilder) WithInitializedSupplyRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.HardSupplyRewardPeriods = append(builder.Params.HardSupplyRewardPeriods, period) + + accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) + builder.HardSupplyAccumulationTimes = append(builder.HardSupplyAccumulationTimes, accumulationTimeForPeriod) + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleSupplyRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedSupplyRewardPeriod(types.NewMultiRewardPeriod( + true, + ctype, + builder.genesisTime, + builder.genesisTime.Add(4*oneYear), + rewardsPerSecond, + )) +} + +func (builder IncentiveGenesisBuilder) WithInitializedDelegatorRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.DelegatorRewardPeriods = append(builder.Params.DelegatorRewardPeriods, period) + + accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) + builder.DelegatorAccumulationTimes = append(builder.DelegatorAccumulationTimes, accumulationTimeForPeriod) + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleDelegatorRewardPeriod(ctype string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedDelegatorRewardPeriod(types.NewMultiRewardPeriod( + true, + ctype, + builder.genesisTime, + builder.genesisTime.Add(4*oneYear), + rewardsPerSecond, + )) +} + +func (builder IncentiveGenesisBuilder) WithInitializedSwapRewardPeriod(period types.MultiRewardPeriod) IncentiveGenesisBuilder { + builder.Params.SwapRewardPeriods = append(builder.Params.SwapRewardPeriods, period) + + accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) + builder.SwapAccumulationTimes = append(builder.SwapAccumulationTimes, accumulationTimeForPeriod) + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleSwapRewardPeriod(poolID string, rewardsPerSecond sdk.Coins) IncentiveGenesisBuilder { + return builder.WithInitializedSwapRewardPeriod(types.NewMultiRewardPeriod( + true, + poolID, + builder.genesisTime, + builder.genesisTime.Add(4*oneYear), + rewardsPerSecond, + )) +} + +func (builder IncentiveGenesisBuilder) WithInitializedUSDXRewardPeriod(period types.RewardPeriod) IncentiveGenesisBuilder { + builder.Params.USDXMintingRewardPeriods = append(builder.Params.USDXMintingRewardPeriods, period) + + accumulationTimeForPeriod := types.NewGenesisAccumulationTime(period.CollateralType, builder.genesisTime) + builder.USDXAccumulationTimes = append(builder.USDXAccumulationTimes, accumulationTimeForPeriod) + return builder +} + +func (builder IncentiveGenesisBuilder) WithSimpleUSDXRewardPeriod(ctype string, rewardsPerSecond sdk.Coin) IncentiveGenesisBuilder { + return builder.WithInitializedUSDXRewardPeriod(types.NewRewardPeriod( + true, + ctype, + builder.genesisTime, + builder.genesisTime.Add(4*oneYear), + rewardsPerSecond, + )) +} + +func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.Multipliers) IncentiveGenesisBuilder { + builder.Params.ClaimMultipliers = multipliers + return builder +} + +// HardGenesisBuilder is a tool for creating a hard genesis state. +// Helper methods add values onto a default genesis state. +// All methods are immutable and return updated copies of the builder. +type HardGenesisBuilder struct { + hardtypes.GenesisState + genesisTime time.Time +} + +func NewHardGenesisBuilder() HardGenesisBuilder { + return HardGenesisBuilder{ + GenesisState: hardtypes.DefaultGenesisState(), + } +} +func (builder HardGenesisBuilder) Build() hardtypes.GenesisState { + return builder.GenesisState +} +func (builder HardGenesisBuilder) BuildMarshalled() app.GenesisState { + return app.GenesisState{ + hardtypes.ModuleName: hardtypes.ModuleCdc.MustMarshalJSON(builder.Build()), + } +} +func (builder HardGenesisBuilder) WithGenesisTime(genTime time.Time) HardGenesisBuilder { + builder.genesisTime = genTime + return builder +} +func (builder HardGenesisBuilder) WithInitializedMoneyMarket(market hard.MoneyMarket) HardGenesisBuilder { + builder.Params.MoneyMarkets = append(builder.Params.MoneyMarkets, market) + + builder.PreviousAccumulationTimes = append( + builder.PreviousAccumulationTimes, + hardtypes.NewGenesisAccumulationTime(market.Denom, builder.genesisTime, sdk.OneDec(), sdk.OneDec()), + ) + return builder +} +func (builder HardGenesisBuilder) WithMinBorrow(minUSDValue sdk.Dec) HardGenesisBuilder { + builder.Params.MinimumBorrowUSDValue = minUSDValue + return builder +} +func NewStandardMoneyMarket(denom string) hardtypes.MoneyMarket { + return hardtypes.NewMoneyMarket( + denom, + hard.NewBorrowLimit( + false, + sdk.NewDec(1e15), + sdk.MustNewDecFromStr("0.6"), + ), + denom+":usd", + sdk.NewInt(1e6), + hard.NewInterestRateModel( + sdk.MustNewDecFromStr("0.05"), + sdk.MustNewDecFromStr("2"), + sdk.MustNewDecFromStr("0.8"), + sdk.MustNewDecFromStr("10"), + ), + sdk.MustNewDecFromStr("0.05"), + sdk.ZeroDec(), + ) +} diff --git a/x/incentive/testutil/integration.go b/x/incentive/testutil/integration.go new file mode 100644 index 00000000..e53dfa4d --- /dev/null +++ b/x/incentive/testutil/integration.go @@ -0,0 +1,167 @@ +package testutil + +import ( + "errors" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/staking" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/hard" + "github.com/kava-labs/kava/x/incentive" + "github.com/kava-labs/kava/x/swap" +) + +type IntegrationTester struct { + suite.Suite + App app.TestApp + Ctx sdk.Context +} + +func (suite *IntegrationTester) NextBlockAt(blockTime time.Time) { + if !suite.Ctx.BlockTime().Before(blockTime) { + panic(fmt.Sprintf("new block time %s must be after current %s", blockTime, suite.Ctx.BlockTime())) + } + blockHeight := suite.Ctx.BlockHeight() + 1 + + _ = suite.App.EndBlocker(suite.Ctx, abci.RequestEndBlock{}) + + suite.Ctx = suite.Ctx.WithBlockTime(blockTime).WithBlockHeight(blockHeight) + + _ = suite.App.BeginBlocker(suite.Ctx, abci.RequestBeginBlock{}) // height and time in RequestBeginBlock are ignored by module begin blockers +} + +func (suite *IntegrationTester) NextBlockAfter(blockDuration time.Duration) { + suite.NextBlockAt(suite.Ctx.BlockTime().Add(blockDuration)) +} + +func (suite *IntegrationTester) DeliverIncentiveMsg(msg sdk.Msg) error { + handler := incentive.NewHandler(suite.App.GetIncentiveKeeper()) + _, err := handler(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverMsgCreateValidator(address sdk.ValAddress, selfDelegation sdk.Coin) error { + msg := staking.NewMsgCreateValidator( + address, + ed25519.GenPrivKey().PubKey(), + selfDelegation, + staking.Description{}, + staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.NewInt(1_000_000), + ) + handler := staking.NewHandler(suite.App.GetStakingKeeper()) + _, err := handler(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverMsgDelegate(delegator sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) error { + msg := staking.NewMsgDelegate( + delegator, + validator, + amount, + ) + handleStakingMsg := staking.NewHandler(suite.App.GetStakingKeeper()) + _, err := handleStakingMsg(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverSwapMsgDeposit(depositor sdk.AccAddress, tokenA, tokenB sdk.Coin, slippage sdk.Dec) error { + msg := swap.NewMsgDeposit( + depositor, + tokenA, + tokenB, + slippage, + suite.Ctx.BlockTime().Add(time.Hour).Unix(), // ensure msg will not fail due to short deadline + ) + _, err := swap.NewHandler(suite.App.GetSwapKeeper())(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverHardMsgDeposit(depositor sdk.AccAddress, deposit sdk.Coins) error { + msg := hard.NewMsgDeposit(depositor, deposit) + _, err := hard.NewHandler(suite.App.GetHardKeeper())(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverHardMsgBorrow(depositor sdk.AccAddress, borrow sdk.Coins) error { + msg := hard.NewMsgBorrow(depositor, borrow) + _, err := hard.NewHandler(suite.App.GetHardKeeper())(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) DeliverMsgCreateCDP(owner sdk.AccAddress, collateral, principal sdk.Coin, collateralType string) error { + msg := cdp.NewMsgCreateCDP(owner, collateral, principal, collateralType) + _, err := cdp.NewHandler(suite.App.GetCDPKeeper())(suite.Ctx, msg) + return err +} + +func (suite *IntegrationTester) GetAccount(addr sdk.AccAddress) authexported.Account { + ak := suite.App.GetAccountKeeper() + return ak.GetAccount(suite.Ctx, addr) +} + +func (suite *IntegrationTester) GetModuleAccount(name string) supplyexported.ModuleAccountI { + sk := suite.App.GetSupplyKeeper() + return sk.GetModuleAccount(suite.Ctx, name) +} + +func (suite *IntegrationTester) GetBalance(address sdk.AccAddress) sdk.Coins { + acc := suite.App.GetAccountKeeper().GetAccount(suite.Ctx, address) + if acc != nil { + return acc.GetCoins() + } else { + return nil + } +} + +func (suite *IntegrationTester) ErrorIs(err, target error) bool { + return suite.Truef(errors.Is(err, target), "err didn't match: %s, it was: %s", target, err) +} + +func (suite *IntegrationTester) BalanceEquals(address sdk.AccAddress, expected sdk.Coins) { + acc := suite.App.GetAccountKeeper().GetAccount(suite.Ctx, address) + suite.Require().NotNil(acc, "expected account to not be nil") + suite.Equalf(expected, acc.GetCoins(), "expected account balance to equal coins %s, but got %s", expected, acc.GetCoins()) +} + +func (suite *IntegrationTester) VestingPeriodsEqual(address sdk.AccAddress, expectedPeriods vesting.Periods) { + acc := suite.App.GetAccountKeeper().GetAccount(suite.Ctx, address) + suite.Require().NotNil(acc, "expected vesting account not to be nil") + vacc, ok := acc.(*vesting.PeriodicVestingAccount) + suite.Require().True(ok, "expected vesting account to be type PeriodicVestingAccount") + suite.Equal(expectedPeriods, vacc.VestingPeriods) +} + +func (suite *IntegrationTester) SwapRewardEquals(owner sdk.AccAddress, expected sdk.Coins) { + claim, found := suite.App.GetIncentiveKeeper().GetSwapClaim(suite.Ctx, owner) + suite.Require().Truef(found, "expected swap claim to be found for %s", owner) + suite.Equalf(expected, claim.Reward, "expected swap claim reward to be %s, but got %s", expected, claim.Reward) +} + +func (suite *IntegrationTester) DelegatorRewardEquals(owner sdk.AccAddress, expected sdk.Coins) { + claim, found := suite.App.GetIncentiveKeeper().GetDelegatorClaim(suite.Ctx, owner) + suite.Require().Truef(found, "expected delegator claim to be found for %s", owner) + suite.Equalf(expected, claim.Reward, "expected delegator claim reward to be %s, but got %s", expected, claim.Reward) +} + +func (suite *IntegrationTester) HardRewardEquals(owner sdk.AccAddress, expected sdk.Coins) { + claim, found := suite.App.GetIncentiveKeeper().GetHardLiquidityProviderClaim(suite.Ctx, owner) + suite.Require().Truef(found, "expected delegator claim to be found for %s", owner) + suite.Equalf(expected, claim.Reward, "expected delegator claim reward to be %s, but got %s", expected, claim.Reward) +} + +func (suite *IntegrationTester) USDXRewardEquals(owner sdk.AccAddress, expected sdk.Coin) { + claim, found := suite.App.GetIncentiveKeeper().GetUSDXMintingClaim(suite.Ctx, owner) + suite.Require().Truef(found, "expected delegator claim to be found for %s", owner) + suite.Equalf(expected, claim.Reward, "expected delegator claim reward to be %s, but got %s", expected, claim.Reward) +} diff --git a/x/incentive/types/account.go b/x/incentive/types/account.go deleted file mode 100644 index d651d7ee..00000000 --- a/x/incentive/types/account.go +++ /dev/null @@ -1,14 +0,0 @@ -package types - -import ( - "github.com/cosmos/cosmos-sdk/x/auth/vesting" -) - -// GetTotalVestingPeriodLength returns the summed length of all vesting periods -func GetTotalVestingPeriodLength(periods vesting.Periods) int64 { - length := int64(0) - for _, period := range periods { - length += period.Length - } - return length -} diff --git a/x/incentive/types/account_test.go b/x/incentive/types/account_test.go deleted file mode 100644 index 5c1f6560..00000000 --- a/x/incentive/types/account_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package types_test - -import ( - "testing" - - "github.com/stretchr/testify/suite" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/vesting" - - "github.com/kava-labs/kava/x/incentive/types" -) - -type accountTest struct { - periods vesting.Periods - expectedVal int64 -} - -type AccountTestSuite struct { - suite.Suite - - tests []accountTest -} - -func (suite *AccountTestSuite) SetupTest() { - tests := []accountTest{ - { - periods: vesting.Periods{ - vesting.Period{ - Length: int64(100), - Amount: sdk.Coins{}, - }, - vesting.Period{ - Length: int64(200), - Amount: sdk.Coins{}, - }, - }, - expectedVal: int64(300), - }, - } - suite.tests = tests -} - -func (suite *AccountTestSuite) TestGetTotalPeriodLength() { - for _, t := range suite.tests { - length := types.GetTotalVestingPeriodLength(t.periods) - suite.Equal(t.expectedVal, length) - } -} - -func TestAccountTestSuite(t *testing.T) { - suite.Run(t, new(AccountTestSuite)) -} diff --git a/x/incentive/types/claims.go b/x/incentive/types/claims.go index bf382a94..d3e586ce 100644 --- a/x/incentive/types/claims.go +++ b/x/incentive/types/claims.go @@ -13,7 +13,8 @@ const ( HardLiquidityProviderClaimType = "hard_liquidity_provider" DelegatorClaimType = "delegator_claim" SwapClaimType = "swap" - BondDenom = "ukava" + + BondDenom = "ukava" ) // Claim is an interface for handling common claim actions diff --git a/x/incentive/types/codec.go b/x/incentive/types/codec.go index 747fae6c..5118ddd7 100644 --- a/x/incentive/types/codec.go +++ b/x/incentive/types/codec.go @@ -18,12 +18,16 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(USDXMintingClaim{}, "incentive/USDXMintingClaim", nil) cdc.RegisterConcrete(HardLiquidityProviderClaim{}, "incentive/HardLiquidityProviderClaim", nil) cdc.RegisterConcrete(DelegatorClaim{}, "incentive/DelegatorClaim", nil) + cdc.RegisterConcrete(SwapClaim{}, "incentive/SwapClaim", nil) // Register msgs cdc.RegisterConcrete(MsgClaimUSDXMintingReward{}, "incentive/MsgClaimUSDXMintingReward", nil) cdc.RegisterConcrete(MsgClaimHardReward{}, "incentive/MsgClaimHardReward", nil) cdc.RegisterConcrete(MsgClaimDelegatorReward{}, "incentive/MsgClaimDelegatorReward", nil) + cdc.RegisterConcrete(MsgClaimSwapReward{}, "incentive/MsgClaimSwapReward", nil) + cdc.RegisterConcrete(MsgClaimUSDXMintingRewardVVesting{}, "incentive/MsgClaimUSDXRewardVVesting", nil) cdc.RegisterConcrete(MsgClaimHardRewardVVesting{}, "incentive/MsgClaimHardRewardVVesting", nil) cdc.RegisterConcrete(MsgClaimDelegatorRewardVVesting{}, "incentive/MsgClaimDelegatorRewardVVesting", nil) + cdc.RegisterConcrete(MsgClaimSwapRewardVVesting{}, "incentive/MsgClaimSwapRewardVVesting", nil) } diff --git a/x/incentive/types/errors.go b/x/incentive/types/errors.go index f53157a9..0eeeb6f1 100644 --- a/x/incentive/types/errors.go +++ b/x/incentive/types/errors.go @@ -20,4 +20,5 @@ var ( ErrInvalidClaimType = sdkerrors.Register(ModuleName, 11, "invalid claim type") ErrInvalidClaimOwner = sdkerrors.Register(ModuleName, 12, "invalid claim owner") ErrDecreasingRewardFactor = sdkerrors.Register(ModuleName, 13, "found new reward factor less than an old reward factor") + ErrInvalidClaimDenoms = sdkerrors.Register(ModuleName, 14, "invalid claim denoms") ) diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index 97e76f5c..ece16c7d 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -54,7 +54,7 @@ type HardKeeper interface { // SwapKeeper defines the required methods needed by this modules keeper type SwapKeeper interface { GetPoolShares(ctx sdk.Context, poolID string) (shares sdk.Int, found bool) - GetDepositorSharesInPool(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (shares sdk.Int, found bool) + GetDepositorSharesAmount(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (shares sdk.Int, found bool) } // AccountKeeper defines the expected keeper interface for interacting with account diff --git a/x/incentive/types/keys.go b/x/incentive/types/keys.go index 27ed7d08..717bcc96 100644 --- a/x/incentive/types/keys.go +++ b/x/incentive/types/keys.go @@ -17,10 +17,6 @@ const ( QuerierRoute = ModuleName ) -// TODO: Refactor so that each incentive type has: -// 1. [Incentive]ClaimKeyPrefix -// 2. [Incentve]AccumulatorKeyPrefix { PreviousAccrualTime block.Time, IndexFactors types.IndexFactors } - // Key Prefixes var ( USDXMintingClaimKeyPrefix = []byte{0x01} // prefix for keys that store USDX minting claims @@ -38,6 +34,5 @@ var ( SwapRewardIndexesKeyPrefix = []byte{0x13} // prefix for key that stores swap reward indexes PreviousSwapRewardAccrualTimeKeyPrefix = []byte{0x14} // prefix for key that stores the previous time swap rewards accrued - USDXMintingRewardDenom = "ukava" - HardLiquidityRewardDenom = "hard" + USDXMintingRewardDenom = "ukava" ) diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go index 225bc1f9..4c5e2541 100644 --- a/x/incentive/types/msg.go +++ b/x/incentive/types/msg.go @@ -1,17 +1,21 @@ package types import ( - "strings" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const MaxDenomsToClaim = 1000 + // ensure Msg interface compliance at compile time var _ sdk.Msg = &MsgClaimUSDXMintingReward{} +var _ sdk.Msg = &MsgClaimUSDXMintingRewardVVesting{} var _ sdk.Msg = &MsgClaimHardReward{} var _ sdk.Msg = &MsgClaimHardRewardVVesting{} -var _ sdk.Msg = &MsgClaimUSDXMintingRewardVVesting{} +var _ sdk.Msg = &MsgClaimDelegatorReward{} +var _ sdk.Msg = &MsgClaimDelegatorRewardVVesting{} +var _ sdk.Msg = &MsgClaimSwapReward{} +var _ sdk.Msg = &MsgClaimSwapRewardVVesting{} // MsgClaimUSDXMintingReward message type used to claim USDX minting rewards type MsgClaimUSDXMintingReward struct { @@ -38,7 +42,10 @@ func (msg MsgClaimUSDXMintingReward) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -84,7 +91,10 @@ func (msg MsgClaimUSDXMintingRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -102,13 +112,15 @@ func (msg MsgClaimUSDXMintingRewardVVesting) GetSigners() []sdk.AccAddress { type MsgClaimHardReward struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimHardReward returns a new MsgClaimHardReward. -func NewMsgClaimHardReward(sender sdk.AccAddress, multiplierName string) MsgClaimHardReward { +func NewMsgClaimHardReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimHardReward { return MsgClaimHardReward{ Sender: sender, MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, } } @@ -125,7 +137,18 @@ func (msg MsgClaimHardReward) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -144,14 +167,16 @@ type MsgClaimHardRewardVVesting struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimHardRewardVVesting returns a new MsgClaimHardRewardVVesting. -func NewMsgClaimHardRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string) MsgClaimHardRewardVVesting { +func NewMsgClaimHardRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimHardRewardVVesting { return MsgClaimHardRewardVVesting{ Sender: sender, Receiver: receiver, MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, } } @@ -171,7 +196,18 @@ func (msg MsgClaimHardRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -189,13 +225,15 @@ func (msg MsgClaimHardRewardVVesting) GetSigners() []sdk.AccAddress { type MsgClaimDelegatorReward struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimDelegatorReward returns a new MsgClaimDelegatorReward. -func NewMsgClaimDelegatorReward(sender sdk.AccAddress, multiplierName string) MsgClaimDelegatorReward { +func NewMsgClaimDelegatorReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimDelegatorReward { return MsgClaimDelegatorReward{ Sender: sender, MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, } } @@ -212,7 +250,18 @@ func (msg MsgClaimDelegatorReward) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -231,14 +280,16 @@ type MsgClaimDelegatorRewardVVesting struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // MsgClaimDelegatorRewardVVesting returns a new MsgClaimDelegatorRewardVVesting. -func NewMsgClaimDelegatorRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string) MsgClaimDelegatorRewardVVesting { +func NewMsgClaimDelegatorRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimDelegatorRewardVVesting { return MsgClaimDelegatorRewardVVesting{ Sender: sender, Receiver: receiver, MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, } } @@ -258,7 +309,18 @@ func (msg MsgClaimDelegatorRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil } // GetSignBytes gets the canonical byte representation of the Msg. @@ -271,3 +333,116 @@ func (msg MsgClaimDelegatorRewardVVesting) GetSignBytes() []byte { func (msg MsgClaimDelegatorRewardVVesting) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } + +// MsgClaimSwapReward message type used to claim delegator rewards +type MsgClaimSwapReward struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` +} + +// NewMsgClaimSwapReward returns a new MsgClaimSwapReward. +func NewMsgClaimSwapReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimSwapReward { + return MsgClaimSwapReward{ + Sender: sender, + MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, + } +} + +// Route return the message type used for routing the message. +func (msg MsgClaimSwapReward) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgClaimSwapReward) Type() string { + return "claim_swap_reward" +} + +// ValidateBasic does a simple validation check that doesn't require access to state. +func (msg MsgClaimSwapReward) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgClaimSwapReward) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgClaimSwapReward) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +// MsgClaimSwapRewardVVesting message type used to claim delegator rewards for validator vesting accounts +type MsgClaimSwapRewardVVesting struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` + DenomsToClaim []string `json:"denoms_to_claim" yaml:"denoms_to_claim"` +} + +// MsgClaimSwapRewardVVesting returns a new MsgClaimSwapRewardVVesting. +func NewMsgClaimSwapRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimSwapRewardVVesting { + return MsgClaimSwapRewardVVesting{ + Sender: sender, + Receiver: receiver, + MultiplierName: multiplierName, + DenomsToClaim: denomsToClaim, + } +} + +// Route return the message type used for routing the message. +func (msg MsgClaimSwapRewardVVesting) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgClaimSwapRewardVVesting) Type() string { + return "claim_swap_reward_vvesting" +} + +// ValidateBasic does a simple validation check that doesn't require access to state. +func (msg MsgClaimSwapRewardVVesting) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if msg.Receiver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") + } + if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + return err + } + for i, d := range msg.DenomsToClaim { + if i >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + if err := sdk.ValidateDenom(d); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgClaimSwapRewardVVesting) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgClaimSwapRewardVVesting) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} diff --git a/x/incentive/types/msg_test.go b/x/incentive/types/msg_test.go index 31544f57..4c9d3bda 100644 --- a/x/incentive/types/msg_test.go +++ b/x/incentive/types/msg_test.go @@ -1,72 +1,522 @@ package types_test import ( + "errors" + "fmt" "testing" - "github.com/stretchr/testify/suite" - sdk "github.com/cosmos/cosmos-sdk/types" - + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto" "github.com/kava-labs/kava/x/incentive/types" ) -type msgTest struct { - from sdk.AccAddress - multiplierName string - expectPass bool -} +func TestMsgClaimVVesting_Validate(t *testing.T) { + validAddress := sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))) -type MsgTestSuite struct { - suite.Suite - - tests []msgTest -} - -func (suite *MsgTestSuite) SetupTest() { - tests := []msgTest{ + type expectedErr struct { + wraps error + pass bool + } + type msgArgs struct { + sender sdk.AccAddress + receiver sdk.AccAddress + multiplierName string + denomsToClaim []string + } + tests := []struct { + name string + msgArgs msgArgs + expect expectedErr + }{ { - from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), - multiplierName: "large", - expectPass: true, + name: "large multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "large", + }, + expect: expectedErr{ + pass: true, + }, }, { - from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), - multiplierName: "medium", - expectPass: true, + name: "medium multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + pass: true, + }, }, { - from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), - multiplierName: "small", - expectPass: true, + name: "small multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "small", + }, + expect: expectedErr{ + pass: true, + }, }, { - from: sdk.AccAddress{}, - multiplierName: "medium", - expectPass: false, + name: "empty denoms to claim is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "small", + denomsToClaim: []string{}, + }, + expect: expectedErr{ + pass: true, + }, }, { - from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), - multiplierName: "huge", - expectPass: false, + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + receiver: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid receiver", + msgArgs: msgArgs{ + sender: validAddress, + receiver: sdk.AccAddress{}, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid multiplier", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "huge", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "multiplier with capitalization is invalid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "Large", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "invalid claim denom", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "small", + denomsToClaim: []string{"a denom string that is invalid because it is much too long"}, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "too many claim denoms", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "small", + denomsToClaim: tooManyClaimDenoms(), + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, }, } - suite.tests = tests -} -func (suite *MsgTestSuite) TestMsgValidation() { - for _, t := range suite.tests { - msg := types.NewMsgClaimUSDXMintingReward(t.from, t.multiplierName) - err := msg.ValidateBasic() - if t.expectPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) + for _, tc := range tests { + msgs := []sdk.Msg{ + types.NewMsgClaimHardRewardVVesting( + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + types.NewMsgClaimDelegatorRewardVVesting( + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + types.NewMsgClaimSwapRewardVVesting( + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + } + for _, msg := range msgs { + t.Run(msg.Type()+" "+tc.name, func(t *testing.T) { + + err := msg.ValidateBasic() + if tc.expect.pass { + require.NoError(t, err) + } else { + require.Truef(t, errors.Is(err, tc.expect.wraps), "expected error '%s' was not actual '%s'", tc.expect.wraps, err) + } + }) } } } -func TestMsgTestSuite(t *testing.T) { - suite.Run(t, new(MsgTestSuite)) +func TestMsgClaim_Validate(t *testing.T) { + validAddress := sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))) + + type expectedErr struct { + wraps error + pass bool + } + type msgArgs struct { + sender sdk.AccAddress + multiplierName string + denomsToClaim []string + } + tests := []struct { + name string + msgArgs msgArgs + expect expectedErr + }{ + { + name: "large multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "large", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "medium multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "small multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "small", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "empty denoms to claim is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "small", + denomsToClaim: []string{}, + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid multiplier", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "huge", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "multiplier with capitalization is invalid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "Large", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "invalid claim denom", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "small", + denomsToClaim: []string{"a denom string that is invalid because it is much too long"}, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "too many claim denoms", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "small", + denomsToClaim: tooManyClaimDenoms(), + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + } + + for _, tc := range tests { + msgs := []sdk.Msg{ + types.NewMsgClaimHardReward( + tc.msgArgs.sender, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + types.NewMsgClaimDelegatorReward( + tc.msgArgs.sender, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + types.NewMsgClaimSwapReward( + tc.msgArgs.sender, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + ), + } + for _, msg := range msgs { + t.Run(msg.Type()+" "+tc.name, func(t *testing.T) { + + err := msg.ValidateBasic() + if tc.expect.pass { + require.NoError(t, err) + } else { + require.Truef(t, errors.Is(err, tc.expect.wraps), "expected error '%s' was not actual '%s'", tc.expect.wraps, err) + } + }) + } + } +} + +func TestMsgClaimUSDXMintingRewardVVesting_Validate(t *testing.T) { + validAddress := sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))) + + type expectedErr struct { + wraps error + pass bool + } + type msgArgs struct { + sender sdk.AccAddress + receiver sdk.AccAddress + multiplierName string + } + tests := []struct { + name string + msgArgs msgArgs + expect expectedErr + }{ + { + name: "large multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "large", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "medium multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "small multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "small", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + receiver: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid receiver", + msgArgs: msgArgs{ + sender: validAddress, + receiver: sdk.AccAddress{}, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid multiplier", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "huge", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "multiplier with capitalization is invalid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + multiplierName: "Large", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + msg := types.NewMsgClaimUSDXMintingRewardVVesting(tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName) + + err := msg.ValidateBasic() + if tc.expect.pass { + require.NoError(t, err) + } else { + require.Truef(t, errors.Is(err, tc.expect.wraps), "expected error '%s' was not actual '%s'", tc.expect.wraps, err) + } + }) + } +} + +func TestMsgClaimUSDXMintingReward_Validate(t *testing.T) { + validAddress := sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))) + + type expectedErr struct { + wraps error + pass bool + } + type msgArgs struct { + sender sdk.AccAddress + multiplierName string + } + tests := []struct { + name string + msgArgs msgArgs + expect expectedErr + }{ + { + name: "large multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "large", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "medium multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "medium", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "small multiplier is valid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "small", + }, + expect: expectedErr{ + pass: true, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + multiplierName: "medium", + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid multiplier", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "huge", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + { + name: "multiplier with capitalization is invalid", + msgArgs: msgArgs{ + sender: validAddress, + multiplierName: "Large", + }, + expect: expectedErr{ + wraps: types.ErrInvalidMultiplier, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + msg := types.NewMsgClaimUSDXMintingReward(tc.msgArgs.sender, tc.msgArgs.multiplierName) + + err := msg.ValidateBasic() + if tc.expect.pass { + require.NoError(t, err) + } else { + require.Truef(t, errors.Is(err, tc.expect.wraps), "expected error '%s' was not actual '%s'", tc.expect.wraps, err) + } + }) + } +} + +func tooManyClaimDenoms() []string { + claimDenoms := make([]string, types.MaxDenomsToClaim+1) + for i := range claimDenoms { + claimDenoms[i] = fmt.Sprintf("denom%d", i) + } + return claimDenoms } diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index e9c30807..bf14267b 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -7,6 +7,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/params" tmtime "github.com/tendermint/tendermint/types/time" @@ -52,7 +53,7 @@ type Params struct { HardSupplyRewardPeriods MultiRewardPeriods `json:"hard_supply_reward_periods" yaml:"hard_supply_reward_periods"` HardBorrowRewardPeriods MultiRewardPeriods `json:"hard_borrow_reward_periods" yaml:"hard_borrow_reward_periods"` DelegatorRewardPeriods MultiRewardPeriods `json:"delegator_reward_periods" yaml:"delegator_reward_periods"` - SwapRewardPeriods MultiRewardPeriods `json:"swap_reward_periods" json:"swap_reward_periods"` + SwapRewardPeriods MultiRewardPeriods `json:"swap_reward_periods" yaml:"swap_reward_periods"` ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` } @@ -418,5 +419,5 @@ func (mn MultiplierName) IsValid() error { case Small, Medium, Large: return nil } - return fmt.Errorf("invalid multiplier name: %s", mn) + return sdkerrors.Wrapf(ErrInvalidMultiplier, "invalid multiplier name: %s", mn) } diff --git a/x/incentive/types/sdk.go b/x/incentive/types/sdk.go new file mode 100644 index 00000000..4923d821 --- /dev/null +++ b/x/incentive/types/sdk.go @@ -0,0 +1,41 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" +) + +// GetTotalVestingPeriodLength returns the summed length of all vesting periods +func GetTotalVestingPeriodLength(periods vesting.Periods) int64 { + length := int64(0) + for _, period := range periods { + length += period.Length + } + return length +} + +// MultiplyCoins multiplies each value in a set of coins by a single decimal value, rounding the result. +func MultiplyCoins(coins sdk.Coins, multiple sdk.Dec) sdk.Coins { + var result sdk.Coins + for _, coin := range coins { + result = result.Add( + sdk.NewCoin(coin.Denom, coin.Amount.ToDec().Mul(multiple).RoundInt()), + ) + } + return result +} + +// FilterCoins returns a subset of the coins by denom. Specifying no denoms will return the original coins. +func FilterCoins(coins sdk.Coins, denoms []string) sdk.Coins { + + if len(denoms) == 0 { + // with no filter, return all the coins + return coins + } + // otherwise select denoms in filter + var filteredCoins sdk.Coins + for _, denom := range denoms { + filteredCoins = filteredCoins.Add(sdk.NewCoin(denom, coins.AmountOf(denom))) + } + return filteredCoins +} diff --git a/x/incentive/types/sdk_test.go b/x/incentive/types/sdk_test.go new file mode 100644 index 00000000..3fc7d096 --- /dev/null +++ b/x/incentive/types/sdk_test.go @@ -0,0 +1,170 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + + "github.com/kava-labs/kava/x/incentive/types" +) + +func TestGetTotalVestingPeriodLength(t *testing.T) { + testCases := []struct { + name string + periods vesting.Periods + expectedVal int64 + }{ + { + name: "two period lengths are added together", + periods: vesting.Periods{ + { + Length: 100, + }, + { + Length: 200, + }, + }, + expectedVal: 300, + }, + { + name: "no periods returns zero", + periods: nil, + expectedVal: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + length := types.GetTotalVestingPeriodLength(tc.periods) + require.Equal(t, tc.expectedVal, length) + }) + } +} + +func TestMultiplyCoins(t *testing.T) { + testCases := []struct { + name string + coins sdk.Coins + multiple sdk.Dec + expected sdk.Coins + }{ + { + name: "decimals are rounded to nearest even, up", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + ), + multiple: sdk.MustNewDecFromStr("3.1415"), + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 3142), + ), + }, + { + name: "decimals are rounded to nearest even, down", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e7), + ), + multiple: sdk.MustNewDecFromStr("3.14159265"), + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 31415926), + ), + }, + { + name: "multiple coin amounts are multiplied", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1), + sdk.NewInt64Coin("ukava", 1e18), + ), + multiple: sdk.MustNewDecFromStr("2.000000000000000002"), + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 2), + sdk.NewInt64Coin("ukava", 2_000_000_000_000_000_002), + ), + }, + { + name: "empty coins return nil", + coins: sdk.Coins{}, + multiple: sdk.MustNewDecFromStr("2.5"), + expected: nil, + }, + { + name: "nil coins return nil", + coins: nil, + multiple: sdk.MustNewDecFromStr("2.5"), + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, + tc.expected, + types.MultiplyCoins(tc.coins, tc.multiple), + ) + }) + } +} + +func TestFilterCoins(t *testing.T) { + testCases := []struct { + name string + coins sdk.Coins + denoms []string + expected sdk.Coins + }{ + { + name: "non-empty filter selects subset of coins", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("ukava", 2e3), + sdk.NewInt64Coin("btc", 3e3), + ), + denoms: []string{"hard", "btc"}, + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("btc", 3e3), + ), + }, + { + name: "when coins are nil a non-empty filter returns nil coins", + coins: nil, + denoms: []string{"hard", "btc"}, + expected: nil, + }, + { + name: "nil filter returns original coins", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("ukava", 2e3), + ), + denoms: nil, + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("ukava", 2e3), + ), + }, + { + name: "empty filter returns original coins", + coins: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("ukava", 2e3), + ), + denoms: []string{}, + expected: sdk.NewCoins( + sdk.NewInt64Coin("hard", 1e3), + sdk.NewInt64Coin("ukava", 2e3), + ), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, + tc.expected, + types.FilterCoins(tc.coins, tc.denoms), + ) + }) + } +} diff --git a/x/swap/keeper/deposit.go b/x/swap/keeper/deposit.go index a76fa991..9417b915 100644 --- a/x/swap/keeper/deposit.go +++ b/x/swap/keeper/deposit.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/kava-labs/kava/x/swap/types" ) @@ -118,6 +119,8 @@ func (k Keeper) initializePool(ctx sdk.Context, poolID string, depositor sdk.Acc k.SetPool(ctx, poolRecord) k.SetDepositorShares(ctx, shareRecord) + k.hooks.AfterPoolDepositCreated(ctx, poolRecord.PoolID, depositor, shareRecord.SharesOwned) + return pool.Reserves(), pool.TotalShares(), nil } @@ -131,8 +134,10 @@ func (k Keeper) addLiquidityToPool(ctx sdk.Context, record types.PoolRecord, dep poolRecord := types.NewPoolRecord(pool) - shareRecord, found := k.GetDepositorShares(ctx, depositor, poolRecord.PoolID) - if found { + shareRecord, sharesFound := k.GetDepositorShares(ctx, depositor, poolRecord.PoolID) + if sharesFound { + k.hooks.BeforePoolDepositModified(ctx, poolRecord.PoolID, depositor, shareRecord.SharesOwned) + shareRecord.SharesOwned = shareRecord.SharesOwned.Add(shares) } else { shareRecord = types.NewShareRecord(depositor, poolRecord.PoolID, shares) @@ -141,5 +146,9 @@ func (k Keeper) addLiquidityToPool(ctx sdk.Context, record types.PoolRecord, dep k.SetPool(ctx, poolRecord) k.SetDepositorShares(ctx, shareRecord) + if !sharesFound { + k.hooks.AfterPoolDepositCreated(ctx, poolRecord.PoolID, depositor, shareRecord.SharesOwned) + } + return depositAmount, shares, nil } diff --git a/x/swap/keeper/hooks.go b/x/swap/keeper/hooks.go index 21945c2d..ffd0146c 100644 --- a/x/swap/keeper/hooks.go +++ b/x/swap/keeper/hooks.go @@ -6,21 +6,9 @@ import ( "github.com/kava-labs/kava/x/swap/types" ) -func (k Keeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Int, bool) { - // FIXME return pool shares once merged with acceptance branch - return sdk.Int{}, false -} - -func (k *Keeper) GetDepositorSharesInPool(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) { - // FIXME return depositor shares once merged with acceptance branch - return sdk.Int{}, false -} - // Implements SwapHooks interface var _ types.SwapHooks = Keeper{} -// FIXME call hooks within pool logic - // AfterPoolDepositCreated - call hook if registered func (k Keeper) AfterPoolDepositCreated(ctx sdk.Context, poolID string, depositor sdk.AccAddress, sharesOwned sdk.Int) { if k.hooks != nil { diff --git a/x/swap/keeper/keeper.go b/x/swap/keeper/keeper.go index e115f9ee..1db44f73 100644 --- a/x/swap/keeper/keeper.go +++ b/x/swap/keeper/keeper.go @@ -117,6 +117,15 @@ func (k Keeper) GetAllPools(ctx sdk.Context) (records types.PoolRecords) { return } +// GetPoolShares gets the total shares in a pool from the store +func (k Keeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Int, bool) { + pool, found := k.GetPool(ctx, poolID) + if !found { + return sdk.Int{}, false + } + return pool.TotalShares, true +} + // GetDepositorShares gets a share record from the store func (k Keeper) GetDepositorShares(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (types.ShareRecord, bool) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositorPoolSharesPrefix) @@ -187,3 +196,12 @@ func (k Keeper) GetAllDepositorSharesByOwner(ctx sdk.Context, owner sdk.AccAddre }) return } + +// GetDepositorSharesAmount gets a depositor's shares in a pool from the store +func (k *Keeper) GetDepositorSharesAmount(ctx sdk.Context, depositor sdk.AccAddress, poolID string) (sdk.Int, bool) { + record, found := k.GetDepositorShares(ctx, depositor, poolID) + if !found { + return sdk.Int{}, false + } + return record.SharesOwned, true +} diff --git a/x/swap/keeper/keeper_test.go b/x/swap/keeper/keeper_test.go index 90df6d7c..8e06d6fa 100644 --- a/x/swap/keeper/keeper_test.go +++ b/x/swap/keeper/keeper_test.go @@ -96,6 +96,10 @@ func (suite *keeperTestSuite) TestPool_Persistance() { suite.True(ok) suite.Equal(record, savedRecord) + savedShares, ok := suite.Keeper.GetPoolShares(suite.Ctx, record.PoolID) + suite.True(ok) + suite.Equal(record.TotalShares, savedShares) + suite.Keeper.DeletePool(suite.Ctx, record.PoolID) deletedPool, ok := suite.Keeper.GetPool(suite.Ctx, record.PoolID) suite.False(ok) @@ -114,6 +118,10 @@ func (suite *keeperTestSuite) TestShare_Persistance() { suite.True(ok) suite.Equal(record, savedRecord) + savedShares, ok := suite.Keeper.GetDepositorSharesAmount(suite.Ctx, depositor, poolID) + suite.True(ok) + suite.Equal(record.SharesOwned, savedShares) + suite.Keeper.DeleteDepositorShares(suite.Ctx, depositor, poolID) deletedShares, ok := suite.Keeper.GetDepositorShares(suite.Ctx, depositor, poolID) suite.False(ok) diff --git a/x/swap/keeper/withdraw.go b/x/swap/keeper/withdraw.go index 8bbe98ae..299e57f9 100644 --- a/x/swap/keeper/withdraw.go +++ b/x/swap/keeper/withdraw.go @@ -52,6 +52,8 @@ func (k Keeper) Withdraw(ctx sdk.Context, owner sdk.AccAddress, shares sdk.Int, } k.updatePool(ctx, poolID, pool) + + k.hooks.BeforePoolDepositModified(ctx, poolID, owner, shareRecord.SharesOwned) k.updateShares(ctx, owner, poolID, shareRecord.SharesOwned.Sub(shares)) err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, owner, withdrawnAmount)