Skip to content

Commit

Permalink
Community pool spend proposal (cosmos#4329)
Browse files Browse the repository at this point in the history
Implement the "CommunityPoolSpendProposal" as described in Cosmos Hub proposal 7.

Also a useful test of Git flow for merging features passed in governance proposals.
  • Loading branch information
cwgoes authored and Alessio Treglia committed May 21, 2019
1 parent 4b872d2 commit dd89c32
Show file tree
Hide file tree
Showing 19 changed files with 383 additions and 17 deletions.
1 change: 1 addition & 0 deletions .pending/features/sdk/Community-pool-spend
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Community pool spend proposal per Cosmos Hub governance proposal #7 "Activate the Community Pool"
3 changes: 2 additions & 1 deletion simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions simapp/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
Expand Down
14 changes: 2 additions & 12 deletions x/bank/simulation/msgs.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package simulation

import (
"errors"
"fmt"
"math/big"
"math/rand"

"github.com/tendermint/tendermint/crypto"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions x/distribution/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ var (
NewValidatorCurrentRewards = types.NewValidatorCurrentRewards
InitialValidatorAccumulatedCommission = types.InitialValidatorAccumulatedCommission
NewValidatorSlashEvent = types.NewValidatorSlashEvent
NewCommunityPoolSpendProposal = types.NewCommunityPoolSpendProposal

// variable aliases
FeePoolKey = keeper.FeePoolKey
Expand Down Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions x/distribution/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 <path/to/proposal.json> --from=<key_or_address>
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
}
35 changes: 35 additions & 0 deletions x/distribution/client/cli/utils.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions x/distribution/client/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,52 @@ 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.
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) {
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})
}
}
20 changes: 20 additions & 0 deletions x/distribution/client/rest/utils.go
Original file line number Diff line number Diff line change
@@ -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"`
}
)
14 changes: 14 additions & 0 deletions x/distribution/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
25 changes: 25 additions & 0 deletions x/distribution/keeper/proposal_handler.go
Original file line number Diff line number Diff line change
@@ -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
}
59 changes: 59 additions & 0 deletions x/distribution/proposal_handler_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading

0 comments on commit dd89c32

Please sign in to comment.