From 4d6f6aab3c85dbcc63e83a2334b9b175ae166789 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Mon, 7 Jun 2021 08:04:32 -0600 Subject: [PATCH] [R4R] re-enable claiming rewards validator vesting accounts (#920) * feat: add new msg types for claim rewards from validator vesting accounts * fix: validate owner is validator vesting account * feat: add validator vesting tests for incent claims * address review comments * fix: client command name and example --- x/incentive/alias.go | 277 ++++++++++--------- x/incentive/client/cli/tx.go | 72 +++++ x/incentive/client/rest/rest.go | 8 + x/incentive/client/rest/tx.go | 69 +++++ x/incentive/handler.go | 27 +- x/incentive/keeper/payout.go | 128 +++++++++ x/incentive/keeper/payout_test.go | 440 ++++++++++++++++++++++++++++++ x/incentive/types/codec.go | 2 + x/incentive/types/msg.go | 94 +++++++ 9 files changed, 988 insertions(+), 129 deletions(-) diff --git a/x/incentive/alias.go b/x/incentive/alias.go index d756e15e..c7986af5 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -1,154 +1,175 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/kava-labs/kava/x/incentive/types +// ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper package incentive -// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) - import ( "github.com/kava-labs/kava/x/incentive/keeper" "github.com/kava-labs/kava/x/incentive/types" ) const ( - BeginningOfMonth = keeper.BeginningOfMonth - MidMonth = keeper.MidMonth - PaymentHour = keeper.PaymentHour - AttributeKeyClaimAmount = types.AttributeKeyClaimAmount - AttributeKeyClaimPeriod = types.AttributeKeyClaimPeriod - AttributeKeyClaimType = types.AttributeKeyClaimType - AttributeKeyClaimedBy = types.AttributeKeyClaimedBy - AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod - AttributeValueCategory = types.AttributeValueCategory - BondDenom = types.BondDenom - DefaultParamspace = types.DefaultParamspace - EventTypeClaim = types.EventTypeClaim - EventTypeClaimPeriod = types.EventTypeClaimPeriod - EventTypeClaimPeriodExpiry = types.EventTypeClaimPeriodExpiry - EventTypeRewardPeriod = types.EventTypeRewardPeriod - HardLiquidityProviderClaimType = types.HardLiquidityProviderClaimType - Large = types.Large - Medium = types.Medium - ModuleName = types.ModuleName - QuerierRoute = types.QuerierRoute - QueryGetClaimPeriods = types.QueryGetClaimPeriods - QueryGetHardRewards = types.QueryGetHardRewards - QueryGetParams = types.QueryGetParams - QueryGetRewardPeriods = types.QueryGetRewardPeriods - QueryGetRewards = types.QueryGetRewards - QueryGetUSDXMintingRewards = types.QueryGetUSDXMintingRewards - RestClaimCollateralType = types.RestClaimCollateralType - RestClaimOwner = types.RestClaimOwner - RestClaimType = types.RestClaimType - RouterKey = types.RouterKey - Small = types.Small - StoreKey = types.StoreKey - USDXMintingClaimType = types.USDXMintingClaimType + USDXMintingClaimType = types.USDXMintingClaimType + HardLiquidityProviderClaimType = types.HardLiquidityProviderClaimType + BondDenom = types.BondDenom + EventTypeClaim = types.EventTypeClaim + EventTypeRewardPeriod = types.EventTypeRewardPeriod + EventTypeClaimPeriod = types.EventTypeClaimPeriod + EventTypeClaimPeriodExpiry = types.EventTypeClaimPeriodExpiry + AttributeValueCategory = types.AttributeValueCategory + AttributeKeyClaimedBy = types.AttributeKeyClaimedBy + AttributeKeyClaimAmount = types.AttributeKeyClaimAmount + AttributeKeyClaimType = types.AttributeKeyClaimType + AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod + AttributeKeyClaimPeriod = types.AttributeKeyClaimPeriod + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + DefaultParamspace = types.DefaultParamspace + QuerierRoute = types.QuerierRoute + Small = types.Small + Medium = types.Medium + Large = types.Large + QueryGetRewards = types.QueryGetRewards + QueryGetHardRewards = types.QueryGetHardRewards + QueryGetHardRewardsUnsynced = types.QueryGetHardRewardsUnsynced + QueryGetUSDXMintingRewards = types.QueryGetUSDXMintingRewards + QueryGetUSDXMintingRewardsUnsynced = types.QueryGetUSDXMintingRewardsUnsynced + QueryGetRewardFactors = types.QueryGetRewardFactors + QueryGetParams = types.QueryGetParams + QueryGetRewardPeriods = types.QueryGetRewardPeriods + QueryGetClaimPeriods = types.QueryGetClaimPeriods + RestClaimCollateralType = types.RestClaimCollateralType + RestClaimOwner = types.RestClaimOwner + RestClaimType = types.RestClaimType + RestUnsynced = types.RestUnsynced + BeginningOfMonth = keeper.BeginningOfMonth + MidMonth = keeper.MidMonth + PaymentHour = keeper.PaymentHour ) var ( - // function aliases - CalculateTimeElapsed = keeper.CalculateTimeElapsed - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier - DefaultGenesisState = types.DefaultGenesisState - DefaultParams = types.DefaultParams - GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength - NewGenesisAccumulationTime = types.NewGenesisAccumulationTime - NewGenesisState = types.NewGenesisState - NewHardLiquidityProviderClaim = types.NewHardLiquidityProviderClaim - NewMsgClaimHardReward = types.NewMsgClaimHardReward - NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward - NewMultiRewardIndex = types.NewMultiRewardIndex - NewMultiRewardPeriod = types.NewMultiRewardPeriod - NewMultiplier = types.NewMultiplier - NewParams = types.NewParams - NewPeriod = types.NewPeriod - NewQueryHardRewardsParams = types.NewQueryHardRewardsParams - NewQueryRewardsParams = types.NewQueryRewardsParams - NewQueryUSDXMintingRewardsParams = types.NewQueryUSDXMintingRewardsParams - NewRewardIndex = types.NewRewardIndex - NewRewardPeriod = types.NewRewardPeriod - NewUSDXMintingClaim = types.NewUSDXMintingClaim - ParamKeyTable = types.ParamKeyTable - RegisterCodec = types.RegisterCodec + // functions aliases + GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength + NewUSDXMintingClaim = types.NewUSDXMintingClaim + NewHardLiquidityProviderClaim = types.NewHardLiquidityProviderClaim + NewMultiRewardPeriod = types.NewMultiRewardPeriod + NewRewardIndex = types.NewRewardIndex + NewMultiRewardIndex = types.NewMultiRewardIndex + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + NewGenesisAccumulationTime = types.NewGenesisAccumulationTime + NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward + NewMsgClaimUSDXMintingRewardVVesting = types.NewMsgClaimUSDXMintingRewardVVesting + NewMsgClaimHardReward = types.NewMsgClaimHardReward + NewMsgClaimHardRewardVVesting = types.NewMsgClaimHardRewardVVesting + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewRewardPeriod = types.NewRewardPeriod + NewMultiplier = types.NewMultiplier + NewPeriod = types.NewPeriod + NewQueryRewardsParams = types.NewQueryRewardsParams + NewQueryUSDXMintingRewardsParams = types.NewQueryUSDXMintingRewardsParams + NewQueryUSDXMintingRewardsUnsyncedParams = types.NewQueryUSDXMintingRewardsUnsyncedParams + NewQueryHardRewardsParams = types.NewQueryHardRewardsParams + NewQueryHardRewardsUnsyncedParams = types.NewQueryHardRewardsUnsyncedParams + NewQueryRewardFactorsParams = types.NewQueryRewardFactorsParams + NewRewardFactor = types.NewRewardFactor + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + CalculateTimeElapsed = keeper.CalculateTimeElapsed // variable aliases - DefaultActive = types.DefaultActive - DefaultClaimEnd = types.DefaultClaimEnd - DefaultGenesisAccumulationTimes = types.DefaultGenesisAccumulationTimes - DefaultHardClaims = types.DefaultHardClaims - DefaultMultiRewardPeriods = types.DefaultMultiRewardPeriods - DefaultMultipliers = types.DefaultMultipliers - DefaultRewardPeriods = types.DefaultRewardPeriods - DefaultUSDXClaims = types.DefaultUSDXClaims - ErrAccountNotFound = types.ErrAccountNotFound - ErrClaimExpired = types.ErrClaimExpired + ModuleCdc = types.ModuleCdc ErrClaimNotFound = types.ErrClaimNotFound - ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance - ErrInvalidAccountType = types.ErrInvalidAccountType - ErrInvalidClaimType = types.ErrInvalidClaimType - ErrInvalidMultiplier = types.ErrInvalidMultiplier - ErrNoClaimsFound = types.ErrNoClaimsFound ErrRewardPeriodNotFound = types.ErrRewardPeriodNotFound + ErrInvalidAccountType = types.ErrInvalidAccountType + ErrNoClaimsFound = types.ErrNoClaimsFound + ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance + ErrAccountNotFound = types.ErrAccountNotFound + ErrInvalidMultiplier = types.ErrInvalidMultiplier ErrZeroClaim = types.ErrZeroClaim - GovDenom = types.GovDenom - HardBorrowRewardIndexesKeyPrefix = types.HardBorrowRewardIndexesKeyPrefix - HardDelegatorRewardFactorKeyPrefix = types.HardDelegatorRewardFactorKeyPrefix + ErrClaimExpired = types.ErrClaimExpired + ErrInvalidClaimType = types.ErrInvalidClaimType + ErrInvalidClaimOwner = types.ErrInvalidClaimOwner + USDXMintingClaimKeyPrefix = types.USDXMintingClaimKeyPrefix + USDXMintingRewardFactorKeyPrefix = types.USDXMintingRewardFactorKeyPrefix + PreviousUSDXMintingRewardAccrualTimeKeyPrefix = types.PreviousUSDXMintingRewardAccrualTimeKeyPrefix HardLiquidityClaimKeyPrefix = types.HardLiquidityClaimKeyPrefix - HardLiquidityRewardDenom = types.HardLiquidityRewardDenom HardSupplyRewardIndexesKeyPrefix = types.HardSupplyRewardIndexesKeyPrefix - IncentiveMacc = types.IncentiveMacc - KeyClaimEnd = types.KeyClaimEnd + PreviousHardSupplyRewardAccrualTimeKeyPrefix = types.PreviousHardSupplyRewardAccrualTimeKeyPrefix + HardBorrowRewardIndexesKeyPrefix = types.HardBorrowRewardIndexesKeyPrefix + PreviousHardBorrowRewardAccrualTimeKeyPrefix = types.PreviousHardBorrowRewardAccrualTimeKeyPrefix + HardDelegatorRewardFactorKeyPrefix = types.HardDelegatorRewardFactorKeyPrefix + PreviousHardDelegatorRewardAccrualTimeKeyPrefix = types.PreviousHardDelegatorRewardAccrualTimeKeyPrefix + USDXMintingRewardDenom = types.USDXMintingRewardDenom + HardLiquidityRewardDenom = types.HardLiquidityRewardDenom + KeyUSDXMintingRewardPeriods = types.KeyUSDXMintingRewardPeriods + KeyHardSupplyRewardPeriods = types.KeyHardSupplyRewardPeriods KeyHardBorrowRewardPeriods = types.KeyHardBorrowRewardPeriods KeyHardDelegatorRewardPeriods = types.KeyHardDelegatorRewardPeriods - KeyHardSupplyRewardPeriods = types.KeyHardSupplyRewardPeriods + KeyClaimEnd = types.KeyClaimEnd KeyMultipliers = types.KeyMultipliers - KeyUSDXMintingRewardPeriods = types.KeyUSDXMintingRewardPeriods - ModuleCdc = types.ModuleCdc - PreviousHardBorrowRewardAccrualTimeKeyPrefix = types.PreviousHardBorrowRewardAccrualTimeKeyPrefix - PreviousHardDelegatorRewardAccrualTimeKeyPrefix = types.PreviousHardDelegatorRewardAccrualTimeKeyPrefix - PreviousHardSupplyRewardAccrualTimeKeyPrefix = types.PreviousHardSupplyRewardAccrualTimeKeyPrefix - PreviousUSDXMintingRewardAccrualTimeKeyPrefix = types.PreviousUSDXMintingRewardAccrualTimeKeyPrefix + DefaultActive = types.DefaultActive + DefaultRewardPeriods = types.DefaultRewardPeriods + DefaultMultiRewardPeriods = types.DefaultMultiRewardPeriods + DefaultMultipliers = types.DefaultMultipliers + DefaultUSDXClaims = types.DefaultUSDXClaims + DefaultHardClaims = types.DefaultHardClaims + DefaultGenesisAccumulationTimes = types.DefaultGenesisAccumulationTimes + DefaultClaimEnd = types.DefaultClaimEnd + GovDenom = types.GovDenom PrincipalDenom = types.PrincipalDenom - USDXMintingClaimKeyPrefix = types.USDXMintingClaimKeyPrefix - USDXMintingRewardDenom = types.USDXMintingRewardDenom - USDXMintingRewardFactorKeyPrefix = types.USDXMintingRewardFactorKeyPrefix + IncentiveMacc = types.IncentiveMacc ) type ( - Hooks = keeper.Hooks - Keeper = keeper.Keeper - AccountKeeper = types.AccountKeeper - BaseClaim = types.BaseClaim - BaseMultiClaim = types.BaseMultiClaim - CDPHooks = types.CDPHooks - CdpKeeper = types.CdpKeeper - Claim = types.Claim - Claims = types.Claims - GenesisAccumulationTime = types.GenesisAccumulationTime - GenesisAccumulationTimes = types.GenesisAccumulationTimes - GenesisState = types.GenesisState - HARDHooks = types.HARDHooks - HardKeeper = types.HardKeeper - HardLiquidityProviderClaim = types.HardLiquidityProviderClaim - HardLiquidityProviderClaims = types.HardLiquidityProviderClaims - MsgClaimHardReward = types.MsgClaimHardReward - MsgClaimUSDXMintingReward = types.MsgClaimUSDXMintingReward - MultiRewardIndex = types.MultiRewardIndex - MultiRewardIndexes = types.MultiRewardIndexes - MultiRewardPeriod = types.MultiRewardPeriod - MultiRewardPeriods = types.MultiRewardPeriods - Multiplier = types.Multiplier - MultiplierName = types.MultiplierName - Multipliers = types.Multipliers - Params = types.Params - QueryHardRewardsParams = types.QueryHardRewardsParams - QueryRewardsParams = types.QueryRewardsParams - QueryUSDXMintingRewardsParams = types.QueryUSDXMintingRewardsParams - RewardIndex = types.RewardIndex - RewardIndexes = types.RewardIndexes - RewardPeriod = types.RewardPeriod - RewardPeriods = types.RewardPeriods - StakingKeeper = types.StakingKeeper - SupplyKeeper = types.SupplyKeeper - USDXMintingClaim = types.USDXMintingClaim - USDXMintingClaims = types.USDXMintingClaims + Claim = types.Claim + Claims = types.Claims + BaseClaim = types.BaseClaim + BaseMultiClaim = types.BaseMultiClaim + USDXMintingClaim = types.USDXMintingClaim + USDXMintingClaims = types.USDXMintingClaims + HardLiquidityProviderClaim = types.HardLiquidityProviderClaim + HardLiquidityProviderClaims = types.HardLiquidityProviderClaims + MultiRewardPeriod = types.MultiRewardPeriod + MultiRewardPeriods = types.MultiRewardPeriods + RewardIndex = types.RewardIndex + RewardIndexes = types.RewardIndexes + MultiRewardIndex = types.MultiRewardIndex + MultiRewardIndexes = types.MultiRewardIndexes + SupplyKeeper = types.SupplyKeeper + StakingKeeper = types.StakingKeeper + CdpKeeper = types.CdpKeeper + HardKeeper = types.HardKeeper + AccountKeeper = types.AccountKeeper + CDPHooks = types.CDPHooks + HARDHooks = types.HARDHooks + GenesisState = types.GenesisState + GenesisAccumulationTime = types.GenesisAccumulationTime + GenesisAccumulationTimes = types.GenesisAccumulationTimes + MsgClaimUSDXMintingReward = types.MsgClaimUSDXMintingReward + MsgClaimUSDXMintingRewardVVesting = types.MsgClaimUSDXMintingRewardVVesting + MsgClaimHardReward = types.MsgClaimHardReward + MsgClaimHardRewardVVesting = types.MsgClaimHardRewardVVesting + Params = types.Params + RewardPeriod = types.RewardPeriod + RewardPeriods = types.RewardPeriods + Multiplier = types.Multiplier + Multipliers = types.Multipliers + MultiplierName = types.MultiplierName + QueryRewardsParams = types.QueryRewardsParams + QueryUSDXMintingRewardsParams = types.QueryUSDXMintingRewardsParams + QueryUSDXMintingRewardsUnsyncedParams = types.QueryUSDXMintingRewardsUnsyncedParams + QueryHardRewardsParams = types.QueryHardRewardsParams + QueryHardRewardsUnsyncedParams = types.QueryHardRewardsUnsyncedParams + QueryRewardFactorsParams = types.QueryRewardFactorsParams + RewardFactor = types.RewardFactor + RewardFactors = types.RewardFactors + Hooks = keeper.Hooks + Keeper = keeper.Keeper ) diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go index 07642149..59f66292 100644 --- a/x/incentive/client/cli/tx.go +++ b/x/incentive/client/cli/tx.go @@ -27,7 +27,9 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { incentiveTxCmd.AddCommand(flags.PostCommands( getCmdClaimCdp(cdc), + getCmdClaimCdpVVesting(cdc), getCmdClaimHard(cdc), + getCmdClaimHardVVesting(cdc), )...) return incentiveTxCmd @@ -63,6 +65,76 @@ func getCmdClaimCdp(cdc *codec.Codec) *cobra.Command { } } +func getCmdClaimCdpVVesting(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "claim-cdp-vesting [multiplier] [receiver]", + Short: "claim CDP rewards using a given multiplier on behalf of a validator vesting account", + Long: strings.TrimSpace( + fmt.Sprintf(`Claim sender's outstanding CDP rewards on behalf of a validator vesting using a given multiplier + + Example: + $ %s tx %s claim-cdp-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw + `, version.ClientName, types.ModuleName), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + receiverStr := args[1] + receiver, err := sdk.AccAddressFromBech32(receiverStr) + if err != nil { + return err + } + + msg := types.NewMsgClaimUSDXMintingRewardVVesting(sender, receiver, multiplier) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +func getCmdClaimHardVVesting(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "claim-hard-vesting [multiplier] [receiver]", + Short: "claim Hard module rewards on behalf of a validator vesting account using a given multiplier", + Long: strings.TrimSpace( + fmt.Sprintf(`Claim sender's outstanding Hard rewards on behalf of a validator vesting account for deposit/borrow/delegate using given multiplier + + Example: + $ %s tx %s claim-hard-vesting large kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw + `, version.ClientName, types.ModuleName), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + + sender := cliCtx.GetFromAddress() + multiplier := args[0] + receiverStr := args[1] + receiver, err := sdk.AccAddressFromBech32(receiverStr) + if err != nil { + return err + } + + msg := types.NewMsgClaimHardRewardVVesting(sender, receiver, multiplier) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + func getCmdClaimHard(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "claim-hard [multiplier]", diff --git a/x/incentive/client/rest/rest.go b/x/incentive/client/rest/rest.go index 9edcb64b..d17af7bc 100644 --- a/x/incentive/client/rest/rest.go +++ b/x/incentive/client/rest/rest.go @@ -26,3 +26,11 @@ type PostClaimReq struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` } + +// 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"` +} diff --git a/x/incentive/client/rest/tx.go b/x/incentive/client/rest/tx.go index 17cf8440..c3396670 100644 --- a/x/incentive/client/rest/tx.go +++ b/x/incentive/client/rest/tx.go @@ -17,7 +17,10 @@ import ( func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc("/incentive/claim-cdp", postClaimCdpHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/incentive/claim-cdp-vesting", postClaimCdpVVestingHandlerFn(cliCtx)).Methods("POST") r.HandleFunc("/incentive/claim-hard", postClaimHardHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/incentive/claim-hard-vesting", postClaimHardVVestingHandlerFn(cliCtx)).Methods("POST") + } func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { @@ -53,6 +56,39 @@ func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } +func postClaimCdpVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var requestBody PostClaimVVestingReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if !bytes.Equal(fromAddr, requestBody.Sender) { + rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) + return + } + + msg := types.NewMsgClaimUSDXMintingRewardVVesting(requestBody.Sender, requestBody.Receiver, requestBody.MultiplierName) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} + func postClaimHardHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody PostClaimReq @@ -85,3 +121,36 @@ func postClaimHardHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) } } + +func postClaimHardVVestingHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var requestBody PostClaimVVestingReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if !bytes.Equal(fromAddr, requestBody.Sender) { + rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) + return + } + + msg := types.NewMsgClaimHardRewardVVesting(requestBody.Sender, requestBody.Receiver, requestBody.MultiplierName) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/incentive/handler.go b/x/incentive/handler.go index a31ddd07..7f26f512 100644 --- a/x/incentive/handler.go +++ b/x/incentive/handler.go @@ -15,8 +15,12 @@ func NewHandler(k keeper.Keeper) sdk.Handler { switch msg := msg.(type) { case types.MsgClaimUSDXMintingReward: return handleMsgClaimUSDXMintingReward(ctx, k, msg) + case types.MsgClaimUSDXMintingRewardVVesting: + return handleMsgClaimUSDXMintingRewardVVesting(ctx, k, msg) case types.MsgClaimHardReward: return handleMsgClaimHardReward(ctx, k, msg) + case types.MsgClaimHardRewardVVesting: + return handleMsgClaimHardRewardVVesting(ctx, k, msg) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } @@ -24,7 +28,6 @@ func NewHandler(k keeper.Keeper) sdk.Handler { } func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingReward) (*sdk.Result, error) { - err := k.ClaimUSDXMintingReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) if err != nil { return nil, err @@ -34,6 +37,17 @@ func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types }, nil } +func handleMsgClaimUSDXMintingRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingRewardVVesting) (*sdk.Result, error) { + + err := k.ClaimUSDXMintingRewardVVesting(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + if err != nil { + return nil, err + } + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} + func handleMsgClaimHardReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardReward) (*sdk.Result, error) { err := k.ClaimHardReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) @@ -44,3 +58,14 @@ func handleMsgClaimHardReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgCla Events: ctx.EventManager().Events(), }, nil } + +func handleMsgClaimHardRewardVVesting(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardRewardVVesting) (*sdk.Result, error) { + + err := k.ClaimHardRewardVVesting(ctx, msg.Sender, msg.Receiver, types.MultiplierName(msg.MultiplierName)) + if err != nil { + return nil, err + } + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index 7776b347..6bf9b7a1 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -74,6 +74,66 @@ func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, addr sdk.AccAddress, mul return nil } +// ClaimUSDXMintingReward sends the reward amount to the input receiver address and zero's out the claim in the store +func (k Keeper) ClaimUSDXMintingRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { + claim, found := k.GetUSDXMintingClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + acc := k.accountKeeper.GetAccount(ctx, owner) + if acc == nil { + return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner) + } + _, ok := acc.(*validatorvesting.ValidatorVestingAccount) + if !ok { + return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) + if err != nil { + return err + } + + rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt() + if rewardAmount.IsZero() { + return types.ErrZeroClaim + } + rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, sdk.NewCoins(rewardCoin), length) + if err != nil { + return err + } + + k.ZeroUSDXMintingClaim(ctx, claim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()), + ), + ) + return nil +} + // ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { _, found := k.GetHardLiquidityProviderClaim(ctx, addr) @@ -133,6 +193,74 @@ func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplier return nil } +// ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store +func (k Keeper) ClaimHardRewardVVesting(ctx sdk.Context, owner, receiver sdk.AccAddress, multiplierName types.MultiplierName) error { + _, found := k.GetHardLiquidityProviderClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + acc := k.accountKeeper.GetAccount(ctx, owner) + if acc == nil { + return sdkerrors.Wrapf(types.ErrAccountNotFound, "address not found: %s", owner) + } + _, ok := acc.(*validatorvesting.ValidatorVestingAccount) + if !ok { + return sdkerrors.Wrapf(types.ErrInvalidAccountType, "owner account must be validator vesting account %s", owner) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + k.SynchronizeHardLiquidityProviderClaim(ctx, owner) + + claim, found := k.GetHardLiquidityProviderClaim(ctx, owner) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", owner) + } + + var rewardCoins sdk.Coins + for _, coin := range claim.Reward { + rewardAmount := coin.Amount.ToDec().Mul(multiplier.Factor).RoundInt() + if rewardAmount.IsZero() { + continue + } + rewardCoins = append(rewardCoins, sdk.NewCoin(coin.Denom, rewardAmount)) + } + if rewardCoins.IsZero() { + return types.ErrZeroClaim + } + length, err := k.GetPeriodLength(ctx, multiplier) + if err != nil { + return err + } + + err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, receiver, rewardCoins, length) + if err != nil { + return err + } + + k.ZeroHardLiquidityProviderClaim(ctx, claim) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaim, + sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), + sdk.NewAttribute(types.AttributeKeyClaimType, claim.GetType()), + ), + ) + return nil +} + // SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account and the input length is greater than zero, the recipient account is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length. func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error { macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule) diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index 188fa965..7f146d76 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -155,6 +155,156 @@ func (suite *KeeperTestSuite) TestPayoutUSDXMintingClaim() { } } +func (suite *KeeperTestSuite) TestPayoutUSDXMintingClaimVVesting() { + type args struct { + ctype string + rewardsPerSecond sdk.Coin + initialTime time.Time + initialCollateral sdk.Coin + initialPrincipal sdk.Coin + multipliers types.Multipliers + multiplier types.MultiplierName + timeElapsed int + expectedBalance sdk.Coins + expectedPeriods vesting.Periods + isPeriodicVestingAccount bool + } + type errArgs struct { + expectPass bool + contains string + } + type test struct { + name string + args args + errArgs errArgs + } + testCases := []test{ + { + "valid 1 day", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 122354), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedBalance: cs(c("ukava", 11571385600)), + expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("ukava", 10571385600))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "invalid zero rewards", + args{ + ctype: "bnb-a", + rewardsPerSecond: c("ukava", 0), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + initialCollateral: c("bnb", 1000000000000), + initialPrincipal: c("usdx", 10000000000), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedBalance: cs(), + expectedPeriods: vesting.Periods{}, + isPeriodicVestingAccount: false, + }, + errArgs{ + expectPass: false, + contains: "claim amount rounds to zero", + }, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // setup incentive state + params := types.NewParams( + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.MultiRewardPeriods{types.NewMultiRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), cs(tc.args.rewardsPerSecond))}, + types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)}, + tc.args.multipliers, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + suite.keeper.SetPreviousUSDXMintingAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime) + suite.keeper.SetUSDXMintingRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec()) + + // sets addrs[2] to be a validator vesting account + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[2]) + bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence()) + bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), suite.ctx.BlockTime().Unix()+16) + suite.Require().NoError(err2) + periods := vesting.Periods{ + vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))}, + } + vva := validatorvesting.NewValidatorVestingAccountRaw(bva, suite.ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90) + ak.SetAccount(suite.ctx, vva) + + // setup account state + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral)) + suite.Require().NoError(err) + err = sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[2], sdk.NewCoins(tc.args.initialCollateral)) + suite.Require().NoError(err) + + // setup kavadist state + err = sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000))) + suite.Require().NoError(err) + + // setup cdp state + cdpKeeper := suite.app.GetCDPKeeper() + err = cdpKeeper.AddCdp(suite.ctx, suite.addrs[2], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype) + suite.Require().NoError(err) + + claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[2]) + suite.Require().True(found) + suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor) + + updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed)) + suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime) + rewardPeriod, found := suite.keeper.GetUSDXMintingRewardPeriod(suite.ctx, tc.args.ctype) + suite.Require().True(found) + err = suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) + suite.Require().NoError(err) + + err = suite.keeper.ClaimUSDXMintingRewardVVesting(suite.ctx, suite.addrs[2], suite.addrs[0], tc.args.multiplier) + + if tc.errArgs.expectPass { + suite.Require().NoError(err) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[0]) + suite.Require().Equal(tc.args.expectedBalance, acc.GetCoins()) // TODO check balance change to decouple from initialized account balance. + + if tc.args.isPeriodicVestingAccount { + vacc, ok := acc.(*vesting.PeriodicVestingAccount) + suite.Require().True(ok) + suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) + } + + claim, found := suite.keeper.GetUSDXMintingClaim(suite.ctx, suite.addrs[2]) + suite.Require().True(found) + suite.Require().Equal(c("ukava", 0), claim.Reward) + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + } + }) + } +} + func (suite *KeeperTestSuite) TestPayoutHardLiquidityProviderClaim() { type args struct { deposit sdk.Coins @@ -431,6 +581,296 @@ func (suite *KeeperTestSuite) TestPayoutHardLiquidityProviderClaim() { } } +func (suite *KeeperTestSuite) TestPayoutHardLiquidityProviderClaimVVesting() { + type args struct { + deposit sdk.Coins + borrow sdk.Coins + rewardsPerSecond sdk.Coins + initialTime time.Time + multipliers types.Multipliers + multiplier types.MultiplierName + timeElapsed int64 + expectedRewards sdk.Coins + expectedPeriods vesting.Periods + isPeriodicVestingAccount bool + } + type errArgs struct { + expectPass bool + contains string + } + type test struct { + name string + args args + errArgs errArgs + } + testCases := []test{ + { + "single reward denom: valid 1 day", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedRewards: cs(c("hard", 21142771200)), // 10571385600 (deposit reward) + 10571385600 (borrow reward) + expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "single reward denom: valid 10 days", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 864000, + expectedRewards: cs(c("hard", 211427712000)), // 105713856000 (deposit reward) + 105713856000 (borrow reward) + expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712000))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "invalid zero rewards", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 0)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedRewards: cs(c("hard", 0)), + expectedPeriods: vesting.Periods{}, + isPeriodicVestingAccount: false, + }, + errArgs{ + expectPass: false, + contains: "claim amount rounds to zero", + }, + }, + { + "multiple reward denoms: valid 1 day", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedRewards: cs(c("hard", 21142771200), c("ukava", 21142771200)), // 10571385600 (deposit reward) + 10571385600 (borrow reward) + expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200), c("ukava", 21142771200))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "multiple reward denoms: valid 10 days", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 122354)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 864000, + expectedRewards: cs(c("hard", 211427712000), c("ukava", 211427712000)), // 105713856000 (deposit reward) + 105713856000 (borrow reward) + expectedPeriods: vesting.Periods{vesting.Period{Length: 32140800, Amount: cs(c("hard", 211427712000), c("ukava", 211427712000))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "multiple reward denoms with different rewards per second: valid 1 day", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: cs(c("hard", 122354), c("ukava", 222222)), + initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), + multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + multiplier: types.MultiplierName("large"), + timeElapsed: 86400, + expectedRewards: cs(c("hard", 21142771200), c("ukava", 38399961600)), + expectedPeriods: vesting.Periods{vesting.Period{Length: 32918400, Amount: cs(c("hard", 21142771200), c("ukava", 38399961600))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWithGenState() + suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime) + + // setup kavadist state + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("hard", 1000000000000000000), c("ukava", 1000000000000000000))) + suite.Require().NoError(err) + + // Set up generic reward periods + var multiRewardPeriods types.MultiRewardPeriods + var rewardPeriods types.RewardPeriods + for _, coin := range tc.args.deposit { + if len(tc.args.rewardsPerSecond) > 0 { + rewardPeriod := types.NewRewardPeriod(true, coin.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond[0]) + rewardPeriods = append(rewardPeriods, rewardPeriod) + } + multiRewardPeriod := types.NewMultiRewardPeriod(true, coin.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) + multiRewardPeriods = append(multiRewardPeriods, multiRewardPeriod) + } + + // Set up generic reward periods + params := types.NewParams( + rewardPeriods, multiRewardPeriods, multiRewardPeriods, rewardPeriods, + types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))}, + tc.args.initialTime.Add(time.Hour*24*365*5), + ) + suite.keeper.SetParams(suite.ctx, params) + + // Set each denom's previous accrual time and supply reward factor + if len(tc.args.rewardsPerSecond) > 0 { + for _, coin := range tc.args.deposit { + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, coin.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, coin.Denom, rewardIndexes) + } + } + + // Set each denom's previous accrual time and borrow reward factor + if len(tc.args.rewardsPerSecond) > 0 { + for _, coin := range tc.args.borrow { + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, coin.Denom, tc.args.initialTime) + var rewardIndexes types.RewardIndexes + for _, rewardCoin := range tc.args.rewardsPerSecond { + rewardIndex := types.NewRewardIndex(rewardCoin.Denom, sdk.ZeroDec()) + rewardIndexes = append(rewardIndexes, rewardIndex) + } + suite.keeper.SetHardBorrowRewardIndexes(suite.ctx, coin.Denom, rewardIndexes) + } + } + + // sets addrs[3] to be a validator vesting account + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[3]) + bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence()) + bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), suite.ctx.BlockTime().Unix()+16) + suite.Require().NoError(err2) + periods := vesting.Periods{ + vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))}, + vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))}, + } + vva := validatorvesting.NewValidatorVestingAccountRaw(bva, suite.ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90) + ak.SetAccount(suite.ctx, vva) + + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + + // User deposits and borrows + err = hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + err = hardKeeper.Borrow(suite.ctx, userAddr, tc.args.borrow) + suite.Require().NoError(err) + + // Check that Hard hooks initialized a HardLiquidityProviderClaim that has 0 rewards + claim, found := suite.keeper.GetHardLiquidityProviderClaim(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + for _, coin := range tc.args.deposit { + suite.Require().Equal(sdk.ZeroInt(), claim.Reward.AmountOf(coin.Denom)) + } + + // Set up future runtime context + runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.timeElapsed), 0) + runCtx := suite.ctx.WithBlockTime(runAtTime) + + // Run Hard begin blocker + hard.BeginBlocker(runCtx, suite.hardKeeper) + + // Accumulate supply rewards for each deposit denom + for _, coin := range tc.args.deposit { + rewardPeriod, found := suite.keeper.GetHardSupplyRewardPeriods(runCtx, coin.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardSupplyRewards(runCtx, rewardPeriod) + suite.Require().NoError(err) + } + + // Accumulate borrow rewards for each deposit denom + for _, coin := range tc.args.borrow { + rewardPeriod, found := suite.keeper.GetHardBorrowRewardPeriods(runCtx, coin.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardBorrowRewards(runCtx, rewardPeriod) + suite.Require().NoError(err) + } + + // Sync hard supply rewards + deposit, found := suite.hardKeeper.GetDeposit(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + suite.keeper.SynchronizeHardSupplyReward(suite.ctx, deposit) + + // Sync hard borrow rewards + borrow, found := suite.hardKeeper.GetBorrow(suite.ctx, suite.addrs[3]) + suite.Require().True(found) + suite.keeper.SynchronizeHardBorrowReward(suite.ctx, borrow) + + // Fetch pre-claim balances + preClaimAcc := ak.GetAccount(runCtx, suite.addrs[2]) + + err = suite.keeper.ClaimHardRewardVVesting(runCtx, suite.addrs[3], suite.addrs[2], tc.args.multiplier) + if tc.errArgs.expectPass { + suite.Require().NoError(err) + + // Check that user's balance has increased by expected reward amount + postClaimAcc := ak.GetAccount(suite.ctx, suite.addrs[2]) + suite.Require().Equal(preClaimAcc.GetCoins().Add(tc.args.expectedRewards...), postClaimAcc.GetCoins()) + + if tc.args.isPeriodicVestingAccount { + vacc, ok := postClaimAcc.(*vesting.PeriodicVestingAccount) + suite.Require().True(ok) + suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods) + } + + // Check that each claim reward coin's amount has been reset to 0 + claim, found := suite.keeper.GetHardLiquidityProviderClaim(runCtx, suite.addrs[3]) + suite.Require().True(found) + for _, claimRewardCoin := range claim.Reward { + suite.Require().Equal(c(claimRewardCoin.Denom, 0), claimRewardCoin) + } + } else { + suite.Require().Error(err) + suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + } + }) + } +} + func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() { type accountArgs struct { periods vesting.Periods diff --git a/x/incentive/types/codec.go b/x/incentive/types/codec.go index 519fb7b2..c1185f15 100644 --- a/x/incentive/types/codec.go +++ b/x/incentive/types/codec.go @@ -21,4 +21,6 @@ func RegisterCodec(cdc *codec.Codec) { // Register msgs cdc.RegisterConcrete(MsgClaimUSDXMintingReward{}, "incentive/MsgClaimUSDXMintingReward", nil) cdc.RegisterConcrete(MsgClaimHardReward{}, "incentive/MsgClaimHardReward", nil) + cdc.RegisterConcrete(MsgClaimUSDXMintingRewardVVesting{}, "incentive/MsgClaimUSDXRewardVVesting", nil) + cdc.RegisterConcrete(MsgClaimHardRewardVVesting{}, "incentive/MsgClaimHardRewardVVesting", nil) } diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go index c53cb4e8..fd77e7df 100644 --- a/x/incentive/types/msg.go +++ b/x/incentive/types/msg.go @@ -10,6 +10,8 @@ import ( // ensure Msg interface compliance at compile time var _ sdk.Msg = &MsgClaimUSDXMintingReward{} var _ sdk.Msg = &MsgClaimHardReward{} +var _ sdk.Msg = &MsgClaimHardRewardVVesting{} +var _ sdk.Msg = &MsgClaimUSDXMintingRewardVVesting{} // MsgClaimUSDXMintingReward message type used to claim USDX minting rewards type MsgClaimUSDXMintingReward struct { @@ -50,6 +52,52 @@ func (msg MsgClaimUSDXMintingReward) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } +// MsgClaimUSDXMintingRewardVVesting message type used to claim USDX minting rewards for validator vesting accounts +type MsgClaimUSDXMintingRewardVVesting struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` +} + +// NewMsgClaimUSDXMintingRewardVVesting returns a new MsgClaimUSDXMintingReward. +func NewMsgClaimUSDXMintingRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string) MsgClaimUSDXMintingRewardVVesting { + return MsgClaimUSDXMintingRewardVVesting{ + Sender: sender, + Receiver: receiver, + MultiplierName: multiplierName, + } +} + +// Route return the message type used for routing the message. +func (msg MsgClaimUSDXMintingRewardVVesting) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgClaimUSDXMintingRewardVVesting) Type() string { + return "claim_usdx_minting_reward_vvesting" +} + +// ValidateBasic does a simple validation check that doesn't require access to state. +func (msg MsgClaimUSDXMintingRewardVVesting) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if msg.Receiver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") + } + return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgClaimUSDXMintingRewardVVesting) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgClaimUSDXMintingRewardVVesting) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + // MsgClaimHardReward message type used to claim Hard liquidity provider rewards type MsgClaimHardReward struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` @@ -90,3 +138,49 @@ func (msg MsgClaimHardReward) GetSignBytes() []byte { func (msg MsgClaimHardReward) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } + +// 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"` +} + +// NewMsgClaimHardRewardVVesting returns a new MsgClaimHardReward. +func NewMsgClaimHardRewardVVesting(sender, receiver sdk.AccAddress, multiplierName string) MsgClaimHardRewardVVesting { + return MsgClaimHardRewardVVesting{ + Sender: sender, + Receiver: receiver, + MultiplierName: multiplierName, + } +} + +// Route return the message type used for routing the message. +func (msg MsgClaimHardRewardVVesting) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgClaimHardRewardVVesting) Type() string { + return "claim_hard_reward_vvesting" +} + +// ValidateBasic does a simple validation check that doesn't require access to state. +func (msg MsgClaimHardRewardVVesting) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + if msg.Receiver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty") + } + return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgClaimHardRewardVVesting) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgClaimHardRewardVVesting) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +}