Skip to content

Commit

Permalink
Merge pull request cosmos#39 from antstalepresh/gov_split_vote
Browse files Browse the repository at this point in the history
Governance split vote
antstalepresh authored Nov 4, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 09d977e + 381d1b0 commit 2671482
Showing 34 changed files with 1,620 additions and 274 deletions.
16 changes: 13 additions & 3 deletions proto/cosmos/gov/v1beta1/gov.proto
Original file line number Diff line number Diff line change
@@ -29,6 +29,16 @@ enum VoteOption {
VOTE_OPTION_NO_WITH_VETO = 4 [(gogoproto.enumvalue_customname) = "OptionNoWithVeto"];
}

// WeightedVoteOption defines a unit of vote for vote split.
message WeightedVoteOption {
VoteOption option = 1;
string weight = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"weight\""
];
}

// TextProposal defines a standard text proposal whose changes need to be
// manually updated in case of approval.
message TextProposal {
@@ -119,9 +129,9 @@ message Vote {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.equal) = false;

uint64 proposal_id = 1 [(gogoproto.moretags) = "yaml:\"proposal_id\""];
string voter = 2;
VoteOption option = 3;
uint64 proposal_id = 1 [(gogoproto.moretags) = "yaml:\"proposal_id\""];
string voter = 2;
repeated WeightedVoteOption options = 3 [(gogoproto.nullable) = false];
}

// DepositParams defines the params for deposits on governance proposals.
18 changes: 18 additions & 0 deletions proto/cosmos/gov/v1beta1/tx.proto
Original file line number Diff line number Diff line change
@@ -17,6 +17,9 @@ service Msg {
// Vote defines a method to add a vote on a specific proposal.
rpc Vote(MsgVote) returns (MsgVoteResponse);

// WeightedVote defines a method to add a weighted vote on a specific proposal.
rpc WeightedVote(MsgWeightedVote) returns (MsgWeightedVoteResponse);

// Deposit defines a method to add deposit on a specific proposal.
rpc Deposit(MsgDeposit) returns (MsgDepositResponse);
}
@@ -55,9 +58,24 @@ message MsgVote {
VoteOption option = 3;
}

// MsgVote defines a message to cast a vote.
message MsgWeightedVote {
option (gogoproto.equal) = false;
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
option (gogoproto.goproto_getters) = false;

uint64 proposal_id = 1 [(gogoproto.jsontag) = "proposal_id", (gogoproto.moretags) = "yaml:\"proposal_id\""];
string voter = 2;
repeated WeightedVoteOption options = 3 [(gogoproto.nullable) = false];
}

// MsgVoteResponse defines the Msg/Vote response type.
message MsgVoteResponse {}

// MsgWeightedVoteResponse defines the MsgWeightedVote response type.
message MsgWeightedVoteResponse {}

// MsgDeposit defines a message to submit a deposit to an existing proposal.
message MsgDeposit {
option (gogoproto.equal) = false;
1 change: 1 addition & 0 deletions simapp/params/weights.go
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ const (
DefaultWeightMsgFundCommunityPool int = 50
DefaultWeightMsgDeposit int = 100
DefaultWeightMsgVote int = 67
DefaultWeightMsgWeightedVote int = 33
DefaultWeightMsgUnjail int = 100
DefaultWeightMsgCreateValidator int = 100
DefaultWeightMsgEditValidator int = 5
4 changes: 2 additions & 2 deletions x/gov/abci_test.go
Original file line number Diff line number Diff line change
@@ -307,7 +307,7 @@ func TestProposalPassedEndblocker(t *testing.T) {
deposits := initialModuleAccCoins.Add(proposal.TotalDeposit...).Add(proposalCoins...)
require.True(t, moduleAccCoins.IsEqual(deposits))

err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.OptionYes)
err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes))
require.NoError(t, err)

newHeader := ctx.BlockHeader()
@@ -348,7 +348,7 @@ func TestEndBlockerProposalHandlerFailed(t *testing.T) {

handleAndCheck(t, gov.NewHandler(app.GovKeeper), ctx, newDepositMsg)

err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.OptionYes)
err = app.GovKeeper.AddVote(ctx, proposal.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes))
require.NoError(t, err)

newHeader := ctx.BlockHeader()
129 changes: 124 additions & 5 deletions x/gov/client/cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -60,6 +60,18 @@ func (s *IntegrationTestSuite) SetupSuite() {
s.Require().NoError(err)
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)

// create a proposal3 with deposit
_, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(),
"Text Proposal 3", "Where is the title!?", types.ProposalTypeText,
fmt.Sprintf("--%s=%s", cli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, types.DefaultMinDepositTokens).String()))
s.Require().NoError(err)
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)

// vote for proposal3 as val
_, err = govtestutil.MsgVote(val.ClientCtx, val.Address.String(), "3", "yes=0.6,no=0.3,abstain=0.05,no_with_veto=0.05")
s.Require().NoError(err)
}

