From dd89c329516e32c309a96bb455761904de0d0cee Mon Sep 17 00:00:00 2001 From: Christopher Goes Date: Tue, 21 May 2019 12:02:10 +0200 Subject: [PATCH] Community pool spend proposal (#4329) Implement the "CommunityPoolSpendProposal" as described in Cosmos Hub proposal 7. Also a useful test of Git flow for merging features passed in governance proposals. --- .pending/features/sdk/Community-pool-spend | 1 + simapp/app.go | 3 +- simapp/sim_test.go | 1 + x/bank/simulation/msgs.go | 14 +--- x/distribution/alias.go | 2 + x/distribution/client/cli/tx.go | 63 ++++++++++++++++++ x/distribution/client/cli/utils.go | 35 ++++++++++ x/distribution/client/rest/rest.go | 39 ++++++++++++ x/distribution/client/rest/utils.go | 20 ++++++ x/distribution/handler.go | 14 ++++ x/distribution/keeper/proposal_handler.go | 25 ++++++++ x/distribution/proposal_handler_test.go | 59 +++++++++++++++++ x/distribution/simulation/msgs.go | 25 ++++++++ x/distribution/types/codec.go | 1 + x/distribution/types/errors.go | 6 ++ x/distribution/types/proposal.go | 74 ++++++++++++++++++++++ x/gov/simulation/msgs.go | 6 +- x/params/simulation/msgs.go | 3 +- x/simulation/rand_util.go | 9 +++ 19 files changed, 383 insertions(+), 17 deletions(-) create mode 100644 .pending/features/sdk/Community-pool-spend create mode 100644 x/distribution/client/cli/utils.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/proposal.go diff --git a/.pending/features/sdk/Community-pool-spend b/.pending/features/sdk/Community-pool-spend new file mode 100644 index 0000000000..e899cc2242 --- /dev/null +++ b/.pending/features/sdk/Community-pool-spend @@ -0,0 +1 @@ +Community pool spend proposal per Cosmos Hub governance proposal #7 "Activate the Community Pool" diff --git a/simapp/app.go b/simapp/app.go index 2ebd16dafd..95036981d6 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 640e3eec9e..56fd236218 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(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/bank/simulation/msgs.go b/x/bank/simulation/msgs.go index 69ca7f99e8..327bee2e2f 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 83407a49b5..e00d007597 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -97,6 +97,7 @@ var ( NewValidatorCurrentRewards = types.NewValidatorCurrentRewards InitialValidatorAccumulatedCommission = types.InitialValidatorAccumulatedCommission NewValidatorSlashEvent = types.NewValidatorSlashEvent + NewCommunityPoolSpendProposal = types.NewCommunityPoolSpendProposal // variable aliases FeePoolKey = keeper.FeePoolKey @@ -145,6 +146,7 @@ type ( ValidatorCurrentRewardsRecord = types.ValidatorCurrentRewardsRecord DelegatorStartingInfoRecord = types.DelegatorStartingInfoRecord ValidatorSlashEventRecord = types.ValidatorSlashEventRecord + CommunityPoolSpendProposal = types.CommunityPoolSpendProposal GenesisState = types.GenesisState MsgSetWithdrawAddress = types.MsgSetWithdrawAddress MsgWithdrawDelegatorReward = types.MsgWithdrawDelegatorReward diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index b101693e82..5dc42bba1b 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -16,6 +16,7 @@ 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" "github.com/cosmos/cosmos-sdk/x/distribution/types" @@ -150,3 +151,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": "Community Pool Spend", + "description": "Pay me some Atoms!", + "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 := 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..a9b52973af --- /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"` + 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 +} diff --git a/x/distribution/client/rest/rest.go b/x/distribution/client/rest/rest.go index 6596ccdf3e..49650c59d9 100644 --- a/x/distribution/client/rest/rest.go +++ b/x/distribution/client/rest/rest.go @@ -2,9 +2,16 @@ 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" + "github.com/cosmos/cosmos-sdk/x/gov" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" ) // RegisterRoutes register distribution REST routes. @@ -12,3 +19,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: "community_pool_spend", + Handler: postProposalHandlerFn(cdc, cliCtx), + } +} + +func postProposalHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req 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/rest/utils.go b/x/distribution/client/rest/utils.go new file mode 100644 index 0000000000..29e0830e76 --- /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"` + } +) diff --git a/x/distribution/handler.go b/x/distribution/handler.go index a0276bf70b..3602bfabb3 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 0000000000..a8c3a4f596 --- /dev/null +++ b/x/distribution/keeper/proposal_handler.go @@ -0,0 +1,25 @@ +package keeper + +import ( + "fmt" + + 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) + 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 { + 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/proposal_handler_test.go b/x/distribution/proposal_handler_test.go new file mode 100644 index 0000000000..8d26753dcd --- /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()) +} diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go index ba610343da..bd787b30fd 100644 --- a/x/distribution/simulation/msgs.go +++ b/x/distribution/simulation/msgs.go @@ -8,6 +8,8 @@ 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" + govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" ) @@ -84,3 +86,26 @@ func SimulateMsgWithdrawValidatorCommission(m auth.AccountKeeper, k distribution return opMsg, nil, nil } } + +// SimulateCommunityPoolSpendProposalContent generates random community-pool-spend proposal content +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, + ) + } +} diff --git a/x/distribution/types/codec.go b/x/distribution/types/codec.go index b4f509bde9..269e01c4ef 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/distribution/types/errors.go b/x/distribution/types/errors.go index dfe988ff34..2b63941141 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 0000000000..068876841a --- /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() +} diff --git a/x/gov/simulation/msgs.go b/x/gov/simulation/msgs.go index 2202add659..80fa851ded 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 b198db8573..3ac4b31531 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 9677f2a612..b46b0fb7d9 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 {