Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Community pool spend proposal #4329

Merged
merged 17 commits into from
May 21, 2019
Merged
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))
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
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))},
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
{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)
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
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
64 changes: 64 additions & 0 deletions x/distribution/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 <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 := distrcutils.ParseCommunityPoolSpendProposalJSON(cdc, args[0])
if err != nil {
return err
}

from := cliCtx.GetFromAddress()
content := types.NewCommunityPoolSpendProposal(proposal.Title, proposal.Description, proposal.Recipient, proposal.Amount)
rigelrozanski marked this conversation as resolved.
Show resolved Hide resolved

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
}
40 changes: 40 additions & 0 deletions x/distribution/client/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,53 @@ 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.
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 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})
}
}
48 changes: 48 additions & 0 deletions x/distribution/client/utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package utils
cwgoes marked this conversation as resolved.
Show resolved Hide resolved

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"`
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
}
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))
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
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))
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
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