func (s *IntegrationTestSuite) TearDownSuite() {
@@ -448,7 +460,7 @@ func (s *IntegrationTestSuite) TestCmdGetProposals() {
var proposals types.QueryProposalsResponse

s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &proposals), out.String())
s.Require().Len(proposals.Proposals, 2)
s.Require().Len(proposals.Proposals, 3)
}
})
}
@@ -689,9 +701,10 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
val := s.network.Validators[0]

testCases := []struct {
name string
args []string
expectErr bool
name string
args []string
expectErr bool
expVoteOptions types.WeightedVoteOptions
}{
{
"get vote of non existing proposal",
@@ -700,6 +713,7 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
val.Address.String(),
},
true,
types.NewNonSplitVoteOption(types.OptionYes),
},
{
"get vote by wrong voter",
@@ -708,6 +722,7 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
"wrong address",
},
true,
types.NewNonSplitVoteOption(types.OptionYes),
},
{
"vote for valid proposal",
@@ -717,6 +732,22 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false,
types.NewNonSplitVoteOption(types.OptionYes),
},
{
"split vote for valid proposal",
[]string{
"3",
val.Address.String(),
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
},
false,
types.WeightedVoteOptions{
types.WeightedVoteOption{Option: types.OptionYes, Weight: sdk.NewDecWithPrec(60, 2)},
types.WeightedVoteOption{Option: types.OptionNo, Weight: sdk.NewDecWithPrec(30, 2)},
types.WeightedVoteOption{Option: types.OptionAbstain, Weight: sdk.NewDecWithPrec(5, 2)},
types.WeightedVoteOption{Option: types.OptionNoWithVeto, Weight: sdk.NewDecWithPrec(5, 2)},
},
},
}

@@ -735,7 +766,11 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {

var vote types.Vote
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &vote), out.String())
s.Require().Equal(types.OptionYes, vote.Option)
s.Require().Equal(len(vote.Options), len(tc.expVoteOptions))
for i, option := range tc.expVoteOptions {
s.Require().Equal(option.Option, vote.Options[i].Option)
s.Require().True(option.Weight.Equal(vote.Options[i].Weight))
}
}
})
}
@@ -801,6 +836,90 @@ func (s *IntegrationTestSuite) TestNewCmdVote() {
}
}

func (s *IntegrationTestSuite) TestNewCmdWeightedVote() {
val := s.network.Validators[0]

testCases := []struct {
name string
args []string
expectErr bool
expectedCode uint32
}{
{
"invalid vote",
[]string{},
true, 0,
},
{
"vote for invalid proposal",
[]string{
"10",
fmt.Sprintf("%s", "yes"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
false, 2,
},
{
"valid vote",
[]string{
"1",
fmt.Sprintf("%s", "yes"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
false, 0,
},
{
"invalid valid split vote string",
[]string{
"1",
fmt.Sprintf("%s", "yes/0.6,no/0.3,abstain/0.05,no_with_veto/0.05"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
true, 0,
},
{
"valid split vote",
[]string{
"1",
fmt.Sprintf("%s", "yes=0.6,no=0.3,abstain=0.05,no_with_veto=0.05"),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
false, 0,
},
}

for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.NewCmdWeightedVote()
clientCtx := val.ClientCtx
var txResp sdk.TxResponse

out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)

if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txResp), out.String())
s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
}
})
}
}

func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
56 changes: 55 additions & 1 deletion x/gov/client/cli/tx.go
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ func NewTxCmd(propCmds []*cobra.Command) *cobra.Command {
govTxCmd.AddCommand(
NewCmdDeposit(),
NewCmdVote(),
NewCmdWeightedVote(),
cmdSubmitProp,
)

@@ -207,7 +208,6 @@ func NewCmdVote() *cobra.Command {
fmt.Sprintf(`Submit a vote for an active proposal. You can
find the proposal-id by running "%s query gov proposals".
Example:
$ %s tx gov vote 1 yes --from mykey
`,
@@ -251,3 +251,57 @@ $ %s tx gov vote 1 yes --from mykey

return cmd
}

// NewCmdWeightedVote implements creating a new weighted vote command.
func NewCmdWeightedVote() *cobra.Command {
cmd := &cobra.Command{
Use: "weighted-vote [proposal-id] [weighted-options]",
Args: cobra.ExactArgs(2),
Short: "Vote for an active proposal, options: yes/no/no_with_veto/abstain",
Long: strings.TrimSpace(
fmt.Sprintf(`Submit a vote for an active proposal. You can
find the proposal-id by running "%s query gov proposals".
Example:
$ %s tx gov vote 1 yes=0.6,no=0.3,abstain=0.05,no_with_veto=0.05 --from mykey
`,
version.AppName, version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
clientCtx, err := client.ReadTxCommandFlags(clientCtx, cmd.Flags())
if err != nil {
return err
}

// Get voting address
from := clientCtx.GetFromAddress()

// validate that the proposal id is a uint
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
}

// Figure out which vote options user chose
options, err := types.WeightedVoteOptionsFromString(govutils.NormalizeWeightedVoteOptions(args[1]))
if err != nil {
return err
}

// Build vote message and run basic validation
msg := types.NewMsgWeightedVote(from, proposalID, options)
err = msg.ValidateBasic()
if err != nil {
return err
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
Loading

0 comments on commit 2671482

Please sign in to comment.