From 719fa73c588c65d5710a98907ee47ecb6cdf289e Mon Sep 17 00:00:00 2001 From: yili_Green Date: Mon, 13 Apr 2020 21:20:13 +0800 Subject: [PATCH 1/2] add the community pool in x/distribution --- .gitignore | 3 +- app/genesis/genesis.json | 4 + app/protocol/protocol_v0.go | 5 +- x/distribution/alias.go | 5 ++ x/distribution/client/cli/query.go | 31 ++++++++ x/distribution/client/cli/tx.go | 64 ++++++++++++++- x/distribution/client/cli/utils.go | 35 +++++++++ x/distribution/client/common/common.go | 11 ++- x/distribution/client/common/pretty_params.go | 8 +- x/distribution/client/proposal_handler.go | 12 +++ x/distribution/client/rest/query.go | 32 ++++++++ x/distribution/client/rest/rest.go | 41 +++++++++- x/distribution/client/rest/utils.go | 21 +++++ x/distribution/genesis.go | 12 ++- x/distribution/genesis_test.go | 6 +- x/distribution/handler.go | 14 ++++ x/distribution/handler_test.go | 7 +- x/distribution/keeper/allocation.go | 77 ++++++++----------- x/distribution/keeper/allocation_test.go | 53 ++++++------- x/distribution/keeper/hooks.go | 13 ++-- x/distribution/keeper/invariants.go | 10 +-- x/distribution/keeper/key.go | 2 + x/distribution/keeper/params.go | 15 ++++ x/distribution/keeper/proposal_handler.go | 47 +++++++++++ x/distribution/keeper/querier.go | 17 ++++ x/distribution/keeper/store.go | 23 ++++++ x/distribution/keeper/test_common.go | 9 ++- x/distribution/proposal_handler_test.go | 67 ++++++++++++++++ x/distribution/types/codec.go | 1 + x/distribution/types/errors.go | 9 +++ x/distribution/types/fee_pool.go | 28 +++++++ x/distribution/types/fee_pool_test.go | 19 +++++ x/distribution/types/genesis.go | 17 +++- x/distribution/types/proposal.go | 74 ++++++++++++++++++ x/distribution/types/querier.go | 2 + 35 files changed, 682 insertions(+), 112 deletions(-) create mode 100644 x/distribution/client/cli/utils.go create mode 100644 x/distribution/client/proposal_handler.go create mode 100644 x/distribution/client/rest/utils.go create mode 100644 x/distribution/keeper/proposal_handler.go create mode 100644 x/distribution/proposal_handler_test.go create mode 100644 x/distribution/types/fee_pool.go create mode 100644 x/distribution/types/fee_pool_test.go create mode 100644 x/distribution/types/proposal.go diff --git a/.gitignore b/.gitignore index 461b5aa192..f115e2cdbd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Build vendor build +tools/ tools/bin/* examples/build/* docs/_build @@ -66,4 +67,4 @@ dependency-graph.png contract_tests/* dev/cache/* go.sum -result.txt \ No newline at end of file +result.txt diff --git a/app/genesis/genesis.json b/app/genesis/genesis.json index f8b3d06f6c..1d4411148c 100644 --- a/app/genesis/genesis.json +++ b/app/genesis/genesis.json @@ -84,7 +84,11 @@ "withdraw_infos": null }, "distribution": { + "community_tax": "0.02000000", "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, "previous_proposer": "", "validator_accumulated_commissions": [], "withdraw_addr_enabled": true diff --git a/app/protocol/protocol_v0.go b/app/protocol/protocol_v0.go index b870abd0e2..2ce639e0eb 100644 --- a/app/protocol/protocol_v0.go +++ b/app/protocol/protocol_v0.go @@ -62,7 +62,7 @@ var ( distr.AppModuleBasic{}, gov.NewAppModuleBasic( upgradeClient.ProposalHandler, paramsclient.ProposalHandler, - dexClient.DelistProposalHandler, + dexClient.DelistProposalHandler, distr.ProposalHandler, ), params.AppModuleBasic{}, crisis.AppModuleBasic{}, @@ -313,7 +313,8 @@ func (p *ProtocolV0) produceKeepers() { govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(&p.paramsKeeper)). AddRoute(dex.RouterKey, dex.NewProposalHandler(&p.dexKeeper)). - AddRoute(upgrade.RouterKey, upgrade.NewAppUpgradeProposalHandler(&p.upgradeKeeper)) + AddRoute(upgrade.RouterKey, upgrade.NewAppUpgradeProposalHandler(&p.upgradeKeeper)). + AddRoute(distr.RouterKey,distr.NewCommunityPoolSpendProposalHandler(p.distrKeeper)) govProposalHandlerRouter := keeper.NewProposalHandlerRouter() govProposalHandlerRouter.AddRoute(params.RouterKey, &p.paramsKeeper). AddRoute(dex.RouterKey, &p.dexKeeper). diff --git a/x/distribution/alias.go b/x/distribution/alias.go index 82ba853cb4..90e93d2f3d 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -7,6 +7,7 @@ package distribution import ( + "github.com/okex/okchain/x/distribution/client" "github.com/okex/okchain/x/distribution/keeper" "github.com/okex/okchain/x/distribution/types" ) @@ -43,6 +44,7 @@ var ( ErrNilValidatorAddr = types.ErrNilValidatorAddr ErrNoValidatorCommission = types.ErrNoValidatorCommission ErrSetWithdrawAddrDisabled = types.ErrSetWithdrawAddrDisabled + InitialFeePool = types.InitialFeePool NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState ValidateGenesis = types.ValidateGenesis @@ -53,9 +55,11 @@ var ( InitialValidatorAccumulatedCommission = types.InitialValidatorAccumulatedCommission // variable aliases + FeePoolKey = keeper.FeePoolKey ProposerKey = keeper.ProposerKey DelegatorWithdrawAddrPrefix = keeper.DelegatorWithdrawAddrPrefix ValidatorAccumulatedCommissionPrefix = keeper.ValidatorAccumulatedCommissionPrefix + ParamStoreKeyCommunityTax = keeper.ParamStoreKeyCommunityTax ParamStoreKeyWithdrawAddrEnabled = keeper.ParamStoreKeyWithdrawAddrEnabled ModuleCdc = types.ModuleCdc EventTypeSetWithdrawAddress = types.EventTypeSetWithdrawAddress @@ -65,6 +69,7 @@ var ( AttributeKeyWithdrawAddress = types.AttributeKeyWithdrawAddress AttributeKeyValidator = types.AttributeKeyValidator AttributeValueCategory = types.AttributeValueCategory + ProposalHandler = client.ProposalHandler ) type ( diff --git a/x/distribution/client/cli/query.go b/x/distribution/client/cli/query.go index 241199ed01..0f84b24ecc 100644 --- a/x/distribution/client/cli/query.go +++ b/x/distribution/client/cli/query.go @@ -29,6 +29,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { distQueryCmd.AddCommand(client.GetCommands( GetCmdQueryParams(queryRoute, cdc), GetCmdQueryValidatorCommission(queryRoute, cdc), + GetCmdQueryCommunityPool(queryRoute, cdc), )...) return distQueryCmd @@ -92,3 +93,33 @@ $ %s query distr commission okchainvaloper1alq9na49n9yycysh889rl90g9nhe58lcs50wu }, } } + +// GetCmdQueryCommunityPool returns the command for fetching community pool info +func GetCmdQueryCommunityPool(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "community-pool", + Args: cobra.NoArgs, + Short: "Query the amount of coins in the community pool", + Long: strings.TrimSpace( + fmt.Sprintf(`Query all coins in the community pool which is under Governance control. + +Example: +$ %s query distr community-pool +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/community_pool", queryRoute), nil) + if err != nil { + return err + } + + var result sdk.DecCoins + cdc.MustUnmarshalJSON(res, &result) + return cliCtx.PrintOutput(result) + }, + } +} diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index ca977b6a88..d48618c6e8 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -12,9 +12,9 @@ import ( "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" - "github.com/spf13/cobra" - "github.com/okex/okchain/x/distribution/types" + "github.com/okex/okchain/x/gov" + "github.com/spf13/cobra" ) // GetTxCmd returns the transaction commands for this module @@ -98,3 +98,63 @@ $ %s tx distr withdraw-rewards okchainvaloper1alq9na49n9yycysh889rl90g9nhe58lcs5 } return cmd } + +// GetCmdSubmitProposal implements the command to submit a community-pool-spend proposal +func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "community-pool-spend [proposal-file]", + Args: cobra.ExactArgs(1), + Short: "Submit a community pool spend proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a community pool spend proposal along with an initial deposit. +The proposal details must be supplied via a JSON file. + +Example: +$ %s tx gov submit-proposal community-pool-spend --from= + +Where proposal.json contains: + +{ + "title": "Community Pool Spend", + "description": "Pay me some Atoms!", + "recipient": "okchain5afhd6gxevu37mkqcvvsj8qeylhn0rz46zdlq", + "amount": [ + { + "denom": "stake", + "amount": "10000" + } + ], + "deposit": [ + { + "denom": "stake", + "amount": "10000" + } + ] +} +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + proposal, err := ParseCommunityPoolSpendProposalJSON(cdc, args[0]) + if err != nil { + return err + } + + from := cliCtx.GetFromAddress() + content := types.NewCommunityPoolSpendProposal(proposal.Title, proposal.Description, proposal.Recipient, proposal.Amount) + + msg := gov.NewMsgSubmitProposal(content, proposal.Deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return cmd +} diff --git a/x/distribution/client/cli/utils.go b/x/distribution/client/cli/utils.go new file mode 100644 index 0000000000..db0f4e600c --- /dev/null +++ b/x/distribution/client/cli/utils.go @@ -0,0 +1,35 @@ +package cli + +import ( + "io/ioutil" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ( + // CommunityPoolSpendProposalJSON defines a CommunityPoolSpendProposal with a deposit + CommunityPoolSpendProposalJSON struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` + Amount sdk.DecCoins `json:"amount" yaml:"amount"` + Deposit sdk.DecCoins `json:"deposit" yaml:"deposit"` + } +) + +// ParseCommunityPoolSpendProposalJSON reads and parses a CommunityPoolSpendProposalJSON from a file. +func ParseCommunityPoolSpendProposalJSON(cdc *codec.Codec, proposalFile string) (CommunityPoolSpendProposalJSON, error) { + proposal := CommunityPoolSpendProposalJSON{} + + contents, err := ioutil.ReadFile(proposalFile) + if err != nil { + return proposal, err + } + + if err := cdc.UnmarshalJSON(contents, &proposal); err != nil { + return proposal, err + } + + return proposal, nil +} diff --git a/x/distribution/client/common/common.go b/x/distribution/client/common/common.go index 6e8e735e83..8b460adda1 100644 --- a/x/distribution/client/common/common.go +++ b/x/distribution/client/common/common.go @@ -10,13 +10,20 @@ import ( // QueryParams actually queries distribution params. func QueryParams(cliCtx context.CLIContext, queryRoute string) (PrettyParams, error) { - route := fmt.Sprintf("custom/%s/params/%s", queryRoute, types.ParamWithdrawAddrEnabled) + route := fmt.Sprintf("custom/%s/params/%s", queryRoute, types.ParamCommunityTax) + + retCommunityTax, _, err := cliCtx.QueryWithData(route, []byte{}) + if err != nil { + return PrettyParams{}, err + } + + route = fmt.Sprintf("custom/%s/params/%s", queryRoute, types.ParamWithdrawAddrEnabled) retWithdrawAddrEnabled, _, err := cliCtx.QueryWithData(route, []byte{}) if err != nil { return PrettyParams{}, err } - return newPrettyParams(retWithdrawAddrEnabled), nil + return newPrettyParams(retCommunityTax, retWithdrawAddrEnabled), nil } // QueryValidatorCommission returns a validator's commission. diff --git a/x/distribution/client/common/pretty_params.go b/x/distribution/client/common/pretty_params.go index 49b108a200..d359ed313a 100644 --- a/x/distribution/client/common/pretty_params.go +++ b/x/distribution/client/common/pretty_params.go @@ -7,12 +7,14 @@ import ( // PrettyParams is the struct for CLI output type PrettyParams struct { + CommunityTax json.RawMessage `json:"community_tax"` WithdrawAddrEnabled json.RawMessage `json:"withdraw_addr_enabled"` } // newPrettyParams creates a new PrettyParams -func newPrettyParams(withdrawAddrEnabled json.RawMessage) PrettyParams { +func newPrettyParams(communityTax, withdrawAddrEnabled json.RawMessage) PrettyParams { return PrettyParams{ + CommunityTax: communityTax, WithdrawAddrEnabled: withdrawAddrEnabled, } } @@ -20,5 +22,7 @@ func newPrettyParams(withdrawAddrEnabled json.RawMessage) PrettyParams { // String returns the params string func (pp PrettyParams) String() string { return fmt.Sprintf(`Distribution Params: - Withdraw Addr Enabled: %s`, pp.WithdrawAddrEnabled) + Community Tax: %s + Withdraw Addr Enabled: %s`, + pp.CommunityTax, pp.WithdrawAddrEnabled) } diff --git a/x/distribution/client/proposal_handler.go b/x/distribution/client/proposal_handler.go new file mode 100644 index 0000000000..03c1ef20a8 --- /dev/null +++ b/x/distribution/client/proposal_handler.go @@ -0,0 +1,12 @@ +package client + +import ( + "github.com/okex/okchain/x/distribution/client/cli" + "github.com/okex/okchain/x/distribution/client/rest" + govclient "github.com/okex/okchain/x/gov/client" +) + +// param change proposal handler +var ( + ProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitProposal, rest.ProposalRESTHandler) +) diff --git a/x/distribution/client/rest/query.go b/x/distribution/client/rest/query.go index b4d431bdd4..5cb4def81d 100644 --- a/x/distribution/client/rest/query.go +++ b/x/distribution/client/rest/query.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/gorilla/mux" "github.com/okex/okchain/x/distribution/client/common" @@ -31,6 +32,12 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, queryRoute st "/distribution/parameters", paramsHandlerFn(cliCtx, queryRoute), ).Methods("GET") + + // Get the amount held in the community pool + r.HandleFunc( + "/distribution/community_pool", + communityPoolHandler(cliCtx, queryRoute), + ).Methods("GET") } // HTTP request handler to query a delegation rewards @@ -76,6 +83,31 @@ func paramsHandlerFn(cliCtx context.CLIContext, queryRoute string) http.HandlerF } } +// HTTP request handler to query the community pool coins +func communityPoolHandler(cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/community_pool", queryRoute), nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + var result sdk.DecCoins + if err := cliCtx.Codec.UnmarshalJSON(res, &result); err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, result) + } +} + // HTTP request handler to query the accumulated commission of one single validator func accumulatedCommissionHandlerFn(cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 3883c2cb52..63a6ff4954 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -1,9 +1,16 @@ package rest import ( - "github.com/gorilla/mux" + "net/http" "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/gorilla/mux" + "github.com/okex/okchain/x/distribution/types" + "github.com/okex/okchain/x/gov" + govrest "github.com/okex/okchain/x/gov/client/rest" ) // RegisterRoutes register distribution REST routes. @@ -11,3 +18,35 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, queryRoute string) registerQueryRoutes(cliCtx, r, queryRoute) registerTxRoutes(cliCtx, r, queryRoute) } + +// ProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. +func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "community_pool_spend", + Handler: postProposalHandlerFn(cliCtx), + } +} + +func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req CommunityPoolSpendProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + content := types.NewCommunityPoolSpendProposal(req.Title, req.Description, req.Recipient, req.Amount) + + msg := gov.NewMsgSubmitProposal(content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} \ No newline at end of file diff --git a/x/distribution/client/rest/utils.go b/x/distribution/client/rest/utils.go new file mode 100644 index 0000000000..bc332e69c0 --- /dev/null +++ b/x/distribution/client/rest/utils.go @@ -0,0 +1,21 @@ +package rest + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" +) + +type ( + // CommunityPoolSpendProposalReq defines a community pool spend proposal request body. + CommunityPoolSpendProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.DecCoins `json:"deposit" yaml:"deposit"` + } +) + diff --git a/x/distribution/genesis.go b/x/distribution/genesis.go index 064b2cbbc8..892573d451 100644 --- a/x/distribution/genesis.go +++ b/x/distribution/genesis.go @@ -9,26 +9,28 @@ import ( // InitGenesis sets distribution information for genesis func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, data types.GenesisState) { - moduleHoldings := sdk.DecCoins{} + keeper.SetFeePool(ctx, data.FeePool) + keeper.SetCommunityTax(ctx, data.CommunityTax) keeper.SetPreviousProposerConsAddr(ctx, data.PreviousProposer) keeper.SetWithdrawAddrEnabled(ctx, data.WithdrawAddrEnabled) for _, dwi := range data.DelegatorWithdrawInfos { keeper.SetDelegatorWithdrawAddr(ctx, dwi.DelegatorAddress, dwi.WithdrawAddress) } + moduleHoldings := sdk.DecCoins{} for _, acc := range data.ValidatorAccumulatedCommissions { keeper.SetValidatorAccumulatedCommission(ctx, acc.ValidatorAddress, acc.Accumulated) moduleHoldings = moduleHoldings.Add(acc.Accumulated) } + moduleHoldings = moduleHoldings.Add(data.FeePool.CommunityPool) // check if the module account exists moduleAcc := keeper.GetDistributionAccount(ctx) if moduleAcc == nil { panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) } - moduleHoldingsInt, _ := moduleHoldings.TruncateDecimal() if moduleAcc.GetCoins().IsZero() { - if err := moduleAcc.SetCoins(moduleHoldingsInt); err != nil { + if err := moduleAcc.SetCoins(moduleHoldings); err != nil { panic(err) } supplyKeeper.SetModuleAccount(ctx, moduleAcc) @@ -37,6 +39,8 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper // ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) types.GenesisState { + feePool := keeper.GetFeePool(ctx) + communityTax := keeper.GetCommunityTax(ctx) withdrawAddrEnabled := keeper.GetWithdrawAddrEnabled(ctx) dwi := make([]types.DelegatorWithdrawInfo, 0) keeper.IterateDelegatorWithdrawAddrs(ctx, func(del sdk.AccAddress, addr sdk.AccAddress) (stop bool) { @@ -58,5 +62,5 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) types.GenesisState { }, ) - return types.NewGenesisState(withdrawAddrEnabled, dwi, pp, acc) + return types.NewGenesisState(feePool, communityTax, withdrawAddrEnabled, dwi, pp, acc) } diff --git a/x/distribution/genesis_test.go b/x/distribution/genesis_test.go index d097c3496f..f133b49bb7 100644 --- a/x/distribution/genesis_test.go +++ b/x/distribution/genesis_test.go @@ -28,6 +28,7 @@ func TestInitGenesis(t *testing.T) { ctx, _, k, _, supplyKeeper := keeper.CreateTestInputDefault(t, false, 1000) valOpAddrs, _, valConsAddrs := keeper.GetTestAddrs() + communityTax := sdk.NewDecWithPrec(2, 2) dwis := make([]DelegatorWithdrawInfo, length) accs := make([]ValidatorAccumulatedCommissionRecord, length) for i, valAddr := range valOpAddrs { @@ -36,8 +37,10 @@ func TestInitGenesis(t *testing.T) { dwis[i].DelegatorAddress, dwis[i].WithdrawAddress = keeper.TestAddrs[i*2], keeper.TestAddrs[i*2+1] } - genesisState := NewGenesisState(true, dwis, valConsAddrs[0], accs) + genesisState := NewGenesisState(InitialFeePool(), communityTax, true, dwis, valConsAddrs[0], accs) InitGenesis(ctx, k, supplyKeeper, genesisState) + require.True(t, k.GetFeePoolCommunityCoins(ctx).IsZero()) + require.Equal(t, genesisState.CommunityTax, k.GetCommunityTax(ctx)) require.Equal(t, genesisState.WithdrawAddrEnabled, k.GetWithdrawAddrEnabled(ctx)) require.Equal(t, genesisState.PreviousProposer, k.GetPreviousProposerConsAddr(ctx)) for i := range accs { @@ -50,6 +53,7 @@ func TestInitGenesis(t *testing.T) { } actualGenesis := ExportGenesis(ctx, k) + require.Equal(t, genesisState.CommunityTax, actualGenesis.CommunityTax) require.Equal(t, genesisState.WithdrawAddrEnabled, actualGenesis.WithdrawAddrEnabled) require.ElementsMatch(t, genesisState.DelegatorWithdrawInfos, actualGenesis.DelegatorWithdrawInfos) require.Equal(t, genesisState.PreviousProposer, actualGenesis.PreviousProposer) diff --git a/x/distribution/handler.go b/x/distribution/handler.go index d3b31c47b7..91a9876dd8 100644 --- a/x/distribution/handler.go +++ b/x/distribution/handler.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/okex/okchain/x/distribution/keeper" "github.com/okex/okchain/x/distribution/types" + govtypes "github.com/okex/okchain/x/gov/types" ) // NewHandler manages all distribution tx @@ -63,3 +64,16 @@ func handleMsgWithdrawValidatorCommission(ctx sdk.Context, return sdk.Result{Events: ctx.EventManager().Events()} } + +func NewCommunityPoolSpendProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content *govtypes.Proposal) sdk.Error { + switch c := content.Content.(type) { + case types.CommunityPoolSpendProposal: + return keeper.HandleCommunityPoolSpendProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized distr proposal content type: %T", c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} diff --git a/x/distribution/handler_test.go b/x/distribution/handler_test.go index f7f4e0963c..5c3c6e88a2 100644 --- a/x/distribution/handler_test.go +++ b/x/distribution/handler_test.go @@ -13,7 +13,8 @@ import ( func TestHandler(t *testing.T) { valOpAddrs, valConsPks, valConsAddrs := keeper.GetTestAddrs() - ctx, ak, _, k, sk, _, supplyKeeper := keeper.CreateTestInputAdvanced(t, false, 1000) + communityTax := sdk.NewDecWithPrec(2, 2) + ctx, ak, _, k, sk, _, supplyKeeper := keeper.CreateTestInputAdvanced(t, false, 1000, communityTax) dh := NewHandler(k) // create one validator @@ -22,9 +23,9 @@ func TestHandler(t *testing.T) { staking.Description{}, keeper.NewTestDecCoin(1, 0)) require.True(t, sh(ctx, skMsg).IsOK()) - //send 1okt fee + //send 100okt fee feeCollector := supplyKeeper.GetModuleAccount(ctx, k.GetFeeCollectorName()) - err := feeCollector.SetCoins(keeper.NewTestDecCoins(1, 0)) + err := feeCollector.SetCoins(keeper.NewTestDecCoins(100, 0)) require.NoError(t, err) ak.SetAccount(ctx, feeCollector) // crate votes info and allocate tokens diff --git a/x/distribution/keeper/allocation.go b/x/distribution/keeper/allocation.go index a3175a8d6d..ec67e8f75f 100644 --- a/x/distribution/keeper/allocation.go +++ b/x/distribution/keeper/allocation.go @@ -4,9 +4,9 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" "github.com/okex/okchain/x/distribution/types" "github.com/okex/okchain/x/staking/exported" + stakingexported "github.com/okex/okchain/x/staking/exported" abci "github.com/tendermint/tendermint/abci/types" ) @@ -23,10 +23,8 @@ func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, logger := k.Logger(ctx) // fetch and clear the collected fees for distribution, since this is // called in BeginBlock, collected fees will be from the previous block - - // get the module account of feeCollector + // (and distributed to the previous proposer) feeCollector := k.supplyKeeper.GetModuleAccount(ctx, k.feeCollectorName) - // get the total Coins from the module account-feeCollector feesCollected := feeCollector.GetCoins() if feesCollected.Empty() { @@ -35,18 +33,20 @@ func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, } logger.Debug("AllocateTokens", "TotalFee", feesCollected.String()) - if totalPreviousPower == 0 { - // if the total previous power is zero, just return without allocate the fees util the power recovers - logger.Error("totalPreviousPower is 0, skip this allocation of fees") - return - } - // transfer collected fees to the distribution module account err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, feesCollected) if err != nil { panic(err) } + feePool := k.GetFeePool(ctx) + if totalPreviousPower == 0 { + feePool.CommunityPool = feePool.CommunityPool.Add(feesCollected) + k.SetFeePool(ctx, feePool) + logger.Debug("totalPreviousPower is zero, send fees to community pool", "fees", feesCollected) + return + } + preProposerVal := k.stakingKeeper.ValidatorByConsAddr(ctx, previousProposer) if preProposerVal == nil { // previous proposer can be unknown if say, the unbonding period is 1 block, so @@ -60,30 +60,18 @@ func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, "We recommend you investigate immediately.", previousProposer.String())) } - fee1, fee2 := feesCollected.MulDecTruncate(valPortion), feesCollected.MulDecTruncate(votePortion) + feesToVals := feesCollected.MulDecTruncate(sdk.OneDec().Sub(k.GetCommunityTax(ctx))) + fee1, fee2 := feesToVals.MulDecTruncate(valPortion), feesToVals.MulDecTruncate(votePortion) remaining := feesCollected.Sub(fee1.Add(fee2)) - remain1 := k.allocateByVal(ctx, fee1, previousVotes) //allocate rewards equally between validators - remain2 := k.allocateByVotePower(ctx, fee2) //allocate rewards by votes + remain1 := k.allocateByVal(ctx, feesToVals.MulDecTruncate(valPortion), previousVotes) //allocate rewards equally between validators + remain2 := k.allocateByVotePower(ctx, feesToVals.MulDecTruncate(votePortion)) //allocate rewards by votes remaining = remaining.Add(remain1.Add(remain2)) - // if it remains some coins, allocate to proposer + // allocate community funding if !remaining.IsZero() { - // if we can't find previous proposer validator from store or being jailed - // then transfer the remaining to fee module account back - if preProposerVal == nil || preProposerVal.IsJailed() { - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, k.feeCollectorName, remaining) - if err != nil { - panic(err) - } - logger.Debug("No Proposer to receive remaining", "Remainder to feeCollector", remaining) - return - } - - k.AllocateTokensToValidator(ctx, preProposerVal, remaining) - logger.Debug("Send remaining to previous proposer", - "previous proposer", preProposerVal.GetOperator().String(), - "remaining coins", remaining, - ) + feePool.CommunityPool = feePool.CommunityPool.Add(remaining) + k.SetFeePool(ctx, feePool) + logger.Debug("Send remaining to community pool", "remaining", remaining) } } @@ -91,30 +79,25 @@ func (k Keeper) allocateByVal(ctx sdk.Context, rewards sdk.DecCoins, previousVot logger := k.Logger(ctx) //count the total sum of the unJailed val - validators := make([]stakingexported.ValidatorI, 0) + var validators []stakingexported.ValidatorI for _, vote := range previousVotes { validator := k.stakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address) - if validator == nil { - // previous validator can be unknown if say, the unbonding period is 1 block, so - // e.g. a validator undelegates at block X, it's removed entirely by - // block X+1's endblock, then X+2 we need to refer to the previous - // validator for X+1, but we've forgotten about them. - continue - } - if validator.IsJailed() { - logger.Debug(fmt.Sprintf("validator %s is jailed, not allowed to get reward by equal", validator.GetOperator())) - } else { - validators = append(validators, validator) + if validator != nil { + if validator.IsJailed() { + logger.Debug(fmt.Sprintf("validator %s is jailed, not allowed to get reward by equal", validator.GetOperator())) + } else { + validators = append(validators, validator) + } } } //calculate the proportion of every valid validator powerFraction := sdk.NewDec(1).QuoTruncate(sdk.NewDec(int64(len(validators)))) - //beginning allocating rewards + //beginning allocating rewards equally remaining := rewards + reward := rewards.MulDecTruncate(powerFraction) for _, val := range validators { - reward := rewards.MulDecTruncate(powerFraction) k.AllocateTokensToValidator(ctx, val, reward) logger.Debug("allocate by equal", val.GetOperator(), reward.String()) remaining = remaining.Sub(reward) @@ -163,9 +146,9 @@ func (k Keeper) AllocateTokensToValidator(ctx sdk.Context, val exported.Validato // split tokens between validator and delegators according to commissions // commissions is always 1.0, so tokens.MulDec(val.GetCommission()) = tokens // only update current commissions - currentCommission := k.GetValidatorAccumulatedCommission(ctx, val.GetOperator()) - currentCommission = currentCommission.Add(tokens) - k.SetValidatorAccumulatedCommission(ctx, val.GetOperator(), currentCommission) + commission := k.GetValidatorAccumulatedCommission(ctx, val.GetOperator()) + commission = commission.Add(tokens) + k.SetValidatorAccumulatedCommission(ctx, val.GetOperator(), commission) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeCommission, diff --git a/x/distribution/keeper/allocation_test.go b/x/distribution/keeper/allocation_test.go index 75c8643c8e..caf332f995 100644 --- a/x/distribution/keeper/allocation_test.go +++ b/x/distribution/keeper/allocation_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/okex/okchain/x/distribution/types" "github.com/okex/okchain/x/staking" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" @@ -25,11 +26,10 @@ func TestAllocateTokensToValidatorWithCommission(t *testing.T) { } type testAllocationParam struct { - totalPower int64 - isVote []bool - isJailed []bool - fee int64 - expected [4]int64 + totalPower int64 + isVote []bool + isJailed []bool + fee sdk.DecCoins } func getTestAllocationParams() []testAllocationParam { @@ -37,59 +37,50 @@ func getTestAllocationParams() []testAllocationParam { { //test the case when fee is zero 10, []bool{true, true, true, true}, []bool{false, false, false, false}, - 0, [4]int64{0, 0, 0, 0}, + nil, }, { //test the case where total power is zero 0, []bool{true, true, true, true}, []bool{false, false, false, false}, - 120, [4]int64{0, 0, 0, 0}, + NewTestDecCoins(123,2), }, { //test the case where just the part of vals has voted 10, []bool{true, true, false, false}, []bool{false, false, false, false}, - 120, [4]int64{24, 33, 27, 36}, + NewTestDecCoins(123,2), }, { //test the case where two vals is jailed 10, []bool{true, true, false, false}, []bool{false, true, true, false}, - 120, [4]int64{48, 0, 0, 72}, + NewTestDecCoins(123,2), }, } } func TestAllocateTokensToManyValidators(t *testing.T) { tests := getTestAllocationParams() - valOpAddrs, _, valConsAddrs := GetTestAddrs() + _, _, valConsAddrs := GetTestAddrs() //start test for _, test := range tests { - ctx, ak, k, sk, supplyKeeper := CreateTestInputDefault(t, false, 1000) + ctx, ak, k, sk, _ := CreateTestInputDefault(t, false, 1000) //set the fee - feeCoins, _ := NewTestDecCoins(test.fee, 0).TruncateDecimal() - setTestFees(t, ctx, k, ak, feeCoins) + setTestFees(t, ctx, k, ak, test.fee) // crate votes info votes := createTestVotes(ctx, sk, test) // allocate the tokens k.AllocateTokens(ctx, test.totalPower, valConsAddrs[0], votes) - - remain := feeCoins - // check the results - for i := int64(0); i < int64(len(test.isVote)); i++ { - expectedCommssion := NewTestDecCoins(test.expected[i], 0) - if !expectedCommssion.IsZero() { - require.Equal(t, expectedCommssion, k.GetValidatorAccumulatedCommission(ctx, valOpAddrs[i])) - commission, err := k.WithdrawValidatorCommission(ctx, valOpAddrs[i]) - require.NoError(t, err) - expectedWithdrawCommission, _ := expectedCommssion.TruncateDecimal() - require.Equal(t, expectedWithdrawCommission, commission) - remain = remain.Sub(commission) - } - } - //TODO when rollback the community pool - //require.True(t, supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().IsEqual(remain)) - //require.True(t, supplyKeeper.GetModuleAccount(ctx, k.feeCollectorName).GetCoins().IsZero()) - require.Equal(t, true, supplyKeeper.GetModuleAccount(ctx, k.feeCollectorName).GetCoins().IsEqual(remain)) + commissions := NewTestDecCoins(0,0) + k.IterateValidatorAccumulatedCommissions(ctx, + func(val sdk.ValAddress, commission types.ValidatorAccumulatedCommission) (stop bool) { + commissions = commissions.Add(commission) + return false + }) + totalCommissions := k.GetDistributionAccount(ctx).GetCoins() + communityCoins := k.GetFeePoolCommunityCoins(ctx) + require.Equal(t, totalCommissions, communityCoins.Add(commissions)) + require.Equal(t, test.fee, totalCommissions) } } diff --git a/x/distribution/keeper/hooks.go b/x/distribution/keeper/hooks.go index e027461fb8..173fd44c7f 100644 --- a/x/distribution/keeper/hooks.go +++ b/x/distribution/keeper/hooks.go @@ -29,17 +29,16 @@ func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr if !commission.IsZero() { // split into integral & remainder coins, remainder := commission.TruncateDecimal() + // remainder to community pool if !remainder.IsZero() { - err := h.k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, h.k.feeCollectorName, remainder) - if err != nil { - panic(err) - } + feePool := h.k.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Add(remainder) + h.k.SetFeePool(ctx, feePool) } - - accAddr := sdk.AccAddress(valAddr) - withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr) // add to validator account if !coins.IsZero() { + accAddr := sdk.AccAddress(valAddr) + withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr) err := h.k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, coins) if err != nil { panic(err) diff --git a/x/distribution/keeper/invariants.go b/x/distribution/keeper/invariants.go index ad57f78b18..dc7a6ece08 100644 --- a/x/distribution/keeper/invariants.go +++ b/x/distribution/keeper/invariants.go @@ -71,18 +71,18 @@ func CanWithdrawInvariant(k Keeper) sdk.Invariant { // is consistent with the sum of accumulated commissions func ModuleAccountInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { - var expectedCoins sdk.DecCoins + var accumulatedCommission sdk.DecCoins k.IterateValidatorAccumulatedCommissions(ctx, func(_ sdk.ValAddress, commission types.ValidatorAccumulatedCommission) (stop bool) { - expectedCoins = expectedCoins.Add(commission) + accumulatedCommission = accumulatedCommission.Add(commission) return false }) - + communityPool := k.GetFeePoolCommunityCoins(ctx) macc := k.GetDistributionAccount(ctx) - broken := !macc.GetCoins().IsEqual(expectedCoins) + broken := !macc.GetCoins().IsEqual(communityPool.Add(accumulatedCommission)) return sdk.FormatInvariant(types.ModuleName, "ModuleAccount coins", fmt.Sprintf("\texpected distribution ModuleAccount coins: %s\n"+ "\tacutal distribution ModuleAccount coins: %s\n", - expectedCoins, macc.GetCoins())), broken + accumulatedCommission, macc.GetCoins())), broken } } diff --git a/x/distribution/keeper/key.go b/x/distribution/keeper/key.go index aef367d342..033cce56a0 100644 --- a/x/distribution/keeper/key.go +++ b/x/distribution/keeper/key.go @@ -19,10 +19,12 @@ const ( // // - 0x07: ValidatorCurrentRewards var ( + FeePoolKey = []byte{0x00} // key for global distribution state ProposerKey = []byte{0x01} // key for the proposer operator address DelegatorWithdrawAddrPrefix = []byte{0x03} // key for delegator withdraw address ValidatorAccumulatedCommissionPrefix = []byte{0x07} // key for accumulated validator commission + ParamStoreKeyCommunityTax = []byte("communitytax") ParamStoreKeyWithdrawAddrEnabled = []byte("withdrawaddrenabled") ) diff --git a/x/distribution/keeper/params.go b/x/distribution/keeper/params.go index 0bb98bc037..4cc6b533dd 100644 --- a/x/distribution/keeper/params.go +++ b/x/distribution/keeper/params.go @@ -8,10 +8,25 @@ import ( // ParamKeyTable is the type declaration for parameters func ParamKeyTable() params.KeyTable { return params.NewKeyTable( + ParamStoreKeyCommunityTax, sdk.Dec{}, ParamStoreKeyWithdrawAddrEnabled, true, ) } +// GetCommunityTax returns the current CommunityTax rate from the global param store +// nolint: errcheck +func (k Keeper) GetCommunityTax(ctx sdk.Context) sdk.Dec { + var percent sdk.Dec + k.paramSpace.Get(ctx, ParamStoreKeyCommunityTax, &percent) + return percent +} + +// SetCommunityTax sets the value of community tax +// nolint: errcheck +func (k Keeper) SetCommunityTax(ctx sdk.Context, percent sdk.Dec) { + k.paramSpace.Set(ctx, ParamStoreKeyCommunityTax, &percent) +} + // GetWithdrawAddrEnabled returns the current WithdrawAddrEnabled // nolint: errcheck func (k Keeper) GetWithdrawAddrEnabled(ctx sdk.Context) bool { diff --git a/x/distribution/keeper/proposal_handler.go b/x/distribution/keeper/proposal_handler.go new file mode 100644 index 0000000000..28db8bf14c --- /dev/null +++ b/x/distribution/keeper/proposal_handler.go @@ -0,0 +1,47 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/okex/okchain/x/distribution/types" +) + +// HandleCommunityPoolSpendProposal is a handler for executing a passed community spend proposal +func HandleCommunityPoolSpendProposal(ctx sdk.Context, k Keeper, p types.CommunityPoolSpendProposal) sdk.Error { + if k.blacklistedAddrs[p.Recipient.String()] { + return sdk.ErrUnauthorized(fmt.Sprintf("%s is blacklisted from receiving external funds", p.Recipient)) + } + + err := k.distributeFromFeePool(ctx, p.Amount, p.Recipient) + if err != nil { + return err + } + + logger := k.Logger(ctx) + logger.Info(fmt.Sprintf("transferred %s from the community pool to recipient %s", p.Amount, p.Recipient)) + return nil +} + +// distributeFromFeePool distributes funds from the distribution module account to +// a receiver address while updating the community pool +func (k Keeper) distributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) sdk.Error { + feePool := k.GetFeePool(ctx) + + // NOTE the community pool isn't a module account, however its coins + // are held in the distribution module account. Thus the community pool + // must be reduced separately from the SendCoinsFromModuleToAccount call + newPool, negative := feePool.CommunityPool.SafeSub(amount) + if negative { + return types.ErrBadDistribution(k.codespace) + } + feePool.CommunityPool = newPool + + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiveAddr, amount) + if err != nil { + return err + } + + k.SetFeePool(ctx, feePool) + return nil +} diff --git a/x/distribution/keeper/querier.go b/x/distribution/keeper/querier.go index c5c6d88865..09bd0013af 100644 --- a/x/distribution/keeper/querier.go +++ b/x/distribution/keeper/querier.go @@ -23,6 +23,9 @@ func NewQuerier(k Keeper) sdk.Querier { case types.QueryWithdrawAddr: return queryDelegatorWithdrawAddress(ctx, path[1:], req, k) + case types.QueryCommunityPool: + return queryCommunityPool(ctx, path[1:], req, k) + default: return nil, sdk.ErrUnknownRequest("unknown distr query endpoint") } @@ -31,6 +34,12 @@ func NewQuerier(k Keeper) sdk.Querier { func queryParams(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { switch path[0] { + case types.ParamCommunityTax: + bz, err := codec.MarshalJSONIndent(k.cdc, k.GetCommunityTax(ctx)) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil case types.ParamWithdrawAddrEnabled: bz, err := codec.MarshalJSONIndent(k.cdc, k.GetWithdrawAddrEnabled(ctx)) if err != nil { @@ -74,3 +83,11 @@ func queryDelegatorWithdrawAddress(ctx sdk.Context, _ []string, req abci.Request return bz, nil } + +func queryCommunityPool(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { + bz, err := k.cdc.MarshalJSON(k.GetFeePoolCommunityCoins(ctx)) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} diff --git a/x/distribution/keeper/store.go b/x/distribution/keeper/store.go index ba119b70f8..1558cc3d26 100644 --- a/x/distribution/keeper/store.go +++ b/x/distribution/keeper/store.go @@ -36,6 +36,29 @@ func (k Keeper) IterateDelegatorWithdrawAddrs(ctx sdk.Context, } } +// GetFeePool returns the global fee pool distribution info +func (k Keeper) GetFeePool(ctx sdk.Context) (feePool types.FeePool) { + store := ctx.KVStore(k.storeKey) + b := store.Get(FeePoolKey) + if b == nil { + panic("Stored fee pool should not have been nil") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &feePool) + return +} + +// SetFeePool sets the global fee pool distribution info +func (k Keeper) SetFeePool(ctx sdk.Context, feePool types.FeePool) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(feePool) + store.Set(FeePoolKey, b) +} + +// GetFeePoolCommunityCoins returns the community coins +func (k Keeper) GetFeePoolCommunityCoins(ctx sdk.Context) sdk.DecCoins { + return k.GetFeePool(ctx).CommunityPool +} + // GetPreviousProposerConsAddr returns the proposer public key for this block func (k Keeper) GetPreviousProposerConsAddr(ctx sdk.Context) (consAddr sdk.ConsAddress) { store := ctx.KVStore(k.storeKey) diff --git a/x/distribution/keeper/test_common.go b/x/distribution/keeper/test_common.go index a309fb512d..4683192a95 100644 --- a/x/distribution/keeper/test_common.go +++ b/x/distribution/keeper/test_common.go @@ -101,7 +101,8 @@ func MakeTestCodec() *codec.Codec { // CreateTestInputDefault test input with default values func CreateTestInputDefault(t *testing.T, isCheckTx bool, initPower int64) ( sdk.Context, auth.AccountKeeper, Keeper, staking.Keeper, types.SupplyKeeper) { - ctx, ak, _, dk, sk, _, supplyKeeper := CreateTestInputAdvanced(t, isCheckTx, initPower) + communityTax := sdk.NewDecWithPrec(2, 2) + ctx, ak, _, dk, sk, _, supplyKeeper := CreateTestInputAdvanced(t, isCheckTx, initPower, communityTax) sh := staking.NewHandler(sk) valOpAddrs, valConsPks, _ := GetTestAddrs() // create four validators @@ -116,7 +117,7 @@ func CreateTestInputDefault(t *testing.T, isCheckTx bool, initPower int64) ( } // CreateTestInputAdvanced hogpodge of all sorts of input required for testing -func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64) ( +func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64, communityTax sdk.Dec) ( sdk.Context, auth.AccountKeeper, bank.Keeper, Keeper, staking.Keeper, params.Keeper, types.SupplyKeeper) { initTokens := sdk.TokensFromConsensusPower(initPower) @@ -195,5 +196,9 @@ func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64) ( // set the distribution hooks on staking sk.SetHooks(keeper.Hooks()) + // set genesis items required for distribution + keeper.SetFeePool(ctx, types.InitialFeePool()) + keeper.SetCommunityTax(ctx, communityTax) + return ctx, accountKeeper, bankKeeper, keeper, sk, pk, supplyKeeper } diff --git a/x/distribution/proposal_handler_test.go b/x/distribution/proposal_handler_test.go new file mode 100644 index 0000000000..c591f91201 --- /dev/null +++ b/x/distribution/proposal_handler_test.go @@ -0,0 +1,67 @@ +package distribution + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/okex/okchain/x/distribution/keeper" + "github.com/okex/okchain/x/distribution/types" + govtypes "github.com/okex/okchain/x/gov/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +var ( + delPk1 = ed25519.GenPrivKey().PubKey() + delAddr1 = sdk.AccAddress(delPk1.Address()) + + amount = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1))) +) + +func testProposal(recipient sdk.AccAddress, amount sdk.Coins) govtypes.Proposal { + return govtypes.Proposal{Content:types.NewCommunityPoolSpendProposal( + "Test", + "description", + recipient, + amount, + )} +} + +func TestProposalHandlerPassed(t *testing.T) { + ctx, accountKeeper, k, _, supplyKeeper := keeper.CreateTestInputDefault(t, false, 10) + recipient := delAddr1 + + // add coins to the module account + macc := k.GetDistributionAccount(ctx) + err := macc.SetCoins(macc.GetCoins().Add(amount)) + require.NoError(t, err) + + supplyKeeper.SetModuleAccount(ctx, macc) + + account := accountKeeper.NewAccountWithAddress(ctx, recipient) + require.True(t, account.GetCoins().IsZero()) + accountKeeper.SetAccount(ctx, account) + + feePool := k.GetFeePool(ctx) + feePool.CommunityPool = sdk.NewDecCoins(amount) + k.SetFeePool(ctx, feePool) + + tp := testProposal(recipient, amount) + hdlr := NewCommunityPoolSpendProposalHandler(k) + require.NoError(t, hdlr(ctx, &tp)) + require.Equal(t, accountKeeper.GetAccount(ctx, recipient).GetCoins(), amount) +} + +func TestProposalHandlerFailed(t *testing.T) { + ctx, accountKeeper, k, _, _ := keeper.CreateTestInputDefault(t, false, 10) + recipient := delAddr1 + + account := accountKeeper.NewAccountWithAddress(ctx, recipient) + require.True(t, account.GetCoins().IsZero()) + accountKeeper.SetAccount(ctx, account) + + tp := testProposal(recipient, amount) + hdlr := NewCommunityPoolSpendProposalHandler(k) + require.Error(t, hdlr(ctx, &tp)) + require.True(t, accountKeeper.GetAccount(ctx, recipient).GetCoins().IsZero()) +} diff --git a/x/distribution/types/codec.go b/x/distribution/types/codec.go index 1272b55b8d..b48671c11d 100644 --- a/x/distribution/types/codec.go +++ b/x/distribution/types/codec.go @@ -8,6 +8,7 @@ import ( func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgWithdrawValidatorCommission{}, "okchain/distribution/MsgWithdrawReward", nil) cdc.RegisterConcrete(MsgSetWithdrawAddress{}, "okchain/distribution/MsgModifyWithdrawAddress", nil) + cdc.RegisterConcrete(CommunityPoolSpendProposal{}, "okchain/distribution/CommunityPoolSpendProposal", nil) } // ModuleCdc generic sealed codec to be used throughout module diff --git a/x/distribution/types/errors.go b/x/distribution/types/errors.go index 9745c982b7..fe7b0e4b4a 100644 --- a/x/distribution/types/errors.go +++ b/x/distribution/types/errors.go @@ -29,3 +29,12 @@ func ErrNoValidatorCommission(codespace sdk.CodespaceType) sdk.Error { func ErrSetWithdrawAddrDisabled(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeSetWithdrawAddrDisabled, "set withdraw address disabled") } +func ErrBadDistribution(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "community pool does not have sufficient coins to distribute") +} +func ErrInvalidProposalAmount(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "invalid community pool spend proposal amount") +} +func ErrEmptyProposalRecipient(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "invalid community pool spend proposal recipient") +} diff --git a/x/distribution/types/fee_pool.go b/x/distribution/types/fee_pool.go new file mode 100644 index 0000000000..d85c24be24 --- /dev/null +++ b/x/distribution/types/fee_pool.go @@ -0,0 +1,28 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// FeePool is the struct of the global fee pool for distribution +type FeePool struct { + CommunityPool sdk.DecCoins `json:"community_pool" yaml:"community_pool"` // pool for community funds yet to be spent +} + +// InitialFeePool zero fee pool +func InitialFeePool() FeePool { + return FeePool{ + CommunityPool: sdk.DecCoins{}, + } +} + +// ValidateGenesis validates the fee pool for a genesis state +func (f FeePool) ValidateGenesis() error { + if f.CommunityPool.IsAnyNegative() { + return fmt.Errorf("negative CommunityPool in distribution fee pool, is %v", + f.CommunityPool) + } + return nil +} diff --git a/x/distribution/types/fee_pool_test.go b/x/distribution/types/fee_pool_test.go new file mode 100644 index 0000000000..bc85e56df2 --- /dev/null +++ b/x/distribution/types/fee_pool_test.go @@ -0,0 +1,19 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestValidateGenesis(t *testing.T) { + + fp := InitialFeePool() + require.Nil(t, fp.ValidateGenesis()) + + fp2 := FeePool{CommunityPool: sdk.DecCoins{{"stake", sdk.NewDec(-1)}}} + require.NotNil(t, fp2.ValidateGenesis()) + +} diff --git a/x/distribution/types/genesis.go b/x/distribution/types/genesis.go index 112c3ae3c1..634f24204b 100644 --- a/x/distribution/types/genesis.go +++ b/x/distribution/types/genesis.go @@ -1,6 +1,8 @@ package types import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -19,6 +21,8 @@ type ValidatorAccumulatedCommissionRecord struct { // GenesisState - all distribution state that must be provided at genesis type GenesisState struct { + FeePool FeePool `json:"fee_pool" yaml:"fee_pool"` + CommunityTax sdk.Dec `json:"community_tax" yaml:"community_tax"` WithdrawAddrEnabled bool `json:"withdraw_addr_enabled" yaml:"withdraw_addr_enabled"` DelegatorWithdrawInfos []DelegatorWithdrawInfo `json:"delegator_withdraw_infos" yaml:"delegator_withdraw_infos"` PreviousProposer sdk.ConsAddress `json:"previous_proposer" yaml:"previous_proposer"` @@ -26,9 +30,12 @@ type GenesisState struct { } // NewGenesisState creates a new object of GenesisState -func NewGenesisState(withdrawAddrEnabled bool, dwis []DelegatorWithdrawInfo, pp sdk.ConsAddress, +func NewGenesisState(feePool FeePool, communityTax sdk.Dec, + withdrawAddrEnabled bool, dwis []DelegatorWithdrawInfo, pp sdk.ConsAddress, acc []ValidatorAccumulatedCommissionRecord) GenesisState { return GenesisState{ + FeePool: feePool, + CommunityTax: communityTax, WithdrawAddrEnabled: withdrawAddrEnabled, DelegatorWithdrawInfos: dwis, PreviousProposer: pp, @@ -39,6 +46,8 @@ func NewGenesisState(withdrawAddrEnabled bool, dwis []DelegatorWithdrawInfo, pp // DefaultGenesisState returns default genesis func DefaultGenesisState() GenesisState { return GenesisState{ + FeePool: InitialFeePool(), + CommunityTax: sdk.NewDecWithPrec(2, 2), // 2% WithdrawAddrEnabled: true, DelegatorWithdrawInfos: []DelegatorWithdrawInfo{}, PreviousProposer: nil, @@ -48,5 +57,9 @@ func DefaultGenesisState() GenesisState { // ValidateGenesis validates the genesis state of distribution genesis input func ValidateGenesis(data GenesisState) error { - return nil + if data.CommunityTax.IsNegative() || data.CommunityTax.GT(sdk.OneDec()) { + return fmt.Errorf("mint parameter CommunityTax should non-negative and "+ + "less than one, is %s", data.CommunityTax.String()) + } + return data.FeePool.ValidateGenesis() } diff --git a/x/distribution/types/proposal.go b/x/distribution/types/proposal.go new file mode 100644 index 0000000000..0454c6ba67 --- /dev/null +++ b/x/distribution/types/proposal.go @@ -0,0 +1,74 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/okex/okchain/x/gov/types" +) + +const ( + // ProposalTypeCommunityPoolSpend defines the type for a CommunityPoolSpendProposal + ProposalTypeCommunityPoolSpend = "CommunityPoolSpend" +) + +// Assert CommunityPoolSpendProposal implements govtypes.Content at compile-time +var _ govtypes.Content = CommunityPoolSpendProposal{} + +func init() { + govtypes.RegisterProposalType(ProposalTypeCommunityPoolSpend) + govtypes.RegisterProposalTypeCodec(CommunityPoolSpendProposal{}, "okchain/distribution/CommunityPoolSpendProposal") +} + +// CommunityPoolSpendProposal spends from the community pool +type CommunityPoolSpendProposal struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// NewCommunityPoolSpendProposal creates a new community pool spned proposal. +func NewCommunityPoolSpendProposal(title, description string, recipient sdk.AccAddress, amount sdk.Coins) CommunityPoolSpendProposal { + return CommunityPoolSpendProposal{title, description, recipient, amount} +} + +// GetTitle returns the title of a community pool spend proposal. +func (csp CommunityPoolSpendProposal) GetTitle() string { return csp.Title } + +// GetDescription returns the description of a community pool spend proposal. +func (csp CommunityPoolSpendProposal) GetDescription() string { return csp.Description } + +// GetDescription returns the routing key of a community pool spend proposal. +func (csp CommunityPoolSpendProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of a community pool spend proposal. +func (csp CommunityPoolSpendProposal) ProposalType() string { return ProposalTypeCommunityPoolSpend } + +// ValidateBasic runs basic stateless validity checks +func (csp CommunityPoolSpendProposal) ValidateBasic() sdk.Error { + err := govtypes.ValidateAbstract(DefaultCodespace, csp) + if err != nil { + return err + } + if !csp.Amount.IsValid() { + return ErrInvalidProposalAmount(DefaultCodespace) + } + if csp.Recipient.Empty() { + return ErrEmptyProposalRecipient(DefaultCodespace) + } + return nil +} + +// String implements the Stringer interface. +func (csp CommunityPoolSpendProposal) String() string { + var b strings.Builder + b.WriteString(fmt.Sprintf(`Community Pool Spend Proposal: + Title: %s + Description: %s + Recipient: %s + Amount: %s +`, csp.Title, csp.Description, csp.Recipient, csp.Amount)) + return b.String() +} diff --git a/x/distribution/types/querier.go b/x/distribution/types/querier.go index cb319f85a4..cc622e7436 100644 --- a/x/distribution/types/querier.go +++ b/x/distribution/types/querier.go @@ -7,7 +7,9 @@ const ( QueryParams = "params" QueryValidatorCommission = "validator_commission" QueryWithdrawAddr = "withdraw_addr" + QueryCommunityPool = "community_pool" + ParamCommunityTax = "community_tax" ParamWithdrawAddrEnabled = "withdraw_addr_enabled" ) From a068d5f4e364f6324bbe5716c9c3375640da2254 Mon Sep 17 00:00:00 2001 From: yili_Green Date: Tue, 14 Apr 2020 18:10:10 +0800 Subject: [PATCH 2/2] go fmt code --- x/distribution/alias.go | 2 +- x/distribution/client/cli/utils.go | 2 +- x/distribution/client/rest/rest.go | 2 +- x/distribution/client/rest/utils.go | 1 - x/distribution/keeper/allocation_test.go | 18 +++++++++--------- x/distribution/proposal_handler_test.go | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/x/distribution/alias.go b/x/distribution/alias.go index 90e93d2f3d..1cdbf9815d 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -44,7 +44,7 @@ var ( ErrNilValidatorAddr = types.ErrNilValidatorAddr ErrNoValidatorCommission = types.ErrNoValidatorCommission ErrSetWithdrawAddrDisabled = types.ErrSetWithdrawAddrDisabled - InitialFeePool = types.InitialFeePool + InitialFeePool = types.InitialFeePool NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState ValidateGenesis = types.ValidateGenesis diff --git a/x/distribution/client/cli/utils.go b/x/distribution/client/cli/utils.go index db0f4e600c..da72530686 100644 --- a/x/distribution/client/cli/utils.go +++ b/x/distribution/client/cli/utils.go @@ -13,7 +13,7 @@ type ( Title string `json:"title" yaml:"title"` Description string `json:"description" yaml:"description"` Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` - Amount sdk.DecCoins `json:"amount" yaml:"amount"` + Amount sdk.DecCoins `json:"amount" yaml:"amount"` Deposit sdk.DecCoins `json:"deposit" yaml:"deposit"` } ) diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 63a6ff4954..d6c9559b84 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -49,4 +49,4 @@ func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) } -} \ No newline at end of file +} diff --git a/x/distribution/client/rest/utils.go b/x/distribution/client/rest/utils.go index bc332e69c0..1569e6d1a7 100644 --- a/x/distribution/client/rest/utils.go +++ b/x/distribution/client/rest/utils.go @@ -18,4 +18,3 @@ type ( Deposit sdk.DecCoins `json:"deposit" yaml:"deposit"` } ) - diff --git a/x/distribution/keeper/allocation_test.go b/x/distribution/keeper/allocation_test.go index caf332f995..b1c0261394 100644 --- a/x/distribution/keeper/allocation_test.go +++ b/x/distribution/keeper/allocation_test.go @@ -26,10 +26,10 @@ func TestAllocateTokensToValidatorWithCommission(t *testing.T) { } type testAllocationParam struct { - totalPower int64 - isVote []bool - isJailed []bool - fee sdk.DecCoins + totalPower int64 + isVote []bool + isJailed []bool + fee sdk.DecCoins } func getTestAllocationParams() []testAllocationParam { @@ -42,17 +42,17 @@ func getTestAllocationParams() []testAllocationParam { { //test the case where total power is zero 0, []bool{true, true, true, true}, []bool{false, false, false, false}, - NewTestDecCoins(123,2), + NewTestDecCoins(123, 2), }, { //test the case where just the part of vals has voted 10, []bool{true, true, false, false}, []bool{false, false, false, false}, - NewTestDecCoins(123,2), + NewTestDecCoins(123, 2), }, { //test the case where two vals is jailed 10, []bool{true, true, false, false}, []bool{false, true, true, false}, - NewTestDecCoins(123,2), + NewTestDecCoins(123, 2), }, } } @@ -71,13 +71,13 @@ func TestAllocateTokensToManyValidators(t *testing.T) { // allocate the tokens k.AllocateTokens(ctx, test.totalPower, valConsAddrs[0], votes) - commissions := NewTestDecCoins(0,0) + commissions := NewTestDecCoins(0, 0) k.IterateValidatorAccumulatedCommissions(ctx, func(val sdk.ValAddress, commission types.ValidatorAccumulatedCommission) (stop bool) { commissions = commissions.Add(commission) return false }) - totalCommissions := k.GetDistributionAccount(ctx).GetCoins() + totalCommissions := k.GetDistributionAccount(ctx).GetCoins() communityCoins := k.GetFeePoolCommunityCoins(ctx) require.Equal(t, totalCommissions, communityCoins.Add(commissions)) require.Equal(t, test.fee, totalCommissions) diff --git a/x/distribution/proposal_handler_test.go b/x/distribution/proposal_handler_test.go index c591f91201..4b0620be3f 100644 --- a/x/distribution/proposal_handler_test.go +++ b/x/distribution/proposal_handler_test.go @@ -19,7 +19,7 @@ var ( ) func testProposal(recipient sdk.AccAddress, amount sdk.Coins) govtypes.Proposal { - return govtypes.Proposal{Content:types.NewCommunityPoolSpendProposal( + return govtypes.Proposal{Content: types.NewCommunityPoolSpendProposal( "Test", "description", recipient,