From 3a08fc582b26d6a22a7e41f14bcfa962534076b9 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Tue, 26 Jan 2021 12:52:34 +0100 Subject: [PATCH] Incentive PR 4: claim Hard rewards via the Incentive module (#780) * claim hard reward keeper methods * test hard claim payout * claim hard rewards via cli * query hard claims via cli * rest txs and queries * add handler test * add claim type event field --- x/incentive/alias.go | 45 ++++--- x/incentive/client/cli/query.go | 68 ++++++++-- x/incentive/client/cli/tx.go | 44 ++++++- x/incentive/client/rest/query.go | 49 +++++++- x/incentive/client/rest/tx.go | 38 +++++- x/incentive/handler.go | 15 ++- x/incentive/handler_test.go | 36 ++++-- x/incentive/keeper/payout.go | 63 +++++++++- x/incentive/keeper/payout_test.go | 172 +++++++++++++++++++++++++- x/incentive/keeper/querier.go | 40 +++++- x/incentive/keeper/rewards.go | 33 ++++- x/incentive/types/codec.go | 1 + x/incentive/types/events.go | 1 + x/incentive/types/expected_keepers.go | 2 + x/incentive/types/msg.go | 46 ++++++- x/incentive/types/querier.go | 29 ++++- 16 files changed, 610 insertions(+), 72 deletions(-) diff --git a/x/incentive/alias.go b/x/incentive/alias.go index 29f75940..98b8c829 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -23,7 +23,8 @@ const ( ModuleName = types.ModuleName QuerierRoute = types.QuerierRoute QueryGetClaimPeriods = types.QueryGetClaimPeriods - QueryGetClaims = types.QueryGetClaims + QueryGetCdpClaims = types.QueryGetCdpClaims + QueryGetHardClaims = types.QueryGetHardClaims QueryGetParams = types.QueryGetParams QueryGetRewardPeriods = types.QueryGetRewardPeriods RestClaimCollateralType = types.RestClaimCollateralType @@ -35,24 +36,27 @@ const ( 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 - NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward - NewMultiplier = types.NewMultiplier - NewParams = types.NewParams - NewPeriod = types.NewPeriod - NewQueryClaimsParams = types.NewQueryClaimsParams - NewRewardIndex = types.NewRewardIndex - NewRewardPeriod = types.NewRewardPeriod - NewUSDXMintingClaim = types.NewUSDXMintingClaim - ParamKeyTable = types.ParamKeyTable - RegisterCodec = types.RegisterCodec + CalculateTimeElapsed = keeper.CalculateTimeElapsed + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + DefaultGenesisState = types.DefaultGenesisState + DefaultParams = types.DefaultParams + GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength + NewGenesisAccumulationTime = types.NewGenesisAccumulationTime + NewGenesisState = types.NewGenesisState + NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward + NewMsgClaimHardLiquidityProviderReward = types.NewMsgClaimHardLiquidityProviderReward + NewMultiplier = types.NewMultiplier + NewParams = types.NewParams + NewPeriod = types.NewPeriod + NewQueryCdpClaimsParams = types.NewQueryCdpClaimsParams + NewQueryHardClaimsParams = types.NewQueryHardClaimsParams + NewRewardIndex = types.NewRewardIndex + NewRewardPeriod = types.NewRewardPeriod + NewUSDXMintingClaim = types.NewUSDXMintingClaim + NewHardLiquidityProviderClaim = types.NewHardLiquidityProviderClaim + ParamKeyTable = types.ParamKeyTable + RegisterCodec = types.RegisterCodec // variable aliases PreviousUSDXMintingRewardAccrualTimeKeyPrefix = types.PreviousUSDXMintingRewardAccrualTimeKeyPrefix @@ -99,7 +103,8 @@ type ( Multipliers = types.Multipliers Params = types.Params PostClaimReq = types.PostClaimReq - QueryClaimsParams = types.QueryClaimsParams + QueryCdpClaimsParams = types.QueryCdpClaimsParams + QueryHardClaimsParams = types.QueryHardClaimsParams RewardIndex = types.RewardIndex RewardIndexes = types.RewardIndexes RewardPeriod = types.RewardPeriod diff --git a/x/incentive/client/cli/query.go b/x/incentive/client/cli/query.go index fd600a15..93099b68 100644 --- a/x/incentive/client/cli/query.go +++ b/x/incentive/client/cli/query.go @@ -25,7 +25,8 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { incentiveQueryCmd.AddCommand(flags.GetCommands( queryParamsCmd(queryRoute, cdc), - queryClaimsCmd(queryRoute, cdc), + queryCdpClaimsCmd(queryRoute, cdc), + queryHardClaimsCmd(queryRoute, cdc), )...) return incentiveQueryCmd @@ -35,16 +36,16 @@ const ( flagOwner = "owner" ) -func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { +func queryCdpClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "claims ", + Use: "cdp-claims", Short: "query USDX minting claims", Long: strings.TrimSpace( fmt.Sprintf(`Query USDX minting claims with optional flag for finding claims for a specifc owner Example: - $ %s query %s claims - $ %s query %s claims --owner kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw + $ %s query %s cdp-claims + $ %s query %s cdp-claims --owner kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw `, version.ClientName, types.ModuleName, version.ClientName, types.ModuleName)), Args: cobra.NoArgs, @@ -60,14 +61,67 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { if err != nil { return err } - params := types.NewQueryClaimsParams(page, limit, owner) + params := types.NewQueryCdpClaimsParams(page, limit, owner) bz, err := cdc.MarshalJSON(params) if err != nil { return err } // Query - route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims) + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpClaims) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + + var claims types.USDXMintingClaims + if err := cdc.UnmarshalJSON(res, &claims); err != nil { + return fmt.Errorf("failed to unmarshal claims: %w", err) + } + return cliCtx.PrintOutput(claims) + + }, + } + cmd.Flags().String(flagOwner, "", "(optional) filter by claim owner address") + cmd.Flags().Int(flags.FlagPage, 1, "pagination page of CDPs to to query for") + cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of CDPs to query for") + return cmd +} + +func queryHardClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "hard-claims", + Short: "query Hard liquidity provider claims", + Long: strings.TrimSpace( + fmt.Sprintf(`Query Hard liquidity provider claims with optional flag for finding claims for a specifc owner + + Example: + $ %s query %s hard-claims + $ %s query %s hard-claims --owner kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw + `, + version.ClientName, types.ModuleName, version.ClientName, types.ModuleName)), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + strOwner := viper.GetString(flagOwner) + page := viper.GetInt(flags.FlagPage) + limit := viper.GetInt(flags.FlagLimit) + + // Prepare params for querier + owner, err := sdk.AccAddressFromBech32(strOwner) + if err != nil { + return err + } + params := types.NewQueryHardClaimsParams(page, limit, owner) + bz, err := cdc.MarshalJSON(params) + if err != nil { + return err + } + + // Query + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetHardClaims) res, height, err := cliCtx.QueryWithData(route, bz) if err != nil { return err diff --git a/x/incentive/client/cli/tx.go b/x/incentive/client/cli/tx.go index 5a51ea4a..559c01f6 100644 --- a/x/incentive/client/cli/tx.go +++ b/x/incentive/client/cli/tx.go @@ -26,22 +26,23 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { } incentiveTxCmd.AddCommand(flags.PostCommands( - getCmdClaim(cdc), + getCmdClaimCdp(cdc), + getCmdClaimHard(cdc), )...) return incentiveTxCmd } -func getCmdClaim(cdc *codec.Codec) *cobra.Command { +func getCmdClaimCdp(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "claim [owner] [multiplier]", - Short: "claim rewards for cdp owner and collateral-type", + Use: "claim-cdp [owner] [multiplier]", + Short: "claim CDP rewards for cdp owner and collateral-type", Long: strings.TrimSpace( - fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input collateral-type and multiplier, + fmt.Sprintf(`Claim any outstanding CDP rewards owned by owner for the input collateral-type and multiplier, Example: - $ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw large + $ %s tx %s claim-cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw large `, version.ClientName, types.ModuleName), ), Args: cobra.ExactArgs(2), @@ -63,3 +64,34 @@ func getCmdClaim(cdc *codec.Codec) *cobra.Command { }, } } + +func getCmdClaimHard(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "claim-hard [owner] [multiplier]", + Short: "claim Hard rewards for deposit/borrow and delegating", + Long: strings.TrimSpace( + fmt.Sprintf(`Claim owner's outstanding Hard rewards using given multiplier multiplier, + + Example: + $ %s tx %s claim-hard kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw large + `, version.ClientName, types.ModuleName), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + msg := types.NewMsgClaimHardLiquidityProviderReward(owner, args[1]) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/incentive/client/rest/query.go b/x/incentive/client/rest/query.go index 09d14072..ae9ffa92 100644 --- a/x/incentive/client/rest/query.go +++ b/x/incentive/client/rest/query.go @@ -15,11 +15,12 @@ import ( ) func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/cdp-claims", types.ModuleName), queryCdpClaimsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/hard-claims", types.ModuleName), queryHardClaimsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET") } -func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func queryCdpClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) if err != nil { @@ -41,14 +42,54 @@ func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } - queryParams := types.NewQueryClaimsParams(page, limit, owner) + queryParams := types.NewQueryCdpClaimsParams(page, limit, owner) bz, err := cliCtx.Codec.MarshalJSON(queryParams) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) return } - res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetClaims), bz) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetCdpClaims), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryHardClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + var owner sdk.AccAddress + if x := r.URL.Query().Get(types.RestClaimOwner); len(x) != 0 { + ownerStr := strings.ToLower(strings.TrimSpace(x)) + owner, err = sdk.AccAddressFromBech32(ownerStr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from claim owner %s", ownerStr)) + return + } + } + + queryParams := types.NewQueryHardClaimsParams(page, limit, owner) + bz, err := cliCtx.Codec.MarshalJSON(queryParams) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetHardClaims), bz) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/incentive/client/rest/tx.go b/x/incentive/client/rest/tx.go index ef4745fd..b8170e4f 100644 --- a/x/incentive/client/rest/tx.go +++ b/x/incentive/client/rest/tx.go @@ -16,10 +16,11 @@ import ( ) func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc("/incentive/claim", postClaimHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/incentive/claim-cdp", postClaimCdpHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/incentive/claim-hard", postClaimHardHandlerFn(cliCtx)).Methods("POST") } -func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func postClaimCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var requestBody types.PostClaimReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { @@ -51,3 +52,36 @@ func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { 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 types.PostClaimReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if !bytes.Equal(fromAddr, requestBody.Sender) { + rest.WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("expected: %s, got: %s", fromAddr, requestBody.Sender)) + return + } + + msg := types.NewMsgClaimHardLiquidityProviderReward(requestBody.Sender, requestBody.MultiplierName) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/incentive/handler.go b/x/incentive/handler.go index ef2b2344..daf6b940 100644 --- a/x/incentive/handler.go +++ b/x/incentive/handler.go @@ -15,6 +15,8 @@ func NewHandler(k keeper.Keeper) sdk.Handler { switch msg := msg.(type) { case types.MsgClaimUSDXMintingReward: return handleMsgClaimUSDXMintingReward(ctx, k, msg) + case types.MsgClaimHardLiquidityProviderReward: + return handleMsgClaimHardLiquidityProviderReward(ctx, k, msg) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) } @@ -23,7 +25,18 @@ func NewHandler(k keeper.Keeper) sdk.Handler { func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingReward) (*sdk.Result, error) { - err := k.ClaimReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) + err := k.ClaimUSDXMintingReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) + if err != nil { + return nil, err + } + return &sdk.Result{ + Events: ctx.EventManager().Events(), + }, nil +} + +func handleMsgClaimHardLiquidityProviderReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimHardLiquidityProviderReward) (*sdk.Result, error) { + + err := k.ClaimHardReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName)) if err != nil { return nil, err } diff --git a/x/incentive/handler_test.go b/x/incentive/handler_test.go index c51e2fd3..c397a0c1 100644 --- a/x/incentive/handler_test.go +++ b/x/incentive/handler_test.go @@ -63,7 +63,34 @@ func (suite *HandlerTestSuite) SetupTest() { suite.ctx = ctx } -func (suite *HandlerTestSuite) addClaim() { +func (suite *HandlerTestSuite) TestMsgUSDXMintingClaimReward() { + suite.addUSDXMintingClaim() + msg := incentive.NewMsgClaimUSDXMintingReward(suite.addrs[0], "small") + res, err := suite.handler(suite.ctx, msg) + suite.NoError(err) + suite.Require().NotNil(res) +} + +func (suite *HandlerTestSuite) TestMsgHardLiquidityProviderClaimReward() { + suite.addHardLiquidityProviderClaim() + msg := incentive.NewMsgClaimHardLiquidityProviderReward(suite.addrs[0], "small") + res, err := suite.handler(suite.ctx, msg) + suite.NoError(err) + suite.Require().NotNil(res) +} + +func (suite *HandlerTestSuite) addHardLiquidityProviderClaim() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000))) + suite.Require().NoError(err) + rewardPeriod := types.RewardIndexes{types.NewRewardIndex("bnb-s", sdk.ZeroDec())} + c1 := incentive.NewHardLiquidityProviderClaim(suite.addrs[0], c("ukava", 1000000), rewardPeriod, rewardPeriod, rewardPeriod) + suite.NotPanics(func() { + suite.keeper.SetHardLiquidityProviderClaim(suite.ctx, c1) + }) +} + +func (suite *HandlerTestSuite) addUSDXMintingClaim() { sk := suite.app.GetSupplyKeeper() err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000))) suite.Require().NoError(err) @@ -73,13 +100,6 @@ func (suite *HandlerTestSuite) addClaim() { }) } -func (suite *HandlerTestSuite) TestMsgClaimReward() { - suite.addClaim() - msg := incentive.NewMsgClaimUSDXMintingReward(suite.addrs[0], "small") - res, err := suite.handler(suite.ctx, msg) - suite.NoError(err) - suite.Require().NotNil(res) -} func TestHandlerTestSuite(t *testing.T) { suite.Run(t, new(HandlerTestSuite)) } diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index b0067c50..c930c703 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -12,8 +12,8 @@ import ( validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) -// ClaimReward sends the reward amount to the input address and zero's out the claim in the store -func (k Keeper) ClaimReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { +// ClaimUSDXMintingReward sends the reward amount to the input address and zero's out the claim in the store +func (k Keeper) ClaimUSDXMintingReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { claim, found := k.GetUSDXMintingClaim(ctx, addr) if !found { return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) @@ -30,7 +30,7 @@ func (k Keeper) ClaimReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) } - claim, err := k.SynchronizeClaim(ctx, claim) + claim, err := k.SynchronizeUSDXMintingClaim(ctx, claim) if err != nil { return err } @@ -47,13 +47,64 @@ func (k Keeper) ClaimReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName return err } - k.ZeroClaim(ctx, claim) + k.ZeroUSDXMintingClaim(ctx, claim) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeyClaimedBy, addr.String()), - sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()), + sdk.NewAttribute(types.AttributeKeyClaimedBy, claim.GetOwner().String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetReward().String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.GetType()), + ), + ) + return nil +} + +// ClaimHardReward sends the reward amount to the input address and zero's out the claim in the store +func (k Keeper) ClaimHardReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error { + _, found := k.GetHardLiquidityProviderClaim(ctx, addr) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) + } + + multiplier, found := k.GetMultiplier(ctx, multiplierName) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) + } + + claimEnd := k.GetClaimEnd(ctx) + + if ctx.BlockTime().After(claimEnd) { + return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd) + } + + k.SynchronizeHardLiquidityProviderClaim(ctx, addr) + + claim, found := k.GetHardLiquidityProviderClaim(ctx, addr) + if !found { + return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr) + } + + rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt() + if rewardAmount.IsZero() { + return types.ErrZeroClaim + } + rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) + length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix() + + err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), 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 diff --git a/x/incentive/keeper/payout_test.go b/x/incentive/keeper/payout_test.go index c3a81356..868ba682 100644 --- a/x/incentive/keeper/payout_test.go +++ b/x/incentive/keeper/payout_test.go @@ -14,12 +14,13 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/kava-labs/kava/app" cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/hard" "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/kavadist" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) -func (suite *KeeperTestSuite) TestPayoutClaim() { +func (suite *KeeperTestSuite) TestPayoutUSDXMintingClaim() { type args struct { ctype string rewardsPerSecond sdk.Coin @@ -129,7 +130,7 @@ func (suite *KeeperTestSuite) TestPayoutClaim() { err = suite.keeper.AccumulateUSDXMintingRewards(suite.ctx, rewardPeriod) suite.Require().NoError(err) - err = suite.keeper.ClaimReward(suite.ctx, suite.addrs[0], tc.args.multiplier) + err = suite.keeper.ClaimUSDXMintingReward(suite.ctx, suite.addrs[0], tc.args.multiplier) if tc.errArgs.expectPass { suite.Require().NoError(err) @@ -155,6 +156,173 @@ func (suite *KeeperTestSuite) TestPayoutClaim() { } } +func (suite *KeeperTestSuite) TestPayoutHardLiquidityProviderClaim() { + type args struct { + deposit sdk.Coins + borrow sdk.Coins + rewardsPerSecond sdk.Coin + initialTime time.Time + multipliers types.Multipliers + multiplier types.MultiplierName + timeElapsed int64 + expectedReward sdk.Coin + 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{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: 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, + expectedReward: c("hard", 21142771200), // 10571385600 (deposit reward) + 10571385600 (borrow reward) + expectedPeriods: vesting.Periods{vesting.Period{Length: 31536000, Amount: cs(c("hard", 21142771200))}}, + isPeriodicVestingAccount: true, + }, + errArgs{ + expectPass: true, + contains: "", + }, + }, + { + "invalid zero rewards", + args{ + deposit: cs(c("bnb", 10000000000)), + borrow: cs(c("bnb", 5000000000)), + rewardsPerSecond: 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, + expectedReward: sdk.Coin{}, + 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 kavadist state + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("hard", 1000000000000))) + suite.Require().NoError(err) + + // Set up generic reward periods + var rewardPeriods types.RewardPeriods + for _, coin := range tc.args.deposit { + rewardPeriod := types.NewRewardPeriod(true, coin.Denom, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond) + rewardPeriods = append(rewardPeriods, rewardPeriod) + } + + // Set up incentive state + params := types.NewParams( + rewardPeriods, rewardPeriods, rewardPeriods, rewardPeriods, + tc.args.multipliers, + 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 + for _, coin := range tc.args.deposit { + suite.keeper.SetPreviousHardSupplyRewardAccrualTime(suite.ctx, coin.Denom, tc.args.initialTime) + suite.keeper.SetHardSupplyRewardFactor(suite.ctx, coin.Denom, sdk.ZeroDec()) + } + for _, coin := range tc.args.borrow { + suite.keeper.SetPreviousHardBorrowRewardAccrualTime(suite.ctx, coin.Denom, tc.args.initialTime) + suite.keeper.SetHardBorrowRewardFactor(suite.ctx, coin.Denom, sdk.ZeroDec()) + } + + hardKeeper := suite.app.GetHardKeeper() + userAddr := suite.addrs[3] + + // User deposits + err = hardKeeper.Deposit(suite.ctx, userAddr, tc.args.deposit) + suite.Require().NoError(err) + + // User borrows + 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) + suite.Require().Equal(sdk.ZeroInt(), claim.Reward.Amount) + + // 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.GetHardSupplyRewardPeriod(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.GetHardBorrowRewardPeriod(runCtx, coin.Denom) + suite.Require().True(found) + err = suite.keeper.AccumulateHardBorrowRewards(runCtx, rewardPeriod) + suite.Require().NoError(err) + } + + // Fetch pre-claim balances + ak := suite.app.GetAccountKeeper() + preClaimAcc := ak.GetAccount(runCtx, suite.addrs[3]) + + err = suite.keeper.ClaimHardReward(runCtx, suite.addrs[3], 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[3]) + suite.Require().Equal(preClaimAcc.GetCoins().Add(tc.args.expectedReward), 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 the claim's reward amount has been reset to 0 + claim, found := suite.keeper.GetHardLiquidityProviderClaim(runCtx, suite.addrs[3]) + suite.Require().True(found) + suite.Require().Equal(c("hard", 0), claim.Reward) + } 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/keeper/querier.go b/x/incentive/keeper/querier.go index c0066d18..73cc999e 100644 --- a/x/incentive/keeper/querier.go +++ b/x/incentive/keeper/querier.go @@ -17,8 +17,10 @@ func NewQuerier(k Keeper) sdk.Querier { switch path[0] { case types.QueryGetParams: return queryGetParams(ctx, req, k) - case types.QueryGetClaims: - return queryGetClaims(ctx, req, k) + case types.QueryGetCdpClaims: + return queryGetCdpClaims(ctx, req, k) + case types.QueryGetHardClaims: + return queryGetHardClaims(ctx, req, k) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) } @@ -38,8 +40,8 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e return bz, nil } -func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { - var requestParams types.QueryClaimsParams +func queryGetCdpClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { + var requestParams types.QueryCdpClaimsParams err := k.cdc.UnmarshalJSON(req.Data, &requestParams) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) @@ -67,3 +69,33 @@ func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e } return bz, nil } + +func queryGetHardClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { + var requestParams types.QueryHardClaimsParams + err := k.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + var claims types.HardLiquidityProviderClaims + if len(requestParams.Owner) > 0 { + claim, _ := k.GetHardLiquidityProviderClaim(ctx, requestParams.Owner) + claims = append(claims, claim) + } else { + claims = k.GetAllHardLiquidityProviderClaims(ctx) + } + + var paginatedClaims types.HardLiquidityProviderClaims + + start, end := client.Paginate(len(claims), requestParams.Page, requestParams.Limit, 100) + if start < 0 || end < 0 { + paginatedClaims = types.HardLiquidityProviderClaims{} + } else { + paginatedClaims = claims[start:end] + } + + bz, err := codec.MarshalJSONIndent(k.cdc, paginatedClaims) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index 1750e1dd..f8280efb 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -518,16 +518,16 @@ func (k Keeper) InitializeHardDelegatorReward(ctx sdk.Context, delegator sdk.Acc k.SetHardLiquidityProviderClaim(ctx, claim) } -// ZeroClaim zeroes out the claim object's rewards and returns the updated claim object -func (k Keeper) ZeroClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { +// ZeroUSDXMintingClaim zeroes out the claim object's rewards and returns the updated claim object +func (k Keeper) ZeroUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim { claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt()) k.SetUSDXMintingClaim(ctx, claim) return claim } -// SynchronizeClaim updates the claim object by adding any rewards that have accumulated. +// SynchronizeUSDXMintingClaim updates the claim object by adding any rewards that have accumulated. // Returns the updated claim object -func (k Keeper) SynchronizeClaim(ctx sdk.Context, claim types.USDXMintingClaim) (types.USDXMintingClaim, error) { +func (k Keeper) SynchronizeUSDXMintingClaim(ctx sdk.Context, claim types.USDXMintingClaim) (types.USDXMintingClaim, error) { for _, ri := range claim.RewardIndexes { cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.Owner, ri.CollateralType) if !found { @@ -546,6 +546,31 @@ func (k Keeper) synchronizeRewardAndReturnClaim(ctx sdk.Context, cdp cdptypes.CD return claim } +// SynchronizeHardLiquidityProviderClaim adds any accumulated rewards +func (k Keeper) SynchronizeHardLiquidityProviderClaim(ctx sdk.Context, owner sdk.AccAddress) { + // Synchronize any hard liquidity supply-side rewards + deposit, foundDeposit := k.hardKeeper.GetDeposit(ctx, owner) + if foundDeposit { + k.SynchronizeHardSupplyReward(ctx, deposit) + } + + // Synchronize any hard liquidity borrow-side rewards + borrow, foundBorrow := k.hardKeeper.GetBorrow(ctx, owner) + if foundBorrow { + k.SynchronizeHardBorrowReward(ctx, borrow) + } + + // Synchronize any hard delegator rewards + k.SynchronizeHardDelegatorRewards(ctx, owner) +} + +// ZeroHardLiquidityProviderClaim zeroes out the claim object's rewards and returns the updated claim object +func (k Keeper) ZeroHardLiquidityProviderClaim(ctx sdk.Context, claim types.HardLiquidityProviderClaim) types.HardLiquidityProviderClaim { + claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt()) + k.SetHardLiquidityProviderClaim(ctx, claim) + return claim +} + // CalculateTimeElapsed calculates the number of reward-eligible seconds that have passed since the previous // time rewards were accrued, taking into account the end time of the reward period func CalculateTimeElapsed(rewardPeriod types.RewardPeriod, blockTime time.Time, previousAccrualTime time.Time) sdk.Int { diff --git a/x/incentive/types/codec.go b/x/incentive/types/codec.go index 8758ee8e..5b8d2f8d 100644 --- a/x/incentive/types/codec.go +++ b/x/incentive/types/codec.go @@ -20,4 +20,5 @@ func RegisterCodec(cdc *codec.Codec) { // Register msgs cdc.RegisterConcrete(MsgClaimUSDXMintingReward{}, "incentive/MsgClaimUSDXMintingReward", nil) + cdc.RegisterConcrete(MsgClaimHardLiquidityProviderReward{}, "incentive/MsgClaimHardLiquidityProviderReward", nil) } diff --git a/x/incentive/types/events.go b/x/incentive/types/events.go index e431617a..5255d1c4 100644 --- a/x/incentive/types/events.go +++ b/x/incentive/types/events.go @@ -10,6 +10,7 @@ const ( AttributeValueCategory = ModuleName AttributeKeyClaimedBy = "claimed_by" AttributeKeyClaimAmount = "claim_amount" + AttributeKeyClaimType = "claim_type" AttributeKeyRewardPeriod = "reward_period" AttributeKeyClaimPeriod = "claim_period" ) diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index 46542435..6725e706 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -33,6 +33,8 @@ type CdpKeeper interface { // HardKeeper defines the expected hard keeper for interacting with Hard protocol type HardKeeper interface { + GetDeposit(ctx sdk.Context, depositor sdk.AccAddress) (hardtypes.Deposit, bool) + GetBorrow(ctx sdk.Context, borrower sdk.AccAddress) (hardtypes.Borrow, bool) GetSupplyInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) GetBorrowInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) GetBorrowedCoins(ctx sdk.Context) (coins sdk.Coins, found bool) diff --git a/x/incentive/types/msg.go b/x/incentive/types/msg.go index 8d253a2a..14e43283 100644 --- a/x/incentive/types/msg.go +++ b/x/incentive/types/msg.go @@ -9,8 +9,9 @@ import ( // ensure Msg interface compliance at compile time var _ sdk.Msg = &MsgClaimUSDXMintingReward{} +var _ sdk.Msg = &MsgClaimHardLiquidityProviderReward{} -// MsgClaimUSDXMintingReward message type used to claim rewards +// MsgClaimUSDXMintingReward message type used to claim USDX minting rewards type MsgClaimUSDXMintingReward struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` @@ -28,7 +29,7 @@ func NewMsgClaimUSDXMintingReward(sender sdk.AccAddress, multiplierName string) func (msg MsgClaimUSDXMintingReward) Route() string { return RouterKey } // Type returns a human-readable string for the message, intended for utilization within tags. -func (msg MsgClaimUSDXMintingReward) Type() string { return "claim_reward" } +func (msg MsgClaimUSDXMintingReward) Type() string { return "claim_usdx_minting_reward" } // ValidateBasic does a simple validation check that doesn't require access to state. func (msg MsgClaimUSDXMintingReward) ValidateBasic() error { @@ -48,3 +49,44 @@ func (msg MsgClaimUSDXMintingReward) GetSignBytes() []byte { func (msg MsgClaimUSDXMintingReward) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } + +// MsgClaimHardLiquidityProviderReward message type used to claim Hard liquidity provider rewards +type MsgClaimHardLiquidityProviderReward struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` +} + +// NewMsgClaimHardLiquidityProviderReward returns a new MsgClaimHardLiquidityProviderReward. +func NewMsgClaimHardLiquidityProviderReward(sender sdk.AccAddress, multiplierName string) MsgClaimHardLiquidityProviderReward { + return MsgClaimHardLiquidityProviderReward{ + Sender: sender, + MultiplierName: multiplierName, + } +} + +// Route return the message type used for routing the message. +func (msg MsgClaimHardLiquidityProviderReward) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgClaimHardLiquidityProviderReward) Type() string { + return "claim_hard_liquidity_provider_reward" +} + +// ValidateBasic does a simple validation check that doesn't require access to state. +func (msg MsgClaimHardLiquidityProviderReward) ValidateBasic() error { + if msg.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") + } + return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid() +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgClaimHardLiquidityProviderReward) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgClaimHardLiquidityProviderReward) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} diff --git a/x/incentive/types/querier.go b/x/incentive/types/querier.go index d13c1027..8e401027 100644 --- a/x/incentive/types/querier.go +++ b/x/incentive/types/querier.go @@ -7,7 +7,8 @@ import ( // Querier routes for the incentive module const ( - QueryGetClaims = "claims" + QueryGetCdpClaims = "cdp-claims" + QueryGetHardClaims = "hard-claims" RestClaimOwner = "owner" RestClaimCollateralType = "collateral_type" QueryGetParams = "parameters" @@ -15,16 +16,32 @@ const ( QueryGetClaimPeriods = "claim-periods" ) -// QueryClaimsParams params for query /incentive/claims -type QueryClaimsParams struct { +// QueryCdpClaimsParams params for query /incentive/claims +type QueryCdpClaimsParams struct { Page int `json:"page" yaml:"page"` Limit int `json:"limit" yaml:"limit"` Owner sdk.AccAddress } -// NewQueryClaimsParams returns QueryClaimsParams -func NewQueryClaimsParams(page, limit int, owner sdk.AccAddress) QueryClaimsParams { - return QueryClaimsParams{ +// NewQueryCdpClaimsParams returns QueryCdpClaimsParams +func NewQueryCdpClaimsParams(page, limit int, owner sdk.AccAddress) QueryCdpClaimsParams { + return QueryCdpClaimsParams{ + Page: page, + Limit: limit, + Owner: owner, + } +} + +// QueryHardClaimsParams params for query /incentive/claims +type QueryHardClaimsParams struct { + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` + Owner sdk.AccAddress +} + +// NewQueryHardClaimsParams returns QueryHardClaimsParams +func NewQueryHardClaimsParams(page, limit int, owner sdk.AccAddress) QueryHardClaimsParams { + return QueryHardClaimsParams{ Page: page, Limit: limit, Owner: owner,