From 139134d665f86edaf5d438f13940a9469e64c4d4 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 13 May 2019 09:27:19 +0200 Subject: [PATCH 01/13] Basic implementation --- cmd/gaia/app/app.go | 3 +- x/distribution/handler.go | 14 +++++ x/distribution/keeper/proposal_handler.go | 20 ++++++ x/distribution/types/errors.go | 6 ++ x/distribution/types/proposal.go | 74 +++++++++++++++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 x/distribution/keeper/proposal_handler.go create mode 100644 x/distribution/types/proposal.go diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 5c732aff81f..00bd45a2076 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -145,7 +145,8 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b govRouter := gov.NewRouter() govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). - AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)) + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) app.govKeeper = gov.NewKeeper( app.cdc, diff --git a/x/distribution/handler.go b/x/distribution/handler.go index a0276bf70bb..3602bfabb3a 100644 --- a/x/distribution/handler.go +++ b/x/distribution/handler.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/distribution/keeper" "github.com/cosmos/cosmos-sdk/x/distribution/tags" "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) func NewHandler(k keeper.Keeper) sdk.Handler { @@ -77,3 +78,16 @@ func handleMsgWithdrawValidatorCommission(ctx sdk.Context, msg types.MsgWithdraw ), } } + +func NewCommunityPoolSpendProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) sdk.Error { + switch c := 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/keeper/proposal_handler.go b/x/distribution/keeper/proposal_handler.go new file mode 100644 index 00000000000..4347b0a1d09 --- /dev/null +++ b/x/distribution/keeper/proposal_handler.go @@ -0,0 +1,20 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +func HandleCommunityPoolSpendProposal(ctx sdk.Context, k Keeper, p types.CommunityPoolSpendProposal) sdk.Error { + feePool := k.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Sub(sdk.NewDecCoins(p.Amount)) + if feePool.CommunityPool.IsAnyNegative() { + return types.ErrBadDistribution(k.codespace) + } + k.SetFeePool(ctx, feePool) + _, err := k.bankKeeper.AddCoins(ctx, p.Recipient, p.Amount) + if err != nil { + return err + } + return nil +} diff --git a/x/distribution/types/errors.go b/x/distribution/types/errors.go index dfe988ff34a..2b639411413 100644 --- a/x/distribution/types/errors.go +++ b/x/distribution/types/errors.go @@ -39,3 +39,9 @@ func ErrSetWithdrawAddrDisabled(codespace sdk.CodespaceType) sdk.Error { 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/proposal.go b/x/distribution/types/proposal.go new file mode 100644 index 00000000000..068876841a4 --- /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/cosmos/cosmos-sdk/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{}, "cosmos-sdk/CommunityPoolSpendProposal") +} + +// CommunityPoolSpendProposal spends from the community pool +type CommunityPoolSpendProposal struct { + Title string `json:"title"` + Description string `json:"description"` + Recipient sdk.AccAddress `json:"recipient"` + Amount sdk.Coins `json:"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() +} From 22fc9747d7601e4cd7b87eb1cf4bf17daa7341eb Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 13:30:15 +0200 Subject: [PATCH 02/13] REST, simulation wip --- simapp/app.go | 3 ++- simapp/sim_test.go | 1 + x/distribution/alias.go | 2 ++ x/distribution/client/rest/rest.go | 40 ++++++++++++++++++++++++++++ x/distribution/client/utils/utils.go | 20 ++++++++++++++ x/distribution/simulation/msgs.go | 11 ++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 x/distribution/client/utils/utils.go diff --git a/simapp/app.go b/simapp/app.go index 2ebd16dafdc..95036981d64 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -159,7 +159,8 @@ func NewSimApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bo // register the proposal types govRouter := gov.NewRouter() govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). - AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)) + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) app.govKeeper = gov.NewKeeper(app.cdc, app.keyGov, app.paramsKeeper, govSubspace, app.bankKeeper, &stakingKeeper, gov.DefaultCodespace, govRouter) diff --git a/simapp/sim_test.go b/simapp/sim_test.go index 640e3eec9e3..76ff2c3591b 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -301,6 +301,7 @@ func testAndRunTxs(app *SimApp) []simulation.WeightedOperation { {50, distrsim.SimulateMsgWithdrawDelegatorReward(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper)}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, govsim.SimulateTextProposalContent)}, + {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, distrsim.SimulateCommunityPoolSpendProposalContent)}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, paramsim.SimulateParamChangeProposalContent)}, {100, govsim.SimulateMsgDeposit(app.govKeeper)}, {100, stakingsim.SimulateMsgCreateValidator(app.accountKeeper, app.stakingKeeper)}, diff --git a/x/distribution/alias.go b/x/distribution/alias.go index c10a760d1c6..f63826eb602 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -55,6 +55,8 @@ var ( NewMsgWithdrawDelegatorReward = types.NewMsgWithdrawDelegatorReward NewMsgWithdrawValidatorCommission = types.NewMsgWithdrawValidatorCommission + NewCommunityPoolSpendProposal = types.NewCommunityPoolSpendProposal + NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier NewQueryValidatorOutstandingRewardsParams = keeper.NewQueryValidatorOutstandingRewardsParams diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 6596ccdf3e1..a2641b4a810 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -2,9 +2,17 @@ package rest import ( "github.com/gorilla/mux" + "net/http" "github.com/cosmos/cosmos-sdk/client/context" + clientrest "github.com/cosmos/cosmos-sdk/client/rest" "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/distribution" + distrcutils "github.com/cosmos/cosmos-sdk/x/distribution/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" ) // RegisterRoutes register distribution REST routes. @@ -12,3 +20,35 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, registerQueryRoutes(cliCtx, r, cdc, queryRoute) registerTxRoutes(cliCtx, r, cdc, queryRoute) } + +// ProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. +func ProposalRESTHandler(cliCtx context.CLIContext, cdc *codec.Codec) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "param_change", + Handler: postProposalHandlerFn(cdc, cliCtx), + } +} + +func postProposalHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req distrcutils.CommunityPoolSpendProposalReq + if !rest.ReadRESTReq(w, r, cdc, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + content := distribution.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 + } + + clientrest.WriteGenerateStdTxResponse(w, cdc, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/distribution/client/utils/utils.go b/x/distribution/client/utils/utils.go new file mode 100644 index 00000000000..42fef1f514f --- /dev/null +++ b/x/distribution/client/utils/utils.go @@ -0,0 +1,20 @@ +package utils + +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"` + + Title string `json:"title"` + Description string `json:"description"` + Recipient sdk.AccAddress `json:"recipient"` + Amount sdk.Coins `json:"amount"` + Proposer sdk.AccAddress `json:"proposer"` + Deposit sdk.Coins `json:"deposit"` + } +) diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go index ba610343dac..d9e770ef7be 100644 --- a/x/distribution/simulation/msgs.go +++ b/x/distribution/simulation/msgs.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/simulation" ) @@ -84,3 +85,13 @@ func SimulateMsgWithdrawValidatorCommission(m auth.AccountKeeper, k distribution return opMsg, nil, nil } } + +// SimulateCommunityPoolSpendProposalContent generates random community-pool-spend proposal content +func SimulateCommunityPoolSpendProposalContent(r *rand.Rand) gov.Content { + return distribution.NewCommunityPoolSpendProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + sdk.AccAddress{}, + sdk.Coins{}, + ) +} From 876b46042fd4c1328938a8d635a8077bffe99a5f Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 13:31:54 +0200 Subject: [PATCH 03/13] sdkch entry --- .pending/features/sdk/Community-pool-spend | 1 + 1 file changed, 1 insertion(+) create mode 100644 .pending/features/sdk/Community-pool-spend diff --git a/.pending/features/sdk/Community-pool-spend b/.pending/features/sdk/Community-pool-spend new file mode 100644 index 00000000000..189b5ad436f --- /dev/null +++ b/.pending/features/sdk/Community-pool-spend @@ -0,0 +1 @@ +Community pool spend proposal per governance proposal #7 \ No newline at end of file From a3cd270927c24a550093ae506152a492dcc5b442 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 13:47:56 +0200 Subject: [PATCH 04/13] Misc fixes --- x/bank/simulation/msgs.go | 14 ++------------ x/distribution/alias.go | 3 +++ x/distribution/simulation/msgs.go | 19 ++++++++++++++++--- x/distribution/types/codec.go | 1 + x/gov/simulation/msgs.go | 6 +++--- x/params/simulation/msgs.go | 3 ++- x/simulation/rand_util.go | 9 +++++++++ 7 files changed, 36 insertions(+), 19 deletions(-) diff --git a/x/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index 69ca7f99e8a..327bee2e2fb 100644 --- a/x/bank/simulation/msgs.go +++ b/x/bank/simulation/msgs.go @@ -1,9 +1,7 @@ package simulation import ( - "errors" "fmt" - "math/big" "math/rand" "github.com/tendermint/tendermint/crypto" @@ -55,7 +53,7 @@ func createMsgSend(r *rand.Rand, ctx sdk.Context, accs []simulation.Account, map } denomIndex := r.Intn(len(initFromCoins)) - amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) + amt, goErr := simulation.RandPositiveInt(r, initFromCoins[denomIndex].Amount) if goErr != nil { return fromAcc, "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, msg, false } @@ -150,7 +148,7 @@ func createSingleInputMsgMultiSend(r *rand.Rand, ctx sdk.Context, accs []simulat } denomIndex := r.Intn(len(initFromCoins)) - amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount) + amt, goErr := simulation.RandPositiveInt(r, initFromCoins[denomIndex].Amount) if goErr != nil { return fromAcc, "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, msg, false } @@ -218,11 +216,3 @@ func sendAndVerifyMsgMultiSend(app *baseapp.BaseApp, mapper auth.AccountKeeper, } return nil } - -func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { - if !max.GT(sdk.OneInt()) { - return sdk.Int{}, errors.New("max too small") - } - max = max.Sub(sdk.OneInt()) - return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil -} diff --git a/x/distribution/alias.go b/x/distribution/alias.go index f63826eb602..09bf4ce0ec8 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -31,6 +31,9 @@ type ( // querier response types QueryDelegatorTotalRewardsResponse = types.QueryDelegatorTotalRewardsResponse DelegationDelegatorReward = types.DelegationDelegatorReward + + // proposal types + CommunityPoolSpendProposal = types.CommunityPoolSpendProposal ) const ( diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go index d9e770ef7be..37c105bcf1d 100644 --- a/x/distribution/simulation/msgs.go +++ b/x/distribution/simulation/msgs.go @@ -87,11 +87,24 @@ func SimulateMsgWithdrawValidatorCommission(m auth.AccountKeeper, k distribution } // SimulateCommunityPoolSpendProposalContent generates random community-pool-spend proposal content -func SimulateCommunityPoolSpendProposalContent(r *rand.Rand) gov.Content { +func SimulateCommunityPoolSpendProposalContent(r *rand.Rand, _ *baseapp.BaseApp, _ sdk.Context, accs []simulation.Account) gov.Content { + recipientAcc := simulation.RandomAcc(r, accs) + balance := sdk.Coins{} + denom := "stake" + amount := sdk.NewInt(0) + if len(balance) > 0 { + denomIndex := r.Intn(len(balance)) + amt, goErr := simulation.RandPositiveInt(r, balance[denomIndex].Amount) + if goErr == nil { + amount = amt + denom = balance[denomIndex].Denom + } + } + coins := sdk.NewCoins(sdk.NewCoin(denom, amount.Mul(sdk.NewInt(2)))) return distribution.NewCommunityPoolSpendProposal( simulation.RandStringOfLength(r, 10), simulation.RandStringOfLength(r, 100), - sdk.AccAddress{}, - sdk.Coins{}, + recipientAcc.Address, + coins, ) } diff --git a/x/distribution/types/codec.go b/x/distribution/types/codec.go index b4f509bde95..269e01c4efb 100644 --- a/x/distribution/types/codec.go +++ b/x/distribution/types/codec.go @@ -9,6 +9,7 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgWithdrawDelegatorReward{}, "cosmos-sdk/MsgWithdrawDelegationReward", nil) cdc.RegisterConcrete(MsgWithdrawValidatorCommission{}, "cosmos-sdk/MsgWithdrawValidatorCommission", nil) cdc.RegisterConcrete(MsgSetWithdrawAddress{}, "cosmos-sdk/MsgModifyWithdrawAddress", nil) + cdc.RegisterConcrete(CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal", nil) } // generic sealed codec to be used throughout module diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go index 2202add659b..80fa851deda 100644 --- a/x/gov/simulation/msgs.go +++ b/x/gov/simulation/msgs.go @@ -14,7 +14,7 @@ import ( // ContentSimulator defines a function type alias for generating random proposal // content. -type ContentSimulator func(r *rand.Rand) gov.Content +type ContentSimulator func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) gov.Content // SimulateSubmittingVotingAndSlashingForProposal simulates creating a msg Submit Proposal // voting on the proposal, and subsequently slashing the proposal. It is implemented using @@ -51,7 +51,7 @@ func SimulateSubmittingVotingAndSlashingForProposal(k gov.Keeper, contentSim Con // 1) submit proposal now sender := simulation.RandomAcc(r, accs) - content := contentSim(r) + content := contentSim(r, app, ctx, accs) msg, err := simulationCreateMsgSubmitProposal(r, content, sender) if err != nil { return simulation.NoOpMsg(), nil, err @@ -102,7 +102,7 @@ func simulateHandleMsgSubmitProposal(msg gov.MsgSubmitProposal, handler sdk.Hand } // SimulateTextProposalContent returns random text proposal content. -func SimulateTextProposalContent(r *rand.Rand) gov.Content { +func SimulateTextProposalContent(r *rand.Rand, _ *baseapp.BaseApp, _ sdk.Context, _ []simulation.Account) gov.Content { return gov.NewTextProposal( simulation.RandStringOfLength(r, 140), simulation.RandStringOfLength(r, 5000), diff --git a/x/params/simulation/msgs.go b/x/params/simulation/msgs.go index b198db8573f..3ac4b31531c 100644 --- a/x/params/simulation/msgs.go +++ b/x/params/simulation/msgs.go @@ -5,6 +5,7 @@ import ( "math/rand" "time" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/params" @@ -109,7 +110,7 @@ var paramChangePool = []simParamChange{ // SimulateParamChangeProposalContent returns random parameter change content. // It will generate a ParameterChangeProposal object with anywhere between 1 and // 3 parameter changes all of which have random, but valid values. -func SimulateParamChangeProposalContent(r *rand.Rand) gov.Content { +func SimulateParamChangeProposalContent(r *rand.Rand, _ *baseapp.BaseApp, _ sdk.Context, _ []simulation.Account) gov.Content { numChanges := simulation.RandIntBetween(r, 1, len(paramChangePool)/2) paramChanges := make([]params.ParamChange, numChanges, numChanges) paramChangesKeys := make(map[string]struct{}) diff --git a/x/simulation/rand_util.go b/x/simulation/rand_util.go index 9677f2a612e..b46b0fb7d91 100644 --- a/x/simulation/rand_util.go +++ b/x/simulation/rand_util.go @@ -1,6 +1,7 @@ package simulation import ( + "errors" "math/big" "math/rand" "time" @@ -35,6 +36,14 @@ func RandStringOfLength(r *rand.Rand, n int) string { return string(b) } +func RandPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { + if !max.GT(sdk.OneInt()) { + return sdk.Int{}, errors.New("max too small") + } + max = max.Sub(sdk.OneInt()) + return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil +} + // Generate a random amount // Note: The range of RandomAmount includes max, and is, in fact, biased to return max as well as 0. func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int { From 4e7a541bcabd82d82d8c09d2e083b5966112750c Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 14:01:56 +0200 Subject: [PATCH 05/13] ... --- simapp/sim_test.go | 2 +- x/distribution/keeper/proposal_handler.go | 4 +++ x/distribution/simulation/msgs.go | 37 ++++++++++++----------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/simapp/sim_test.go b/simapp/sim_test.go index 76ff2c3591b..56fd236218d 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -301,7 +301,7 @@ func testAndRunTxs(app *SimApp) []simulation.WeightedOperation { {50, distrsim.SimulateMsgWithdrawDelegatorReward(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper)}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, govsim.SimulateTextProposalContent)}, - {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, distrsim.SimulateCommunityPoolSpendProposalContent)}, + {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, distrsim.SimulateCommunityPoolSpendProposalContent(app.distrKeeper))}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, paramsim.SimulateParamChangeProposalContent)}, {100, govsim.SimulateMsgDeposit(app.govKeeper)}, {100, stakingsim.SimulateMsgCreateValidator(app.accountKeeper, app.stakingKeeper)}, diff --git a/x/distribution/keeper/proposal_handler.go b/x/distribution/keeper/proposal_handler.go index 4347b0a1d09..434147f65db 100644 --- a/x/distribution/keeper/proposal_handler.go +++ b/x/distribution/keeper/proposal_handler.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/distribution/types" ) @@ -16,5 +18,7 @@ func HandleCommunityPoolSpendProposal(ctx sdk.Context, k Keeper, p types.Communi if err != nil { return err } + logger := k.Logger(ctx) + logger.Info(fmt.Sprintf("Spent %s coins from the community pool to recipient %s", p.Amount, p.Recipient)) return nil } diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go index 37c105bcf1d..bd787b30fd6 100644 --- a/x/distribution/simulation/msgs.go +++ b/x/distribution/simulation/msgs.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/gov" + govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" ) @@ -87,24 +88,24 @@ func SimulateMsgWithdrawValidatorCommission(m auth.AccountKeeper, k distribution } // SimulateCommunityPoolSpendProposalContent generates random community-pool-spend proposal content -func SimulateCommunityPoolSpendProposalContent(r *rand.Rand, _ *baseapp.BaseApp, _ sdk.Context, accs []simulation.Account) gov.Content { - recipientAcc := simulation.RandomAcc(r, accs) - balance := sdk.Coins{} - denom := "stake" - amount := sdk.NewInt(0) - if len(balance) > 0 { - denomIndex := r.Intn(len(balance)) - amt, goErr := simulation.RandPositiveInt(r, balance[denomIndex].Amount) - if goErr == nil { - amount = amt - denom = balance[denomIndex].Denom +func SimulateCommunityPoolSpendProposalContent(k distribution.Keeper) govsim.ContentSimulator { + return func(r *rand.Rand, _ *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) gov.Content { + recipientAcc := simulation.RandomAcc(r, accs) + coins := sdk.Coins{} + balance := k.GetFeePool(ctx).CommunityPool + if len(balance) > 0 { + denomIndex := r.Intn(len(balance)) + amount, goErr := simulation.RandPositiveInt(r, balance[denomIndex].Amount.TruncateInt()) + if goErr == nil { + denom := balance[denomIndex].Denom + coins = sdk.NewCoins(sdk.NewCoin(denom, amount.Mul(sdk.NewInt(2)))) + } } + return distribution.NewCommunityPoolSpendProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + recipientAcc.Address, + coins, + ) } - coins := sdk.NewCoins(sdk.NewCoin(denom, amount.Mul(sdk.NewInt(2)))) - return distribution.NewCommunityPoolSpendProposal( - simulation.RandStringOfLength(r, 10), - simulation.RandStringOfLength(r, 100), - recipientAcc.Address, - coins, - ) } From 49ba726ccd29e7e9781be42cfd03cc27e5de17e8 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 15:17:50 +0200 Subject: [PATCH 06/13] Bugfix --- x/distribution/keeper/proposal_handler.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x/distribution/keeper/proposal_handler.go b/x/distribution/keeper/proposal_handler.go index 434147f65db..a8c3a4f596f 100644 --- a/x/distribution/keeper/proposal_handler.go +++ b/x/distribution/keeper/proposal_handler.go @@ -9,10 +9,11 @@ import ( func HandleCommunityPoolSpendProposal(ctx sdk.Context, k Keeper, p types.CommunityPoolSpendProposal) sdk.Error { feePool := k.GetFeePool(ctx) - feePool.CommunityPool = feePool.CommunityPool.Sub(sdk.NewDecCoins(p.Amount)) - if feePool.CommunityPool.IsAnyNegative() { + newPool, negative := feePool.CommunityPool.SafeSub(sdk.NewDecCoins(p.Amount)) + if negative { return types.ErrBadDistribution(k.codespace) } + feePool.CommunityPool = newPool k.SetFeePool(ctx, feePool) _, err := k.bankKeeper.AddCoins(ctx, p.Recipient, p.Amount) if err != nil { From b7052b7db3e1faa47295bc025d2b7b57474704f3 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 15:44:14 +0200 Subject: [PATCH 07/13] Add testcases --- x/distribution/proposal_handler_test.go | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 x/distribution/proposal_handler_test.go diff --git a/x/distribution/proposal_handler_test.go b/x/distribution/proposal_handler_test.go new file mode 100644 index 00000000000..8d26753dcde --- /dev/null +++ b/x/distribution/proposal_handler_test.go @@ -0,0 +1,59 @@ +package distribution + +import ( + "testing" + + "github.com/tendermint/tendermint/crypto/ed25519" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/stretchr/testify/require" +) + +var ( + delPk1 = ed25519.GenPrivKey().PubKey() + delAddr1 = sdk.AccAddress(delPk1.Address()) +) + +func testProposal(recipient sdk.AccAddress, amount sdk.Coins) types.CommunityPoolSpendProposal { + return types.NewCommunityPoolSpendProposal( + "Test", + "description", + recipient, + amount, + ) +} + +func TestProposalHandlerPassed(t *testing.T) { + ctx, accountKeeper, keeper, _, _ := CreateTestInputDefault(t, false, 10) + recipient := delAddr1 + amount := sdk.NewCoin("stake", sdk.NewInt(1)) + + account := accountKeeper.NewAccountWithAddress(ctx, recipient) + require.True(t, account.GetCoins().IsZero()) + accountKeeper.SetAccount(ctx, account) + + feePool := keeper.GetFeePool(ctx) + feePool.CommunityPool = sdk.DecCoins{sdk.NewDecCoinFromCoin(amount)} + keeper.SetFeePool(ctx, feePool) + + tp := testProposal(recipient, sdk.NewCoins(amount)) + hdlr := NewCommunityPoolSpendProposalHandler(keeper) + require.NoError(t, hdlr(ctx, tp)) + require.Equal(t, accountKeeper.GetAccount(ctx, recipient).GetCoins(), sdk.NewCoins(amount)) +} + +func TestProposalHandlerFailed(t *testing.T) { + ctx, accountKeeper, keeper, _, _ := CreateTestInputDefault(t, false, 10) + recipient := delAddr1 + amount := sdk.NewCoin("stake", sdk.NewInt(1)) + + account := accountKeeper.NewAccountWithAddress(ctx, recipient) + require.True(t, account.GetCoins().IsZero()) + accountKeeper.SetAccount(ctx, account) + + tp := testProposal(recipient, sdk.NewCoins(amount)) + hdlr := NewCommunityPoolSpendProposalHandler(keeper) + require.Error(t, hdlr(ctx, tp)) + require.True(t, accountKeeper.GetAccount(ctx, recipient).GetCoins().IsZero()) +} From bb550d08438a8ac5b9bf6afcfbd407b72c2134dd Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 15:48:06 +0200 Subject: [PATCH 08/13] Add more details to log entry --- .pending/features/sdk/Community-pool-spend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pending/features/sdk/Community-pool-spend b/.pending/features/sdk/Community-pool-spend index 189b5ad436f..e899cc22428 100644 --- a/.pending/features/sdk/Community-pool-spend +++ b/.pending/features/sdk/Community-pool-spend @@ -1 +1 @@ -Community pool spend proposal per governance proposal #7 \ No newline at end of file +Community pool spend proposal per Cosmos Hub governance proposal #7 "Activate the Community Pool" From e6d9116ea71f95926d755842d9cbf26c463cae1e Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 15:58:11 +0200 Subject: [PATCH 09/13] Add CLI command --- x/distribution/client/cli/tx.go | 64 ++++++++++++++++++++++++++++ x/distribution/client/utils/utils.go | 28 ++++++++++++ 2 files changed, 92 insertions(+) diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index b101693e827..354752f1595 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -16,8 +16,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/distribution/client/common" + distrcutils "github.com/cosmos/cosmos-sdk/x/distribution/client/utils" "github.com/cosmos/cosmos-sdk/x/distribution/types" ) @@ -150,3 +152,65 @@ $ %s tx set-withdraw-addr cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p --from m } 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": "Staking Param Change", + "description": "Update max validators", + "recipient": "cosmos1s5afhd6gxevu37mkqcvvsj8qeylhn0rz46zdlq", + "amount": [ + { + "denom": "stake", + "amount": "10000" + } + ], + "deposit": [ + { + "denom": "stake", + "amount": "10000" + } + ] +} +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithAccountDecoder(cdc) + + proposal, err := distrcutils.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/utils/utils.go b/x/distribution/client/utils/utils.go index 42fef1f514f..193449478ea 100644 --- a/x/distribution/client/utils/utils.go +++ b/x/distribution/client/utils/utils.go @@ -1,6 +1,9 @@ package utils import ( + "io/ioutil" + + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" ) @@ -17,4 +20,29 @@ type ( Proposer sdk.AccAddress `json:"proposer"` Deposit sdk.Coins `json:"deposit"` } + + // CommunityPoolSpendProposalJSON defines a CommunityPoolSpendProposal with a deposit + CommunityPoolSpendProposalJSON struct { + Title string `json:"title"` + Description string `json:"description"` + Recipient sdk.AccAddress `json:"recipient"` + Amount sdk.Coins `json:"amount"` + Deposit sdk.Coins `json:"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 +} From c4541150fb7fc10f1d981d6ffb762b6d591672c1 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 18:07:40 +0200 Subject: [PATCH 10/13] Update x/distribution/client/rest/rest.go Co-Authored-By: Alexander Bezobchuk --- x/distribution/client/rest/rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index a2641b4a810..1342ece4dfd 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -24,7 +24,7 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, // ProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. func ProposalRESTHandler(cliCtx context.CLIContext, cdc *codec.Codec) govrest.ProposalRESTHandler { return govrest.ProposalRESTHandler{ - SubRoute: "param_change", + SubRoute: "community-pool-spend", Handler: postProposalHandlerFn(cdc, cliCtx), } } From 43c4635a5d657acbdef5a3f4fec88e3b42181038 Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 18:08:46 +0200 Subject: [PATCH 11/13] Address @alexanderbez comment --- x/distribution/client/cli/tx.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index 354752f1595..e5e392b85f2 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -169,8 +169,8 @@ $ %s tx gov submit-proposal community-pool-spend --from= Where proposal.json contains: { - "title": "Staking Param Change", - "description": "Update max validators", + "title": "Community Pool Spend", + "description": "Pay me some Atoms!", "recipient": "cosmos1s5afhd6gxevu37mkqcvvsj8qeylhn0rz46zdlq", "amount": [ { From 7c557eefed7d10db28a68a59dc9dddedcced6f2b Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Mon, 20 May 2019 18:41:40 +0200 Subject: [PATCH 12/13] Use underscores in REST route --- x/distribution/client/rest/rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 1342ece4dfd..5e1651a3461 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -24,7 +24,7 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, // ProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool spend REST handler with a given sub-route. func ProposalRESTHandler(cliCtx context.CLIContext, cdc *codec.Codec) govrest.ProposalRESTHandler { return govrest.ProposalRESTHandler{ - SubRoute: "community-pool-spend", + SubRoute: "community_pool_spend", Handler: postProposalHandlerFn(cdc, cliCtx), } } From a74daafffb48585d56aa5505e3b9c40b3947817c Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Tue, 21 May 2019 10:22:56 +0200 Subject: [PATCH 13/13] Move packages --- x/distribution/client/cli/tx.go | 3 +-- x/distribution/client/{utils => cli}/utils.go | 15 +------------- x/distribution/client/rest/rest.go | 3 +-- x/distribution/client/rest/utils.go | 20 +++++++++++++++++++ 4 files changed, 23 insertions(+), 18 deletions(-) rename x/distribution/client/{utils => cli}/utils.go (65%) create mode 100644 x/distribution/client/rest/utils.go diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index e5e392b85f2..5dc42bba1b0 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -19,7 +19,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/distribution/client/common" - distrcutils "github.com/cosmos/cosmos-sdk/x/distribution/client/utils" "github.com/cosmos/cosmos-sdk/x/distribution/types" ) @@ -195,7 +194,7 @@ Where proposal.json contains: WithCodec(cdc). WithAccountDecoder(cdc) - proposal, err := distrcutils.ParseCommunityPoolSpendProposalJSON(cdc, args[0]) + proposal, err := ParseCommunityPoolSpendProposalJSON(cdc, args[0]) if err != nil { return err } diff --git a/x/distribution/client/utils/utils.go b/x/distribution/client/cli/utils.go similarity index 65% rename from x/distribution/client/utils/utils.go rename to x/distribution/client/cli/utils.go index 193449478ea..a9b52973af1 100644 --- a/x/distribution/client/utils/utils.go +++ b/x/distribution/client/cli/utils.go @@ -1,26 +1,13 @@ -package utils +package cli import ( "io/ioutil" "github.com/cosmos/cosmos-sdk/codec" 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"` - - Title string `json:"title"` - Description string `json:"description"` - Recipient sdk.AccAddress `json:"recipient"` - Amount sdk.Coins `json:"amount"` - Proposer sdk.AccAddress `json:"proposer"` - Deposit sdk.Coins `json:"deposit"` - } - // CommunityPoolSpendProposalJSON defines a CommunityPoolSpendProposal with a deposit CommunityPoolSpendProposalJSON struct { Title string `json:"title"` diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 5e1651a3461..49650c59d9c 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -10,7 +10,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/x/distribution" - distrcutils "github.com/cosmos/cosmos-sdk/x/distribution/client/utils" "github.com/cosmos/cosmos-sdk/x/gov" govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" ) @@ -31,7 +30,7 @@ func ProposalRESTHandler(cliCtx context.CLIContext, cdc *codec.Codec) govrest.Pr func postProposalHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req distrcutils.CommunityPoolSpendProposalReq + var req CommunityPoolSpendProposalReq if !rest.ReadRESTReq(w, r, cdc, &req) { return } diff --git a/x/distribution/client/rest/utils.go b/x/distribution/client/rest/utils.go new file mode 100644 index 00000000000..29e0830e767 --- /dev/null +++ b/x/distribution/client/rest/utils.go @@ -0,0 +1,20 @@ +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"` + + Title string `json:"title"` + Description string `json:"description"` + Recipient sdk.AccAddress `json:"recipient"` + Amount sdk.Coins `json:"amount"` + Proposer sdk.AccAddress `json:"proposer"` + Deposit sdk.Coins `json:"deposit"` + } +)