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

Governance split vote #39

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions proto/cosmos/gov/v1beta1/gov.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions proto/cosmos/gov/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions simapp/params/weights.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions x/gov/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
129 changes: 124 additions & 5 deletions x/gov/client/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
})
}
Expand Down Expand Up @@ -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",
Expand All @@ -700,6 +713,7 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
val.Address.String(),
},
true,
types.NewNonSplitVoteOption(types.OptionYes),
},
{
"get vote by wrong voter",
Expand All @@ -708,6 +722,7 @@ func (s *IntegrationTestSuite) TestCmdQueryVote() {
"wrong address",
},
true,
types.NewNonSplitVoteOption(types.OptionYes),
},
{
"vote for valid proposal",
Expand All @@ -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)},
},
},
}

Expand All @@ -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))
}
}
})
}
Expand Down Expand Up @@ -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
Expand Up @@ -68,6 +68,7 @@ func NewTxCmd(propCmds []*cobra.Command) *cobra.Command {
govTxCmd.AddCommand(
NewCmdDeposit(),
NewCmdVote(),
NewCmdWeightedVote(),
cmdSubmitProp,
)

Expand Down Expand Up @@ -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
`,
Expand Down Expand Up @@ -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