From 63c8421a81841f3ac20439026363d43ea800236d Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Wed, 4 Aug 2021 15:55:29 +0100 Subject: [PATCH] Separate multipliers per claimed denom (#971) * move multipliers to their own file * add multipliers per denom to MsgClaimHardReward * claim with multipliers per denom for hard claims * remove unused error * add multipliers per denom to params connect to to hard claiming * temporary fix migration * update usdx claiming for new multiplier params * claim with multipliers per denom for delegator * claim with multipliers per denom for swap rewards * tidy up multiplier name validation * rename new multipliers field in params * remove old multpliers from params * clear up various TODOs * add tags to new structs * remove dead code --- migrate/v0_15/incentive.go | 16 +- x/incentive/alias.go | 9 +- x/incentive/client/cli/tx.go | 233 +++++++------- x/incentive/client/rest/rest.go | 18 +- x/incentive/client/rest/tx.go | 55 ++-- x/incentive/genesis_test.go | 50 ++- x/incentive/handler.go | 53 ++-- x/incentive/handler_delegator_test.go | 46 +-- x/incentive/handler_hard_test.go | 50 +-- x/incentive/handler_swap_test.go | 82 ++--- x/incentive/handler_usdx_test.go | 4 +- x/incentive/keeper/claim.go | 68 ++-- x/incentive/keeper/claim_test.go | 29 +- x/incentive/keeper/params.go | 12 +- x/incentive/keeper/payout_test.go | 14 +- x/incentive/keeper/rewards_supply_test.go | 1 - x/incentive/testutil/builder.go | 2 +- x/incentive/types/errors.go | 1 - x/incentive/types/genesis_test.go | 10 +- x/incentive/types/msg.go | 141 +++------ x/incentive/types/msg_test.go | 360 +++++++++++++++------- x/incentive/types/multipliers.go | 201 ++++++++++++ x/incentive/types/params.go | 114 ++----- x/incentive/types/params_test.go | 26 +- x/incentive/types/sdk.go | 26 -- x/incentive/types/sdk_test.go | 126 -------- 26 files changed, 974 insertions(+), 773 deletions(-) create mode 100644 x/incentive/types/multipliers.go diff --git a/migrate/v0_15/incentive.go b/migrate/v0_15/incentive.go index eb191c36..b501c485 100644 --- a/migrate/v0_15/incentive.go +++ b/migrate/v0_15/incentive.go @@ -15,6 +15,20 @@ func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisSt newMultiplier := v0_15incentive.NewMultiplier(v0_15incentive.MultiplierName(m.Name), m.MonthsLockup, m.Factor) claimMultipliers = append(claimMultipliers, newMultiplier) } + newMultipliers := v0_15incentive.MultipliersPerDenom{ + { + Denom: "hard", + Multipliers: claimMultipliers, + }, + { + Denom: "ukava", + Multipliers: claimMultipliers, + }, + { + Denom: "swp", + Multipliers: claimMultipliers, // TODO set the correct multipliers + }, + } usdxMintingRewardPeriods := v0_15incentive.RewardPeriods{} for _, rp := range incentiveGS.Params.USDXMintingRewardPeriods { @@ -41,7 +55,7 @@ func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisSt migrateMultiRewardPeriods(incentiveGS.Params.HardBorrowRewardPeriods), hardDelegatorRewardPeriods, swapRewardPeriods, - claimMultipliers, + newMultipliers, incentiveGS.Params.ClaimEnd, ) diff --git a/x/incentive/alias.go b/x/incentive/alias.go index 81716762..3284c1e0 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -52,9 +52,7 @@ var ( NewQuerier = keeper.NewQuerier DefaultGenesisState = types.DefaultGenesisState DefaultParams = types.DefaultParams - FilterCoins = types.FilterCoins GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength - MultiplyCoins = types.MultiplyCoins NewAccumulationTime = types.NewAccumulationTime NewAccumulator = types.NewAccumulator NewDelegatorClaim = types.NewDelegatorClaim @@ -79,9 +77,12 @@ var ( NewQueryRewardsParams = types.NewQueryRewardsParams NewRewardIndex = types.NewRewardIndex NewRewardPeriod = types.NewRewardPeriod + NewSelection = types.NewSelection + NewSelectionsFromMap = types.NewSelectionsFromMap NewSwapClaim = types.NewSwapClaim NewUSDXMintingClaim = types.NewUSDXMintingClaim ParamKeyTable = types.ParamKeyTable + ParseMultiplierName = types.ParseMultiplierName RegisterCodec = types.RegisterCodec // variable aliases @@ -105,7 +106,6 @@ var ( ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance ErrInvalidAccountType = types.ErrInvalidAccountType ErrInvalidClaimDenoms = types.ErrInvalidClaimDenoms - ErrInvalidClaimOwner = types.ErrInvalidClaimOwner ErrInvalidClaimType = types.ErrInvalidClaimType ErrInvalidMultiplier = types.ErrInvalidMultiplier ErrNoClaimsFound = types.ErrNoClaimsFound @@ -171,6 +171,7 @@ type ( Multiplier = types.Multiplier MultiplierName = types.MultiplierName Multipliers = types.Multipliers + MultipliersPerDenom = types.MultipliersPerDenom ParamSubspace = types.ParamSubspace Params = types.Params QueryGetRewardFactorsResponse = types.QueryGetRewardFactorsResponse @@ -179,6 +180,8 @@ type ( RewardIndexes = types.RewardIndexes RewardPeriod = types.RewardPeriod RewardPeriods = types.RewardPeriods + Selection = types.Selection + Selections = types.Selections StakingKeeper = types.StakingKeeper SupplyKeeper = types.SupplyKeeper SwapClaim = types.SwapClaim diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go index 211bb269..dd12b067 100644 --- a/x/incentive/client/cli/tx.go +++ b/x/incentive/client/cli/tx.go @@ -18,6 +18,9 @@ import ( "github.com/kava-labs/kava/x/incentive/types" ) +const multiplierFlag = "multiplier" +const multiplierFlagShort = "m" + // GetTxCmd returns the transaction cli commands for the incentive module func GetTxCmd(cdc *codec.Codec) *cobra.Command { incentiveTxCmd := &cobra.Command{ @@ -100,84 +103,118 @@ A receiver address for the rewards is needed as validator vesting accounts canno } func getCmdClaimHard(cdc *codec.Codec) *cobra.Command { - var denomsToClaim []string + var denomsToClaim map[string]string 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), + Use: "claim-hard", + Short: "claim sender's Hard module rewards using given multipliers", + Long: `Claim sender's outstanding Hard rewards for deposit/borrow using given multipliers`, + Example: strings.Join([]string{ + fmt.Sprintf(` $ %s tx %s claim-hard --%s hard=large --%s ukava=small`, version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(` $ %s tx %s claim-hard --%s hard=large,ukava=small`, version.ClientName, types.ModuleName, multiplierFlag), + }, "\n"), + Args: cobra.NoArgs, 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] + selections := types.NewSelectionsFromMap(denomsToClaim) - msg := types.NewMsgClaimHardReward(sender, multiplier, denomsToClaim) + msg := types.NewMsgClaimHardReward(sender, selections...) 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") - + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) return cmd } func getCmdClaimHardVVesting(cdc *codec.Codec) *cobra.Command { - var denomsToClaim []string + var denomsToClaim map[string]string 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.`, + Use: "claim-hard-vesting [receiver]", + Short: "claim Hard module rewards on behalf of a validator vesting account using given multipliers", + Long: `Claim sender's outstanding hard supply/borrow rewards on behalf of a validator vesting account using given multipliers +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens.`, 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), + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s hard=large --%s ukava=small", version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s hard=large,ukava=small", version.ClientName, types.ModuleName, multiplierFlag), }, "\n"), - Args: cobra.ExactArgs(2), + 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] - receiverStr := args[1] - receiver, err := sdk.AccAddressFromBech32(receiverStr) + receiver, err := sdk.AccAddressFromBech32(args[1]) if err != nil { return err } + selections := types.NewSelectionsFromMap(denomsToClaim) - msg := types.NewMsgClaimHardRewardVVesting(sender, receiver, multiplier, denomsToClaim) + msg := types.NewMsgClaimHardRewardVVesting(sender, receiver, selections...) 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") + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) return cmd } func getCmdClaimDelegator(cdc *codec.Codec) *cobra.Command { - var denomsToClaim []string + var denomsToClaim map[string]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.`, + Use: "claim-delegator", + Short: "claim sender's non-sdk delegator rewards using given multipliers", + Long: `Claim sender's outstanding delegator rewards using given multipliers`, 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), + fmt.Sprintf(` $ %s tx %s claim-delegator --%s hard=large --%s swp=small`, version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(` $ %s tx %s claim-delegator --%s hard=large,swp=small`, version.ClientName, types.ModuleName, multiplierFlag), + }, "\n"), + Args: cobra.NoArgs, + 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() + selections := types.NewSelectionsFromMap(denomsToClaim) + + msg := types.NewMsgClaimDelegatorReward(sender, selections...) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) + return cmd +} + +func getCmdClaimDelegatorVVesting(cdc *codec.Codec) *cobra.Command { + var denomsToClaim map[string]string + + cmd := &cobra.Command{ + Use: "claim-delegator-vesting [receiver]", + Short: "claim non-sdk delegator rewards on behalf of a validator vesting account using given multipliers", + Long: `Claim sender's outstanding delegator rewards on behalf of a validator vesting account using given multipliers +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s hard=large --%s swp=small", version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s hard=large,swp=small", version.ClientName, types.ModuleName, multiplierFlag), }, "\n"), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -186,70 +223,68 @@ Optionally claim only certain denoms from the rewards. Specifying none will clai 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) + receiver, err := sdk.AccAddressFromBech32(args[1]) if err != nil { return err } + selections := types.NewSelectionsFromMap(denomsToClaim) - msg := types.NewMsgClaimDelegatorRewardVVesting(sender, receiver, multiplier, denomsToClaim) + msg := types.NewMsgClaimDelegatorRewardVVesting(sender, receiver, selections...) 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") + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) return cmd } func getCmdClaimSwap(cdc *codec.Codec) *cobra.Command { - var denomsToClaim []string + var denomsToClaim map[string]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.`, + Use: "claim-swap", + Short: "claim sender's swap rewards using given multipliers", + Long: `Claim sender's outstanding swap rewards using given multipliers`, 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), + fmt.Sprintf(` $ %s tx %s claim-hard --%s swp=large --%s ukava=small`, version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(` $ %s tx %s claim-hard --%s swp=large,ukava=small`, version.ClientName, types.ModuleName, multiplierFlag), + }, "\n"), + Args: cobra.NoArgs, + 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() + selections := types.NewSelectionsFromMap(denomsToClaim) + + msg := types.NewMsgClaimSwapReward(sender, selections...) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) + return cmd +} + +func getCmdClaimSwapVVesting(cdc *codec.Codec) *cobra.Command { + var denomsToClaim map[string]string + + cmd := &cobra.Command{ + Use: "claim-swap-vesting [receiver]", + Short: "claim swap rewards on behalf of a validator vesting account using given multipliers", + Long: `Claim sender's outstanding swap rewards on behalf of a validator vesting account using given multipliers +A receiver address for the rewards is needed as validator vesting accounts cannot receive locked tokens.`, + Example: strings.Join([]string{ + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s ukava=large --%s swp=small", version.ClientName, types.ModuleName, multiplierFlag, multiplierFlag), + fmt.Sprintf(" $ %s tx %s claim-hard-vesting kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw --%s ukava=large,swp=small", version.ClientName, types.ModuleName, multiplierFlag), }, "\n"), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -258,55 +293,21 @@ Optionally claim only certain denoms from the rewards. Specifying none will clai 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) + receiver, err := sdk.AccAddressFromBech32(args[1]) if err != nil { return err } + selections := types.NewSelectionsFromMap(denomsToClaim) - msg := types.NewMsgClaimSwapRewardVVesting(sender, receiver, multiplier, denomsToClaim) + msg := types.NewMsgClaimSwapRewardVVesting(sender, receiver, selections...) 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") + cmd.Flags().StringToStringVarP(&denomsToClaim, multiplierFlag, multiplierFlagShort, nil, "specify the denoms to claim, each with a multiplier lockup") + cmd.MarkFlagRequired(multiplierFlag) return cmd } diff --git a/x/incentive/client/rest/rest.go b/x/incentive/client/rest/rest.go index 35fa2d04..1aaa0a98 100644 --- a/x/incentive/client/rest/rest.go +++ b/x/incentive/client/rest/rest.go @@ -6,6 +6,8 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" + + "github.com/kava-labs/kava/x/incentive/types" ) // REST variable names @@ -22,17 +24,15 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { // PostClaimReq defines the properties of claim transaction's request body. 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"` + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + DenomsToClaim types.Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // PostClaimReq defines the properties of claim transaction's request body. type PostClaimVVestingReq struct { - BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` - 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"` + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + DenomsToClaim types.Selections `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 ee00b276..7efd6bf4 100644 --- a/x/incentive/client/rest/tx.go +++ b/x/incentive/client/rest/tx.go @@ -29,20 +29,23 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc("/incentive/claim-swap-vesting", postClaimVVestingHandlerFn(cliCtx, swapVVGenerator)).Methods("POST") } -func usdxMintingGenerator(req PostClaimReq) sdk.Msg { - return types.NewMsgClaimUSDXMintingReward(req.Sender, req.MultiplierName) +func usdxMintingGenerator(req PostClaimReq) (sdk.Msg, error) { + if len(req.DenomsToClaim) != 1 { + return nil, fmt.Errorf("must only claim %s denom for usdx minting rewards, got '%s", types.USDXMintingRewardDenom, req.DenomsToClaim) + } + return types.NewMsgClaimUSDXMintingReward(req.Sender, req.DenomsToClaim[0].MultiplierName), nil } -func hardGenerator(req PostClaimReq) sdk.Msg { - return types.NewMsgClaimHardReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +func hardGenerator(req PostClaimReq) (sdk.Msg, error) { + return types.NewMsgClaimHardReward(req.Sender, req.DenomsToClaim...), nil } -func delegatorGenerator(req PostClaimReq) sdk.Msg { - return types.NewMsgClaimDelegatorReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +func delegatorGenerator(req PostClaimReq) (sdk.Msg, error) { + return types.NewMsgClaimDelegatorReward(req.Sender, req.DenomsToClaim...), nil } -func swapGenerator(req PostClaimReq) sdk.Msg { - return types.NewMsgClaimSwapReward(req.Sender, req.MultiplierName, req.DenomsToClaim) +func swapGenerator(req PostClaimReq) (sdk.Msg, error) { + return types.NewMsgClaimSwapReward(req.Sender, req.DenomsToClaim...), nil } -func postClaimHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimReq) sdk.Msg) http.HandlerFunc { +func postClaimHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimReq) (sdk.Msg, error)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody PostClaimReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { @@ -65,7 +68,12 @@ func postClaimHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostCla return } - msg := msgGenerator(requestBody) + msg, err := msgGenerator(requestBody) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return @@ -75,20 +83,23 @@ func postClaimHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostCla } } -func usdxMintingVVGenerator(req PostClaimVVestingReq) sdk.Msg { - return types.NewMsgClaimUSDXMintingRewardVVesting(req.Sender, req.Receiver, req.MultiplierName) +func usdxMintingVVGenerator(req PostClaimVVestingReq) (sdk.Msg, error) { + if len(req.DenomsToClaim) != 1 { + return nil, fmt.Errorf("must only claim %s denom for usdx minting rewards, got '%s", types.USDXMintingRewardDenom, req.DenomsToClaim) + } + return types.NewMsgClaimUSDXMintingRewardVVesting(req.Sender, req.Receiver, req.DenomsToClaim[0].MultiplierName), nil } -func hardVVGenerator(req PostClaimVVestingReq) sdk.Msg { - return types.NewMsgClaimHardRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +func hardVVGenerator(req PostClaimVVestingReq) (sdk.Msg, error) { + return types.NewMsgClaimHardRewardVVesting(req.Sender, req.Receiver, req.DenomsToClaim...), nil } -func delegatorVVGenerator(req PostClaimVVestingReq) sdk.Msg { - return types.NewMsgClaimDelegatorRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +func delegatorVVGenerator(req PostClaimVVestingReq) (sdk.Msg, error) { + return types.NewMsgClaimDelegatorRewardVVesting(req.Sender, req.Receiver, req.DenomsToClaim...), nil } -func swapVVGenerator(req PostClaimVVestingReq) sdk.Msg { - return types.NewMsgClaimSwapRewardVVesting(req.Sender, req.Receiver, req.MultiplierName, req.DenomsToClaim) +func swapVVGenerator(req PostClaimVVestingReq) (sdk.Msg, error) { + return types.NewMsgClaimSwapRewardVVesting(req.Sender, req.Receiver, req.DenomsToClaim...), nil } -func postClaimVVestingHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimVVestingReq) sdk.Msg) http.HandlerFunc { +func postClaimVVestingHandlerFn(cliCtx context.CLIContext, msgGenerator func(req PostClaimVVestingReq) (sdk.Msg, error)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody PostClaimVVestingReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { @@ -111,7 +122,11 @@ func postClaimVVestingHandlerFn(cliCtx context.CLIContext, msgGenerator func(req return } - msg := msgGenerator(requestBody) + msg, err := msgGenerator(requestBody) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } 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 e2036495..902cdd9e 100644 --- a/x/incentive/genesis_test.go +++ b/x/incentive/genesis_test.go @@ -65,8 +65,29 @@ func (suite *GenesisTestSuite) SetupTest() { incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, - incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, d("0.25")), incentive.NewMultiplier(incentive.Large, 12, d("1.0"))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("swp", 122354)))}, + incentive.MultipliersPerDenom{ + { + Denom: "ukava", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Large, 12, d("1.0")), + }, + }, + { + Denom: "hard", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Small, 1, d("0.25")), + incentive.NewMultiplier(incentive.Large, 12, d("1.0")), + }, + }, + { + Denom: "swp", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Small, 1, d("0.25")), + incentive.NewMultiplier(incentive.Medium, 6, d("0.8")), + }, + }, + }, suite.genesisTime.Add(5*oneYear), ), incentive.DefaultGenesisRewardState, @@ -104,8 +125,29 @@ func (suite *GenesisTestSuite) TestExportedGenesisMatchesImported() { incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", genesisTime.Add(-1*oneYear), genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", genesisTime.Add(-1*oneYear), genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", genesisTime.Add(-1*oneYear), genesisTime.Add(oneYear), cs(c("hard", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", genesisTime.Add(-1*oneYear), genesisTime.Add(oneYear), cs(c("swap", 122354)))}, - incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, d("0.25")), incentive.NewMultiplier(incentive.Large, 12, d("1.0"))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", genesisTime.Add(-1*oneYear), genesisTime.Add(oneYear), cs(c("swp", 122354)))}, + incentive.MultipliersPerDenom{ + { + Denom: "ukava", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Large, 12, d("1.0")), + }, + }, + { + Denom: "hard", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Small, 1, d("0.25")), + incentive.NewMultiplier(incentive.Large, 12, d("1.0")), + }, + }, + { + Denom: "swp", + Multipliers: incentive.Multipliers{ + incentive.NewMultiplier(incentive.Small, 1, d("0.25")), + incentive.NewMultiplier(incentive.Medium, 6, d("0.8")), + }, + }, + }, genesisTime.Add(5*oneYear), ), incentive.NewGenesisRewardState( diff --git a/x/incentive/handler.go b/x/incentive/handler.go index be44db0e..89ce7c3b 100644 --- a/x/incentive/handler.go +++ b/x/incentive/handler.go @@ -36,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, msg.Sender, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimUSDXMintingReward(ctx, msg.Sender, msg.Sender, msg.MultiplierName) if err != nil { return nil, err } @@ -50,7 +50,7 @@ func handleMsgClaimUSDXMintingRewardVVesting(ctx sdk.Context, k keeper.Keeper, m if err := k.ValidateIsValidatorVestingAccount(ctx, msg.Sender); err != nil { return nil, err } - err := k.ClaimUSDXMintingReward(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimUSDXMintingReward(ctx, msg.Sender, msg.Receiver, msg.MultiplierName) if err != nil { return nil, err } @@ -60,10 +60,12 @@ func handleMsgClaimUSDXMintingRewardVVesting(ctx sdk.Context, k keeper.Keeper, m } func handleMsgClaimHardReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardReward) (*sdk.Result, error) { + for _, selection := range msg.DenomsToClaim { + err := k.ClaimHardReward(ctx, msg.Sender, msg.Sender, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } - err := k.ClaimHardReward(ctx, msg.Sender, msg.Sender, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) - if err != nil { - return nil, err } return &sdk.Result{ Events: ctx.EventManager().Events(), @@ -75,9 +77,12 @@ func handleMsgClaimHardRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg type 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 + for _, selection := range msg.DenomsToClaim { + err := k.ClaimHardReward(ctx, msg.Sender, msg.Receiver, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } + } return &sdk.Result{ Events: ctx.EventManager().Events(), @@ -86,9 +91,11 @@ 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, msg.Sender, types.MultiplierName(msg.MultiplierName), msg.DenomsToClaim) - if err != nil { - return nil, err + for _, selection := range msg.DenomsToClaim { + err := k.ClaimDelegatorReward(ctx, msg.Sender, msg.Sender, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } } return &sdk.Result{ Events: ctx.EventManager().Events(), @@ -100,9 +107,11 @@ func handleMsgClaimDelegatorRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg 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 + for _, selection := range msg.DenomsToClaim { + err := k.ClaimDelegatorReward(ctx, msg.Sender, msg.Receiver, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } } return &sdk.Result{ Events: ctx.EventManager().Events(), @@ -111,9 +120,11 @@ func handleMsgClaimDelegatorRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg 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 + for _, selection := range msg.DenomsToClaim { + err := k.ClaimSwapReward(ctx, msg.Sender, msg.Sender, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } } return &sdk.Result{ Events: ctx.EventManager().Events(), @@ -125,9 +136,11 @@ func handleMsgClaimSwapRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg type 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 + for _, selection := range msg.DenomsToClaim { + err := k.ClaimSwapReward(ctx, msg.Sender, msg.Receiver, selection.Denom, selection.MultiplierName) + if err != nil { + return nil, err + } } return &sdk.Result{ Events: ctx.EventManager().Events(), diff --git a/x/incentive/handler_delegator_test.go b/x/incentive/handler_delegator_test.go index 2929c9ab..32424295 100644 --- a/x/incentive/handler_delegator_test.go +++ b/x/incentive/handler_delegator_test.go @@ -9,7 +9,7 @@ import ( "github.com/kava-labs/kava/x/incentive/types" ) -func (suite *HandlerTestSuite) TestPayoutDelegatorClaim() { +func (suite *HandlerTestSuite) TestPayoutDelegatorClaimMultiDenom() { userAddr := suite.addrs[0] receiverAddr := suite.addrs[1] @@ -18,7 +18,7 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaim() { WithSimpleAccount(receiverAddr, nil) incentBuilder := suite.incentiveBuilder(). - WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6))) + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6), c("swap", 1e6))) suite.SetupWithGenState(authBulder, incentBuilder) @@ -35,22 +35,24 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaim() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorRewardVVesting(userAddr, receiverAddr, "large", nil), + types.NewMsgClaimDelegatorRewardVVesting(userAddr, receiverAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) - // Claim a single denom + // Claim denoms err = suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorReward(userAddr, "large", nil), + types.NewMsgClaimDelegatorReward(userAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := cs(c("hard", 2*7*1e6)) - suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(2*7*1e6))) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004786, Amount: expectedRewards}, + {Length: (17+31)*secondsPerDay - 2*7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that claimed coins have been removed from a claim's reward suite.DelegatorRewardEquals(userAddr, nil) @@ -80,13 +82,13 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimSingleDenom() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorRewardVVesting(userAddr, suite.addrs[1], "large", nil), + types.NewMsgClaimDelegatorRewardVVesting(userAddr, suite.addrs[1], types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorReward(userAddr, "large", []string{"swap"}), + types.NewMsgClaimDelegatorReward(userAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -95,14 +97,14 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimSingleDenom() { suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004786, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 2*7, 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() { +func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVestingMultiDenom() { valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12))) @@ -112,7 +114,7 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVesting() { WithSimpleAccount(receiverAddr, nil) incentBuilder := suite.incentiveBuilder(). - WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6))) + WithSimpleDelegatorRewardPeriod(types.BondDenom, cs(c("hard", 1e6), c("swap", 1e6))) suite.SetupWithGenState(authBulder, incentBuilder) @@ -129,22 +131,24 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVesting() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorReward(valAddr, "large", nil), + types.NewMsgClaimDelegatorReward(valAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, "large", nil), + types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := c("hard", 2*7*1e6) - suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(2*7*1e6))) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004786, Amount: cs(expectedRewards)}, + {Length: (17+31)*secondsPerDay - 2*7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that each claim reward coin's amount has been reset to 0 @@ -178,13 +182,13 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVestingSingleDenom() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorReward(valAddr, "large", []string{"swap"}), + types.NewMsgClaimDelegatorReward(valAddr, types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + types.NewMsgClaimDelegatorRewardVVesting(valAddr, receiverAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -193,7 +197,7 @@ func (suite *HandlerTestSuite) TestPayoutDelegatorClaimVVestingSingleDenom() { suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004786, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 2*7, Amount: cs(expectedRewards)}, }) // Check that claimed coins have been removed from a claim's reward diff --git a/x/incentive/handler_hard_test.go b/x/incentive/handler_hard_test.go index a431b810..95dba70b 100644 --- a/x/incentive/handler_hard_test.go +++ b/x/incentive/handler_hard_test.go @@ -8,7 +8,7 @@ import ( "github.com/kava-labs/kava/x/incentive/types" ) -func (suite *HandlerTestSuite) TestPayoutHardClaim() { +func (suite *HandlerTestSuite) TestPayoutHardClaimMultiDenom() { userAddr, receiverAddr := suite.addrs[0], suite.addrs[1] authBulder := suite.authBuilder(). @@ -16,8 +16,8 @@ func (suite *HandlerTestSuite) TestPayoutHardClaim() { WithSimpleAccount(receiverAddr, nil) incentBuilder := suite.incentiveBuilder(). - WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6))). - WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6))) + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))) suite.SetupWithGenState(authBulder, incentBuilder) @@ -32,22 +32,24 @@ func (suite *HandlerTestSuite) TestPayoutHardClaim() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimHardRewardVVesting(userAddr, receiverAddr, "large", nil), + types.NewMsgClaimHardRewardVVesting(userAddr, receiverAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) - // Claim a single denom + // Claim denoms err = suite.DeliverIncentiveMsg( - types.NewMsgClaimHardReward(userAddr, "large", nil), + types.NewMsgClaimHardReward(userAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := cs(c("hard", 2*7*1e6)) - suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(2*7*1e6))) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004793, Amount: expectedRewards}, + {Length: (17+31)*secondsPerDay - 7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that claimed coins have been removed from a claim's reward suite.HardRewardEquals(userAddr, nil) @@ -76,13 +78,13 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimSingleDenom() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimHardRewardVVesting(userAddr, suite.addrs[1], "large", []string{"swap"}), + types.NewMsgClaimHardRewardVVesting(userAddr, suite.addrs[1], types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimHardReward(userAddr, "large", []string{"swap"}), + types.NewMsgClaimHardReward(userAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -91,14 +93,14 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimSingleDenom() { suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, 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() { +func (suite *HandlerTestSuite) TestPayoutHardClaimVVestingMultiDenom() { valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("bnb", 1e12))) @@ -108,8 +110,8 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimVVesting() { WithSimpleAccount(receiverAddr, nil) incentBuilder := suite.incentiveBuilder(). - WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6))). - WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6))) + WithSimpleSupplyRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))). + WithSimpleBorrowRewardPeriod("bnb", cs(c("hard", 1e6), c("swap", 1e6))) suite.SetupWithGenState(authBulder, incentBuilder) @@ -124,22 +126,24 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimVVesting() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimHardReward(valAddr, "large", nil), + types.NewMsgClaimHardReward(valAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, "large", nil), + types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := c("hard", 2*7*1e6) - suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) + expectedRewardsHard := c("hard", int64(0.2*float64(2*7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(2*7*1e6))) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31)*secondsPerDay - 7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that each claim reward coin's amount has been reset to 0 @@ -172,13 +176,13 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimVVestingSingleDenom() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimHardReward(valAddr, "large", []string{"swap"}), + types.NewMsgClaimHardReward(valAddr, types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + types.NewMsgClaimHardRewardVVesting(valAddr, receiverAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -187,7 +191,7 @@ func (suite *HandlerTestSuite) TestPayoutHardClaimVVestingSingleDenom() { suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, Amount: cs(expectedRewards)}, }) // Check that claimed coins have been removed from a claim's reward diff --git a/x/incentive/handler_swap_test.go b/x/incentive/handler_swap_test.go index 07df122b..ceb2417a 100644 --- a/x/incentive/handler_swap_test.go +++ b/x/incentive/handler_swap_test.go @@ -11,26 +11,18 @@ import ( 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" ) +const secondsPerDay = 24 * 60 * 60 + // 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 } @@ -52,11 +44,6 @@ func (suite *HandlerTestSuite) SetupTest() { 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}) } @@ -106,13 +93,32 @@ func (suite *HandlerTestSuite) authBuilder() app.AuthGenesisBuilder { 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")), + WithMultipliers(types.MultipliersPerDenom{ + { + Denom: "hard", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.MultiplierName("small"), 1, d("0.2")), + types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0")), + }, + }, + { + Denom: "swap", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.MultiplierName("medium"), 6, d("0.5")), + types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0")), + }, + }, + { + Denom: "ukava", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.MultiplierName("small"), 1, d("0.2")), + types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0")), + }, + }, }) } -func (suite *HandlerTestSuite) TestPayoutSwapClaim() { +func (suite *HandlerTestSuite) TestPayoutSwapClaimMultiDenom() { userAddr := suite.addrs[0] authBulder := suite.authBuilder(). @@ -134,22 +140,24 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaim() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], "large", nil), + types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapReward(userAddr, "large", nil), + types.NewMsgClaimSwapReward(userAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := cs(c("swap", 7*1e6), c("hard", 7*1e6)) - suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) + expectedRewardsHard := c("hard", int64(0.2*float64(7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(7*1e6))) + suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004793, Amount: expectedRewards}, + {Length: (17+31)*secondsPerDay - 7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that each claim reward coin's amount has been reset to 0 @@ -179,13 +187,13 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimSingleDenom() { // Check rewards cannot be claimed by vvesting claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], "large", nil), + types.NewMsgClaimSwapRewardVVesting(userAddr, suite.addrs[1], types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapReward(userAddr, "large", []string{"swap"}), + types.NewMsgClaimSwapReward(userAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -194,14 +202,14 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimSingleDenom() { suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, 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() { +func (suite *HandlerTestSuite) TestPayoutSwapClaimVVestingMultiDenom() { valAddr, receiverAddr := suite.addrs[0], suite.addrs[1] vva := suite.NewValidatorVestingAccountWithBalance(valAddr, cs(c("ukava", 1e12), c("busd", 1e12))) @@ -227,22 +235,24 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimVVesting() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapReward(valAddr, "large", nil), + types.NewMsgClaimSwapReward(valAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, "large", nil), + types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, types.NewSelection("hard", "small"), types.NewSelection("swap", "medium")), ) suite.NoError(err) // Check rewards were paid out - expectedRewards := cs(c("hard", 7*1e6), c("swap", 7*1e6)) - suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards...)) + expectedRewardsHard := c("hard", int64(0.2*float64(7*1e6))) + expectedRewardsSwap := c("swap", int64(0.5*float64(7*1e6))) + suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewardsHard, expectedRewardsSwap)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004793, Amount: expectedRewards}, + {Length: (17+31)*secondsPerDay - 7, Amount: cs(expectedRewardsHard)}, + {Length: (28 + 31 + 30 + 31 + 30) * secondsPerDay, Amount: cs(expectedRewardsSwap)}, // second length is stacked on top of the first }) // Check that each claim reward coin's amount has been reset to 0 @@ -275,13 +285,13 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimVVestingSingleDenom() { // Check rewards cannot be claimed by normal claim msgs err := suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapReward(valAddr, "large", []string{"swap"}), + types.NewMsgClaimSwapReward(valAddr, types.NewSelection("swap", "large")), ) suite.ErrorIs(err, types.ErrInvalidAccountType) // Claim rewards err = suite.DeliverIncentiveMsg( - types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, "large", []string{"swap"}), + types.NewMsgClaimSwapRewardVVesting(valAddr, receiverAddr, types.NewSelection("swap", "large")), ) suite.NoError(err) @@ -290,7 +300,7 @@ func (suite *HandlerTestSuite) TestPayoutSwapClaimVVestingSingleDenom() { suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, Amount: cs(expectedRewards)}, }) // Check that claimed coins have been removed from a claim's reward diff --git a/x/incentive/handler_usdx_test.go b/x/incentive/handler_usdx_test.go index 9edc8b75..9f8121c8 100644 --- a/x/incentive/handler_usdx_test.go +++ b/x/incentive/handler_usdx_test.go @@ -44,7 +44,7 @@ func (suite *HandlerTestSuite) TestPayoutUSDXClaim() { suite.BalanceEquals(userAddr, preClaimBal.Add(expectedRewards...)) suite.VestingPeriodsEqual(userAddr, vesting.Periods{ - {Length: 33004793, Amount: expectedRewards}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, Amount: expectedRewards}, }) // Check that claimed coins have been removed from a claim's reward suite.USDXRewardEquals(userAddr, c(types.USDXMintingRewardDenom, 0)) @@ -88,7 +88,7 @@ func (suite *HandlerTestSuite) TestPayoutUSDXClaimVVesting() { suite.BalanceEquals(receiverAddr, preClaimBal.Add(expectedRewards)) suite.VestingPeriodsEqual(receiverAddr, vesting.Periods{ - {Length: 33004793, Amount: cs(expectedRewards)}, + {Length: (17+31+28+31+30+31+30+31+31+30+31+30+31)*secondsPerDay - 7, Amount: cs(expectedRewards)}, }) // Check that each claim reward coin's amount has been reset to 0 diff --git a/x/incentive/keeper/claim.go b/x/incentive/keeper/claim.go index 85a9612c..0484b959 100644 --- a/x/incentive/keeper/claim.go +++ b/x/incentive/keeper/claim.go @@ -10,15 +10,20 @@ import ( // 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 { +func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName string) error { claim, found := k.GetUSDXMintingClaim(ctx, owner) if !found { return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) } - multiplier, found := k.GetMultiplier(ctx, multiplierName) + name, err := types.ParseMultiplierName(multiplierName) + if err != nil { + return err + } + + multiplier, found := k.GetMultiplierByDenom(ctx, types.USDXMintingRewardDenom, name) if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, "denom '%s' has no multiplier '%s'", types.USDXMintingRewardDenom, name) } claimEnd := k.GetClaimEnd(ctx) @@ -27,7 +32,7 @@ func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, owner, receiver sdk.AccA return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) } - claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) + claim, err = k.SynchronizeUSDXMintingClaim(ctx, claim) if err != nil { return err } @@ -62,15 +67,15 @@ func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, owner, receiver sdk.AccA // 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) +func (k Keeper) ClaimHardReward(ctx sdk.Context, owner, receiver sdk.AccAddress, denom string, multiplierName string) error { + name, err := types.ParseMultiplierName(multiplierName) + if err != nil { + return err } - multiplier, found := k.GetMultiplier(ctx, multiplierName) + multiplier, found := k.GetMultiplierByDenom(ctx, denom, name) if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, "denom '%s' has no multiplier '%s'", denom, name) } claimEnd := k.GetClaimEnd(ctx) @@ -86,8 +91,10 @@ func (k Keeper) ClaimHardReward(ctx sdk.Context, owner, receiver sdk.AccAddress, return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) } - claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) - rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + amt := syncedClaim.Reward.AmountOf(denom) + + claimingCoins := sdk.NewCoins(sdk.NewCoin(denom, amt)) + rewardCoins := sdk.NewCoins(sdk.NewCoin(denom, amt.ToDec().Mul(multiplier.Factor).RoundInt())) if rewardCoins.IsZero() { return types.ErrZeroClaim } @@ -118,15 +125,20 @@ func (k Keeper) ClaimHardReward(ctx sdk.Context, owner, receiver sdk.AccAddress, // 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 { +func (k Keeper) ClaimDelegatorReward(ctx sdk.Context, owner, receiver sdk.AccAddress, denom string, multiplierName string) error { claim, found := k.GetDelegatorClaim(ctx, owner) if !found { return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) } - multiplier, found := k.GetMultiplier(ctx, multiplierName) + name, err := types.ParseMultiplierName(multiplierName) + if err != nil { + return err + } + + multiplier, found := k.GetMultiplierByDenom(ctx, denom, name) if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, "denom '%s' has no multiplier '%s'", denom, name) } claimEnd := k.GetClaimEnd(ctx) @@ -140,8 +152,10 @@ func (k Keeper) ClaimDelegatorReward(ctx sdk.Context, owner, receiver sdk.AccAdd return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) } - claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) - rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + amt := syncedClaim.Reward.AmountOf(denom) + + claimingCoins := sdk.NewCoins(sdk.NewCoin(denom, amt)) + rewardCoins := sdk.NewCoins(sdk.NewCoin(denom, amt.ToDec().Mul(multiplier.Factor).RoundInt())) if rewardCoins.IsZero() { return types.ErrZeroClaim } @@ -173,15 +187,14 @@ func (k Keeper) ClaimDelegatorReward(ctx sdk.Context, owner, receiver sdk.AccAdd // 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) +func (k Keeper) ClaimSwapReward(ctx sdk.Context, owner, receiver sdk.AccAddress, denom string, multiplierName string) error { + name, err := types.ParseMultiplierName(multiplierName) + if err != nil { + return err } - - multiplier, found := k.GetMultiplier(ctx, multiplierName) + multiplier, found := k.GetMultiplierByDenom(ctx, denom, name) if !found { - return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, "denom '%s' has no multiplier '%s'", denom, name) } claimEnd := k.GetClaimEnd(ctx) @@ -195,8 +208,10 @@ func (k Keeper) ClaimSwapReward(ctx sdk.Context, owner, receiver sdk.AccAddress, return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) } - claimingCoins := types.FilterCoins(syncedClaim.Reward, denomsToClaim) - rewardCoins := types.MultiplyCoins(claimingCoins, multiplier.Factor) + amt := syncedClaim.Reward.AmountOf(denom) + + claimingCoins := sdk.NewCoins(sdk.NewCoin(denom, amt)) + rewardCoins := sdk.NewCoins(sdk.NewCoin(denom, amt.ToDec().Mul(multiplier.Factor).RoundInt())) if rewardCoins.IsZero() { return types.ErrZeroClaim } @@ -236,4 +251,3 @@ func (k Keeper) ValidateIsValidatorVestingAccount(ctx sdk.Context, address sdk.A } return nil } - diff --git a/x/incentive/keeper/claim_test.go b/x/incentive/keeper/claim_test.go index 6aafba18..c6c26172 100644 --- a/x/incentive/keeper/claim_test.go +++ b/x/incentive/keeper/claim_test.go @@ -26,8 +26,13 @@ func (suite *ClaimTests) ErrorIs(err, target error) bool { func (suite *ClaimTests) TestCannotClaimWhenMultiplierNotRecognised() { subspace := &fakeParamSubspace{ params: types.Params{ - ClaimMultipliers: types.Multipliers{ - types.NewMultiplier(types.Small, 1, d("0.2")), + ClaimMultipliers: types.MultipliersPerDenom{ + { + Denom: "hard", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, d("0.2")), + }, + }, }, }, } @@ -41,16 +46,11 @@ func (suite *ClaimTests) TestCannotClaimWhenMultiplierNotRecognised() { suite.storeDelegatorClaim(claim) // multiplier not in params - err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, types.Large, nil) + err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, "hard", "large") 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) + err = suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, "hard", "") suite.ErrorIs(err, types.ErrInvalidMultiplier) } @@ -59,8 +59,13 @@ func (suite *ClaimTests) TestCannotClaimAfterEndTime() { subspace := &fakeParamSubspace{ params: types.Params{ - ClaimMultipliers: types.Multipliers{ - types.NewMultiplier(types.Small, 1, d("0.2")), + ClaimMultipliers: types.MultipliersPerDenom{ + { + Denom: "hard", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, d("0.2")), + }, + }, }, ClaimEnd: endTime, }, @@ -76,6 +81,6 @@ func (suite *ClaimTests) TestCannotClaimAfterEndTime() { } suite.storeDelegatorClaim(claim) - err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, types.Small, nil) + err := suite.keeper.ClaimDelegatorReward(suite.ctx, claim.Owner, claim.Owner, "hard", "small") suite.ErrorIs(err, types.ErrClaimExpired) } diff --git a/x/incentive/keeper/params.go b/x/incentive/keeper/params.go index 587f1d5d..d21e91eb 100644 --- a/x/incentive/keeper/params.go +++ b/x/incentive/keeper/params.go @@ -64,12 +64,14 @@ func (k Keeper) GetDelegatorRewardPeriods(ctx sdk.Context, denom string) (types. return types.MultiRewardPeriod{}, false } -// GetMultiplier returns the multiplier with the specified name if it's found in the params -func (k Keeper) GetMultiplier(ctx sdk.Context, name types.MultiplierName) (types.Multiplier, bool) { +// GetMultiplierByDenom fetches a multiplier from the params matching the denom and name. +func (k Keeper) GetMultiplierByDenom(ctx sdk.Context, denom string, name types.MultiplierName) (types.Multiplier, bool) { params := k.GetParams(ctx) - for _, m := range params.ClaimMultipliers { - if m.Name == name { - return m, true + + for _, dm := range params.ClaimMultipliers { + if dm.Denom == denom { + m, found := dm.Multipliers.Get(name) + return m, found } } return types.Multiplier{}, false diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index 9ea0c6cd..187513fa 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -502,7 +502,7 @@ func (suite *PayoutTestSuite) TestGetPeriodLength() { }, }, { - name: "exactly half of month", + name: "exactly half of month, is pushed to start of month + lockup", args: args{ blockTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")), @@ -525,6 +525,18 @@ func (suite *PayoutTestSuite) TestGetPeriodLength() { contains: "", }, }, + { + name: "just after start of month payout time, is pushed to mid month + lockup", + args: args{ + blockTime: time.Date(2020, 12, 1, 14, 0, 1, 0, time.UTC), + multiplier: types.NewMultiplier(types.Medium, 1, sdk.MustNewDecFromStr("0.333333")), + expectedLength: time.Date(2021, 1, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 1, 14, 0, 1, 0, time.UTC).Unix(), + }, + errArgs: errArgs{ + expectPass: true, + contains: "", + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { diff --git a/x/incentive/keeper/rewards_supply_test.go b/x/incentive/keeper/rewards_supply_test.go index 4380daf2..eb527775 100644 --- a/x/incentive/keeper/rewards_supply_test.go +++ b/x/incentive/keeper/rewards_supply_test.go @@ -222,7 +222,6 @@ func (suite *SupplyRewardsTestSuite) TestAccumulateHardSupplyRewards() { }, }, }, - // TODO test accumulate when there is a reward period with 0 rewardsPerSecond } for _, tc := range testCases { suite.Run(tc.name, func() { diff --git a/x/incentive/testutil/builder.go b/x/incentive/testutil/builder.go index a77e7b48..ba79ce7c 100644 --- a/x/incentive/testutil/builder.go +++ b/x/incentive/testutil/builder.go @@ -166,7 +166,7 @@ func (builder IncentiveGenesisBuilder) WithSimpleUSDXRewardPeriod(ctype string, )) } -func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.Multipliers) IncentiveGenesisBuilder { +func (builder IncentiveGenesisBuilder) WithMultipliers(multipliers types.MultipliersPerDenom) IncentiveGenesisBuilder { builder.Params.ClaimMultipliers = multipliers return builder } diff --git a/x/incentive/types/errors.go b/x/incentive/types/errors.go index 0eeeb6f1..d265a87c 100644 --- a/x/incentive/types/errors.go +++ b/x/incentive/types/errors.go @@ -18,7 +18,6 @@ var ( ErrZeroClaim = sdkerrors.Register(ModuleName, 9, "cannot claim - claim amount rounds to zero") ErrClaimExpired = sdkerrors.Register(ModuleName, 10, "claim has expired") 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/genesis_test.go b/x/incentive/types/genesis_test.go index ff393a7b..7124b1e6 100644 --- a/x/incentive/types/genesis_test.go +++ b/x/incentive/types/genesis_test.go @@ -45,8 +45,14 @@ func TestGenesisState_Validate(t *testing.T) { DefaultMultiRewardPeriods, DefaultMultiRewardPeriods, DefaultMultiRewardPeriods, - Multipliers{ - NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33")), + MultipliersPerDenom{ + { + Denom: "ukava", + Multipliers: Multipliers{ + NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33")), + NewMultiplier(Large, 12, sdk.MustNewDecFromStr("1.00")), + }, + }, }, time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), ), diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go index 4c5e2541..64df59bc 100644 --- a/x/incentive/types/msg.go +++ b/x/incentive/types/msg.go @@ -110,17 +110,15 @@ func (msg MsgClaimUSDXMintingRewardVVesting) GetSigners() []sdk.AccAddress { // MsgClaimHardReward message type used to claim Hard liquidity provider rewards 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"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimHardReward returns a new MsgClaimHardReward. -func NewMsgClaimHardReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimHardReward { +func NewMsgClaimHardReward(sender sdk.AccAddress, denomsToClaim ...Selection) MsgClaimHardReward { return MsgClaimHardReward{ - Sender: sender, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + DenomsToClaim: denomsToClaim, } } @@ -137,17 +135,9 @@ func (msg MsgClaimHardReward) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } - if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + if err := msg.DenomsToClaim.Validate(); 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 } @@ -164,19 +154,17 @@ func (msg MsgClaimHardReward) GetSigners() []sdk.AccAddress { // MsgClaimHardRewardVVesting message type used to claim Hard liquidity provider rewards for validator vesting accounts 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"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimHardRewardVVesting returns a new MsgClaimHardRewardVVesting. -func NewMsgClaimHardRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimHardRewardVVesting { +func NewMsgClaimHardRewardVVesting(sender, receiver sdk.AccAddress, denomsToClaim ...Selection) MsgClaimHardRewardVVesting { return MsgClaimHardRewardVVesting{ - Sender: sender, - Receiver: receiver, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + Receiver: receiver, + DenomsToClaim: denomsToClaim, } } @@ -196,17 +184,9 @@ func (msg MsgClaimHardRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + if err := msg.DenomsToClaim.Validate(); 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 } @@ -223,17 +203,15 @@ func (msg MsgClaimHardRewardVVesting) GetSigners() []sdk.AccAddress { // MsgClaimDelegatorReward message type used to claim delegator rewards 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"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimDelegatorReward returns a new MsgClaimDelegatorReward. -func NewMsgClaimDelegatorReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimDelegatorReward { +func NewMsgClaimDelegatorReward(sender sdk.AccAddress, denomsToClaim ...Selection) MsgClaimDelegatorReward { return MsgClaimDelegatorReward{ - Sender: sender, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + DenomsToClaim: denomsToClaim, } } @@ -250,17 +228,9 @@ func (msg MsgClaimDelegatorReward) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } - if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + if err := msg.DenomsToClaim.Validate(); 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 } @@ -277,19 +247,17 @@ func (msg MsgClaimDelegatorReward) GetSigners() []sdk.AccAddress { // MsgClaimDelegatorRewardVVesting message type used to claim delegator rewards for validator vesting accounts 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"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // MsgClaimDelegatorRewardVVesting returns a new MsgClaimDelegatorRewardVVesting. -func NewMsgClaimDelegatorRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimDelegatorRewardVVesting { +func NewMsgClaimDelegatorRewardVVesting(sender, receiver sdk.AccAddress, denomsToClaim ...Selection) MsgClaimDelegatorRewardVVesting { return MsgClaimDelegatorRewardVVesting{ - Sender: sender, - Receiver: receiver, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + Receiver: receiver, + DenomsToClaim: denomsToClaim, } } @@ -309,17 +277,9 @@ func (msg MsgClaimDelegatorRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + if err := msg.DenomsToClaim.Validate(); 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 } @@ -338,15 +298,14 @@ func (msg MsgClaimDelegatorRewardVVesting) GetSigners() []sdk.AccAddress { 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"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // NewMsgClaimSwapReward returns a new MsgClaimSwapReward. -func NewMsgClaimSwapReward(sender sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimSwapReward { +func NewMsgClaimSwapReward(sender sdk.AccAddress, denomsToClaim ...Selection) MsgClaimSwapReward { return MsgClaimSwapReward{ - Sender: sender, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + DenomsToClaim: denomsToClaim, } } @@ -363,17 +322,9 @@ 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 { + if err := msg.DenomsToClaim.Validate(); 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 } @@ -390,19 +341,17 @@ func (msg MsgClaimSwapReward) GetSigners() []sdk.AccAddress { // 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"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + DenomsToClaim Selections `json:"denoms_to_claim" yaml:"denoms_to_claim"` } // MsgClaimSwapRewardVVesting returns a new MsgClaimSwapRewardVVesting. -func NewMsgClaimSwapRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string, denomsToClaim []string) MsgClaimSwapRewardVVesting { +func NewMsgClaimSwapRewardVVesting(sender, receiver sdk.AccAddress, denomsToClaim ...Selection) MsgClaimSwapRewardVVesting { return MsgClaimSwapRewardVVesting{ - Sender: sender, - Receiver: receiver, - MultiplierName: multiplierName, - DenomsToClaim: denomsToClaim, + Sender: sender, + Receiver: receiver, + DenomsToClaim: denomsToClaim, } } @@ -422,17 +371,9 @@ func (msg MsgClaimSwapRewardVVesting) ValidateBasic() error { if msg.Receiver.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") } - if err := MultiplierName(msg.MultiplierName).IsValid(); err != nil { + if err := msg.DenomsToClaim.Validate(); 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 } diff --git a/x/incentive/types/msg_test.go b/x/incentive/types/msg_test.go index 4c9d3bda..4374f808 100644 --- a/x/incentive/types/msg_test.go +++ b/x/incentive/types/msg_test.go @@ -21,10 +21,9 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { pass bool } type msgArgs struct { - sender sdk.AccAddress - receiver sdk.AccAddress - multiplierName string - denomsToClaim []string + sender sdk.AccAddress + receiver sdk.AccAddress + denomsToClaim types.Selections } tests := []struct { name string @@ -34,9 +33,14 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { { name: "large multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "large", + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "large", + }, + }, }, expect: expectedErr{ pass: true, @@ -45,9 +49,14 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { { name: "medium multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "medium", + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + }, }, expect: expectedErr{ pass: true, @@ -56,54 +65,30 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { { name: "small multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "small", + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "small", + }, + }, }, expect: expectedErr{ pass: true, }, }, - { - name: "empty denoms to claim is valid", - msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "small", - denomsToClaim: []string{}, - }, - 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", + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "huge", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidMultiplier, @@ -112,21 +97,85 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { { name: "multiplier with capitalization is invalid", msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "Large", + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "Large", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidMultiplier, }, }, + { + name: "empty denoms to claim is not valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{}, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "nil denoms to claim is not valid", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + denomsToClaim: nil, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + }, + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { + name: "invalid receiver", + msgArgs: msgArgs{ + sender: validAddress, + receiver: sdk.AccAddress{}, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + }, + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, + { 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"}, + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "a denom string that is invalid because it is much too long", + MultiplierName: "medium", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidClaimDenoms, @@ -135,10 +184,29 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { { name: "too many claim denoms", msgArgs: msgArgs{ - sender: validAddress, - receiver: validAddress, - multiplierName: "small", - denomsToClaim: tooManyClaimDenoms(), + sender: validAddress, + receiver: validAddress, + denomsToClaim: tooManySelections(), + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "duplicated claim denoms", + msgArgs: msgArgs{ + sender: validAddress, + receiver: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + { + Denom: "hard", + MultiplierName: "large", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidClaimDenoms, @@ -149,13 +217,13 @@ func TestMsgClaimVVesting_Validate(t *testing.T) { for _, tc := range tests { msgs := []sdk.Msg{ types.NewMsgClaimHardRewardVVesting( - tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.denomsToClaim..., ), types.NewMsgClaimDelegatorRewardVVesting( - tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.denomsToClaim..., ), types.NewMsgClaimSwapRewardVVesting( - tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.multiplierName, tc.msgArgs.denomsToClaim, + tc.msgArgs.sender, tc.msgArgs.receiver, tc.msgArgs.denomsToClaim..., ), } for _, msg := range msgs { @@ -180,20 +248,25 @@ func TestMsgClaim_Validate(t *testing.T) { pass bool } type msgArgs struct { - sender sdk.AccAddress - multiplierName string - denomsToClaim []string + sender sdk.AccAddress + denomsToClaim types.Selections } tests := []struct { name string msgArgs msgArgs expect expectedErr }{ + { name: "large multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "large", + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "large", + }, + }, }, expect: expectedErr{ pass: true, @@ -202,8 +275,13 @@ func TestMsgClaim_Validate(t *testing.T) { { name: "medium multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "medium", + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + }, }, expect: expectedErr{ pass: true, @@ -212,39 +290,28 @@ func TestMsgClaim_Validate(t *testing.T) { { name: "small multiplier is valid", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "small", + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + 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", + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "huge", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidMultiplier, @@ -253,19 +320,63 @@ func TestMsgClaim_Validate(t *testing.T) { { name: "multiplier with capitalization is invalid", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "Large", + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "Large", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidMultiplier, }, }, + { + name: "empty denoms to claim is not valid", + msgArgs: msgArgs{ + sender: validAddress, + denomsToClaim: types.Selections{}, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "nil denoms to claim is not valid", + msgArgs: msgArgs{ + sender: validAddress, + denomsToClaim: nil, + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: sdk.AccAddress{}, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + }, + }, + expect: expectedErr{ + wraps: sdkerrors.ErrInvalidAddress, + }, + }, { name: "invalid claim denom", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "small", - denomsToClaim: []string{"a denom string that is invalid because it is much too long"}, + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "a denom string that is invalid because it is much too long", + MultiplierName: "medium", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidClaimDenoms, @@ -274,9 +385,27 @@ func TestMsgClaim_Validate(t *testing.T) { { name: "too many claim denoms", msgArgs: msgArgs{ - sender: validAddress, - multiplierName: "small", - denomsToClaim: tooManyClaimDenoms(), + sender: validAddress, + denomsToClaim: tooManySelections(), + }, + expect: expectedErr{ + wraps: types.ErrInvalidClaimDenoms, + }, + }, + { + name: "duplicated claim denoms", + msgArgs: msgArgs{ + sender: validAddress, + denomsToClaim: types.Selections{ + { + Denom: "hard", + MultiplierName: "medium", + }, + { + Denom: "hard", + MultiplierName: "large", + }, + }, }, expect: expectedErr{ wraps: types.ErrInvalidClaimDenoms, @@ -286,15 +415,9 @@ func TestMsgClaim_Validate(t *testing.T) { 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, - ), + types.NewMsgClaimHardReward(tc.msgArgs.sender, tc.msgArgs.denomsToClaim...), + types.NewMsgClaimDelegatorReward(tc.msgArgs.sender, tc.msgArgs.denomsToClaim...), + types.NewMsgClaimSwapReward(tc.msgArgs.sender, tc.msgArgs.denomsToClaim...), } for _, msg := range msgs { t.Run(msg.Type()+" "+tc.name, func(t *testing.T) { @@ -520,3 +643,14 @@ func tooManyClaimDenoms() []string { } return claimDenoms } + +func tooManySelections() types.Selections { + selections := make(types.Selections, types.MaxDenomsToClaim+1) + for i := range selections { + selections[i] = types.Selection{ + Denom: fmt.Sprintf("denom%d", i), + MultiplierName: "large", + } + } + return selections +} diff --git a/x/incentive/types/multipliers.go b/x/incentive/types/multipliers.go new file mode 100644 index 00000000..19f043d4 --- /dev/null +++ b/x/incentive/types/multipliers.go @@ -0,0 +1,201 @@ +package types + +import ( + "fmt" + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Available reward multipliers names +const ( + Small MultiplierName = "small" + Medium MultiplierName = "medium" + Large MultiplierName = "large" +) + +// MultiplierName is the user facing ID for a multiplier. There is a restricted set of possible values. +type MultiplierName string + +// IsValid checks if the input is one of the expected strings +func (mn MultiplierName) IsValid() error { + switch mn { + case Small, Medium, Large: + return nil + } + return sdkerrors.Wrapf(ErrInvalidMultiplier, "invalid multiplier name: %s", mn) +} + +// ParseMultiplierName converts a string into a valid MultiplierName value. +func ParseMultiplierName(unparsedName string) (MultiplierName, error) { + name := MultiplierName(unparsedName) + if err := name.IsValid(); err != nil { + return "", err + } + return name, nil +} + +// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked +type Multiplier struct { + Name MultiplierName `json:"name" yaml:"name"` + MonthsLockup int64 `json:"months_lockup" yaml:"months_lockup"` + Factor sdk.Dec `json:"factor" yaml:"factor"` +} + +// NewMultiplier returns a new Multiplier +func NewMultiplier(name MultiplierName, lockup int64, factor sdk.Dec) Multiplier { + return Multiplier{ + Name: name, + MonthsLockup: lockup, + Factor: factor, + } +} + +// Validate multiplier param +func (m Multiplier) Validate() error { + if err := m.Name.IsValid(); err != nil { + return err + } + if m.MonthsLockup < 0 { + return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup) + } + if m.Factor.IsNegative() { + return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String()) + } + + return nil +} + +// String implements fmt.Stringer +func (m Multiplier) String() string { + return fmt.Sprintf(`Claim Multiplier: + Name: %s + Months Lockup %d + Factor %s + `, m.Name, m.MonthsLockup, m.Factor) +} + +// Multipliers is a slice of Multiplier +type Multipliers []Multiplier + +// Validate validates each multiplier +func (ms Multipliers) Validate() error { + for _, m := range ms { + if err := m.Validate(); err != nil { + return err + } + } + return nil +} + +// Get returns a multiplier with a matching name +func (ms Multipliers) Get(name MultiplierName) (Multiplier, bool) { + for _, m := range ms { + if m.Name == name { + return m, true + } + } + return Multiplier{}, false +} + +// String implements fmt.Stringer +func (ms Multipliers) String() string { + out := "Claim Multipliers\n" + for _, s := range ms { + out += fmt.Sprintf("%s\n", s) + } + return out +} + +// MultipliersPerDenom is a map of denoms to a set of multipliers +type MultipliersPerDenom []struct { + Denom string `json:"denom" yaml:"denom"` + Multipliers Multipliers `json:"multipliers" yaml:"multipliers"` +} + +// Validate checks each denom and multipliers for invalid values. +func (mpd MultipliersPerDenom) Validate() error { + foundDenoms := map[string]bool{} + + for _, item := range mpd { + if err := sdk.ValidateDenom(item.Denom); err != nil { + return err + } + if err := item.Multipliers.Validate(); err != nil { + return err + } + + if foundDenoms[item.Denom] { + return fmt.Errorf("") + } + foundDenoms[item.Denom] = true + } + return nil +} + +// Selection a pair of denom and multiplier name. It holds the choice of multiplier a user makes when they claim a denom. +type Selection struct { + Denom string `json:"denom" yaml:"denom"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` +} + +// NewSelection returns a new Selection +func NewSelection(denom, multiplierName string) Selection { + return Selection{ + Denom: denom, + MultiplierName: multiplierName, + } +} + +// Validate performs basic validation checks +func (s Selection) Validate() error { + if err := sdk.ValidateDenom(s.Denom); err != nil { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, err.Error()) + } + if _, err := ParseMultiplierName(s.MultiplierName); err != nil { + return err + } + return nil +} + +// Selections are a list of denom - multiplier pairs that specify what rewards to claim and with what lockups. +type Selections []Selection + +// NewSelectionsFromMap creates a new set of selections from a string to string map. +// It sorts the output before returning. +func NewSelectionsFromMap(selectionMap map[string]string) Selections { + var selections Selections + for k, v := range selectionMap { + selections = append(selections, NewSelection(k, v)) + } + // deterministically sort the slice to protect against the random range order causing consensus failures + sort.Slice(selections, func(i, j int) bool { + if selections[i].Denom != selections[j].Denom { + return selections[i].Denom < selections[j].Denom + } + return selections[i].MultiplierName < selections[j].MultiplierName + }) + return selections +} + +// Valdate performs basic validaton checks +func (ss Selections) Validate() error { + if len(ss) == 0 { + return sdkerrors.Wrap(ErrInvalidClaimDenoms, "cannot claim 0 denoms") + } + if len(ss) >= MaxDenomsToClaim { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim more than %d denoms", MaxDenomsToClaim) + } + foundDenoms := map[string]bool{} + for _, s := range ss { + if err := s.Validate(); err != nil { + return err + } + if foundDenoms[s.Denom] { + return sdkerrors.Wrapf(ErrInvalidClaimDenoms, "cannot claim denom '%s' more than once", s.Denom) + } + foundDenoms[s.Denom] = true + } + return nil +} diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index 5b9bc578..6b7c6e04 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -7,20 +7,12 @@ 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" kavadistTypes "github.com/kava-labs/kava/x/kavadist/types" ) -// Valid reward multipliers -const ( - Small MultiplierName = "small" - Medium MultiplierName = "medium" - Large MultiplierName = "large" -) - // Parameter keys and default values var ( KeyUSDXMintingRewardPeriods = []byte("USDXMintingRewardPeriods") @@ -34,7 +26,7 @@ var ( DefaultActive = false DefaultRewardPeriods = RewardPeriods{} DefaultMultiRewardPeriods = MultiRewardPeriods{} - DefaultMultipliers = Multipliers{} + DefaultMultipliers = MultipliersPerDenom{} DefaultClaimEnd = tmtime.Canonical(time.Unix(1, 0)) BondDenom = "ukava" @@ -45,18 +37,18 @@ var ( // Params governance parameters for the incentive module type Params struct { - USDXMintingRewardPeriods RewardPeriods `json:"usdx_minting_reward_periods" yaml:"usdx_minting_reward_periods"` - 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" yaml:"swap_reward_periods"` - ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` - ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` + USDXMintingRewardPeriods RewardPeriods `json:"usdx_minting_reward_periods" yaml:"usdx_minting_reward_periods"` + 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" yaml:"swap_reward_periods"` + ClaimMultipliers MultipliersPerDenom `json:"claim_multipliers" yaml:"claim_multipliers"` + ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` } // NewParams returns a new params object func NewParams(usdxMinting RewardPeriods, hardSupply, hardBorrow, delegator, swap MultiRewardPeriods, - multipliers Multipliers, claimEnd time.Time) Params { + multipliers MultipliersPerDenom, claimEnd time.Time) Params { return Params{ USDXMintingRewardPeriods: usdxMinting, HardSupplyRewardPeriods: hardSupply, @@ -89,7 +81,7 @@ func (p Params) String() string { Hard Borrow Reward Periods: %s Delegator Reward Periods: %s Swap Reward Periods: %s - Claim Multipliers :%s + Claim Multipliers: %s Claim End Time: %s `, p.USDXMintingRewardPeriods, p.HardSupplyRewardPeriods, p.HardBorrowRewardPeriods, p.DelegatorRewardPeriods, p.SwapRewardPeriods, p.ClaimMultipliers, p.ClaimEnd) @@ -108,15 +100,15 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { params.NewParamSetPair(KeyHardBorrowRewardPeriods, &p.HardBorrowRewardPeriods, validateMultiRewardPeriodsParam), params.NewParamSetPair(KeyDelegatorRewardPeriods, &p.DelegatorRewardPeriods, validateMultiRewardPeriodsParam), params.NewParamSetPair(KeySwapRewardPeriods, &p.SwapRewardPeriods, validateMultiRewardPeriodsParam), + params.NewParamSetPair(KeyMultipliers, &p.ClaimMultipliers, validateMultipliersPerDenomParam), params.NewParamSetPair(KeyClaimEnd, &p.ClaimEnd, validateClaimEndParam), - params.NewParamSetPair(KeyMultipliers, &p.ClaimMultipliers, validateMultipliersParam), } } // Validate checks that the parameters have valid values. func (p Params) Validate() error { - if err := validateMultipliersParam(p.ClaimMultipliers); err != nil { + if err := validateMultipliersPerDenomParam(p.ClaimMultipliers); err != nil { return err } @@ -169,6 +161,14 @@ func validateMultipliersParam(i interface{}) error { return multipliers.Validate() } +func validateMultipliersPerDenomParam(i interface{}) error { + multipliers, ok := i.(MultipliersPerDenom) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return multipliers.Validate() +} + func validateClaimEndParam(i interface{}) error { endTime, ok := i.(time.Time) if !ok { @@ -360,77 +360,3 @@ func (mrps MultiRewardPeriods) Validate() error { return nil } - -// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked -type Multiplier struct { - Name MultiplierName `json:"name" yaml:"name"` - MonthsLockup int64 `json:"months_lockup" yaml:"months_lockup"` - Factor sdk.Dec `json:"factor" yaml:"factor"` -} - -// NewMultiplier returns a new Multiplier -func NewMultiplier(name MultiplierName, lockup int64, factor sdk.Dec) Multiplier { - return Multiplier{ - Name: name, - MonthsLockup: lockup, - Factor: factor, - } -} - -// Validate multiplier param -func (m Multiplier) Validate() error { - if err := m.Name.IsValid(); err != nil { - return err - } - if m.MonthsLockup < 0 { - return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup) - } - if m.Factor.IsNegative() { - return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String()) - } - - return nil -} - -// String implements fmt.Stringer -func (m Multiplier) String() string { - return fmt.Sprintf(`Claim Multiplier: - Name: %s - Months Lockup %d - Factor %s - `, m.Name, m.MonthsLockup, m.Factor) -} - -// Multipliers slice of Multiplier -type Multipliers []Multiplier - -// Validate validates each multiplier -func (ms Multipliers) Validate() error { - for _, m := range ms { - if err := m.Validate(); err != nil { - return err - } - } - return nil -} - -// String implements fmt.Stringer -func (ms Multipliers) String() string { - out := "Claim Multipliers\n" - for _, s := range ms { - out += fmt.Sprintf("%s\n", s) - } - return out -} - -// MultiplierName name for valid multiplier -type MultiplierName string - -// IsValid checks if the input is one of the expected strings -func (mn MultiplierName) IsValid() error { - switch mn { - case Small, Medium, Large: - return nil - } - return sdkerrors.Wrapf(ErrInvalidMultiplier, "invalid multiplier name: %s", mn) -} diff --git a/x/incentive/types/params_test.go b/x/incentive/types/params_test.go index 91240b25..2b411465 100644 --- a/x/incentive/types/params_test.go +++ b/x/incentive/types/params_test.go @@ -77,19 +77,27 @@ func (suite *ParamTestSuite) TestParamValidation() { time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)), )}, - ClaimMultipliers: types.Multipliers{ - types.NewMultiplier( - types.Small, 1, sdk.MustNewDecFromStr("0.25"), - ), - types.NewMultiplier( - types.Large, 1, sdk.MustNewDecFromStr("1.0"), - ), - }, HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, DelegatorRewardPeriods: types.DefaultMultiRewardPeriods, SwapRewardPeriods: types.DefaultMultiRewardPeriods, - ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + ClaimMultipliers: types.MultipliersPerDenom{ + { + Denom: "hard", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.25")), + types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0")), + }, + }, + { + Denom: "ukava", + Multipliers: types.Multipliers{ + types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.2")), + types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0")), + }, + }, + }, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), }, errArgs{ expectPass: true, diff --git a/x/incentive/types/sdk.go b/x/incentive/types/sdk.go index 276ce6b8..81def485 100644 --- a/x/incentive/types/sdk.go +++ b/x/incentive/types/sdk.go @@ -18,29 +18,3 @@ func GetTotalVestingPeriodLength(periods vesting.Periods) int64 { } 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 index 3fc7d096..839235cd 100644 --- a/x/incentive/types/sdk_test.go +++ b/x/incentive/types/sdk_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -43,128 +42,3 @@ func TestGetTotalVestingPeriodLength(t *testing.T) { }) } } - -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), - ) - }) - } -}