diff --git a/codec/types/any.pb.go b/codec/types/any.pb.go index 97d9f1c2aa64..94cf80216287 100644 --- a/codec/types/any.pb.go +++ b/codec/types/any.pb.go @@ -476,10 +476,7 @@ func (m *Any) Unmarshal(dAtA []byte) error { if err != nil { return err } - if skippy < 0 { - return ErrInvalidLengthAny - } - if (iNdEx + skippy) < 0 { + if (skippy < 0) || (iNdEx+skippy) < 0 { return ErrInvalidLengthAny } if (iNdEx + skippy) > l { diff --git a/proto/cosmos/gov/v1beta1/gov.proto b/proto/cosmos/gov/v1beta1/gov.proto index 1d72e643212c..e874ff582509 100644 --- a/proto/cosmos/gov/v1beta1/gov.proto +++ b/proto/cosmos/gov/v1beta1/gov.proto @@ -36,8 +36,9 @@ message TextProposal { option (gogoproto.equal) = true; - string title = 1; - string description = 2; + string title = 1; + string description = 2; + string tallystrategy = 3; } // Deposit defines an amount deposited by an account address to an active diff --git a/simapp/app.go b/simapp/app.go index b73538df032d..5ec3428fb61b 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -58,6 +58,7 @@ import ( genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" "github.com/cosmos/cosmos-sdk/x/gov" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + "github.com/cosmos/cosmos-sdk/x/tallystrategies/stakingtally" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" transfer "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer" ibctransferkeeper "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/keeper" @@ -289,11 +290,18 @@ func NewSimApp( AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)). AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)). AddRoute(ibchost.RouterKey, ibcclient.NewClientUpdateProposalHandler(app.IBCKeeper.ClientKeeper)) + + // routes are added to tallyrouter after creation of govkeeper, so the govkeeper can be passed into tally routes + tallyRouter := govtypes.NewTallyRouter() + app.GovKeeper = govkeeper.NewKeeper( appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper, - &stakingKeeper, govRouter, + &stakingKeeper, govRouter, tallyRouter, ) + tallyRouter.AddRoute(govtypes.RootTallyRoute, stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper)) + tallyRouter.AddRoute(stakingtally.TallyRoute, stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper)) + // Create Transfer Keepers app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, keys[ibctransfertypes.StoreKey], app.GetSubspace(ibctransfertypes.ModuleName), diff --git a/types/simulation/types.go b/types/simulation/types.go index f541b1d764de..10449517d37b 100644 --- a/types/simulation/types.go +++ b/types/simulation/types.go @@ -23,6 +23,7 @@ type Content interface { GetDescription() string ProposalRoute() string ProposalType() string + TallyRoute() string ValidateBasic() error String() string } diff --git a/x/distribution/types/proposal.go b/x/distribution/types/proposal.go index 1a0d0a886ed7..cb61fb6234e0 100644 --- a/x/distribution/types/proposal.go +++ b/x/distribution/types/proposal.go @@ -39,6 +39,10 @@ func (csp *CommunityPoolSpendProposal) ProposalRoute() string { return RouterKey // ProposalType returns the type of a community pool spend proposal. func (csp *CommunityPoolSpendProposal) ProposalType() string { return ProposalTypeCommunityPoolSpend } +// TallyRoute returns the tally route of the strategy that should weight votes +// on a community pool spend proposal. +func (csp *CommunityPoolSpendProposal) TallyRoute() string { return govtypes.RootTallyRoute } + // ValidateBasic runs basic stateless validity checks func (csp *CommunityPoolSpendProposal) ValidateBasic() error { err := govtypes.ValidateAbstract(csp) diff --git a/x/gov/abci_test.go b/x/gov/abci_test.go index bfda1b3590b6..d6c217eb71db 100644 --- a/x/gov/abci_test.go +++ b/x/gov/abci_test.go @@ -31,7 +31,7 @@ func TestTickExpiredDepositPeriod(t *testing.T) { inactiveQueue.Close() newProposalMsg, err := types.NewMsgSubmitProposal( - types.ContentFromProposalType("test", "test", types.ProposalTypeText), + types.ContentFromProposalType("test", "test", types.ProposalTypeText, types.RootTallyRoute), sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, addrs[0], ) @@ -83,7 +83,7 @@ func TestTickMultipleExpiredDepositPeriod(t *testing.T) { inactiveQueue.Close() newProposalMsg, err := types.NewMsgSubmitProposal( - types.ContentFromProposalType("test", "test", types.ProposalTypeText), + types.ContentFromProposalType("test", "test", types.ProposalTypeText, types.RootTallyRoute), sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, addrs[0], ) @@ -106,7 +106,7 @@ func TestTickMultipleExpiredDepositPeriod(t *testing.T) { inactiveQueue.Close() newProposalMsg2, err := types.NewMsgSubmitProposal( - types.ContentFromProposalType("test2", "test2", types.ProposalTypeText), + types.ContentFromProposalType("test2", "test2", types.ProposalTypeText, types.RootTallyRoute), sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, addrs[0], ) @@ -163,7 +163,7 @@ func TestTickPassedDepositPeriod(t *testing.T) { activeQueue.Close() newProposalMsg, err := types.NewMsgSubmitProposal( - types.ContentFromProposalType("test2", "test2", types.ProposalTypeText), + types.ContentFromProposalType("test2", "test2", types.ProposalTypeText, types.RootTallyRoute), sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, addrs[0], ) diff --git a/x/gov/client/cli/cli_test.go b/x/gov/client/cli/cli_test.go index 981b13024c47..74158e1d4741 100644 --- a/x/gov/client/cli/cli_test.go +++ b/x/gov/client/cli/cli_test.go @@ -45,7 +45,7 @@ func (s *IntegrationTestSuite) SetupSuite() { // create a proposal with deposit _, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(), - "Text Proposal 1", "Where is the title!?", types.ProposalTypeText, + "Text Proposal 1", "Where is the title!?", types.ProposalTypeText, types.RootTallyRoute, fmt.Sprintf("--%s=%s", cli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, types.DefaultMinDepositTokens).String())) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) @@ -57,7 +57,7 @@ func (s *IntegrationTestSuite) SetupSuite() { // create a proposal without deposit _, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(), - "Text Proposal 2", "Where is the title!?", types.ProposalTypeText) + "Text Proposal 2", "Where is the title!?", types.ProposalTypeText, types.RootTallyRoute) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) s.Require().NoError(err) diff --git a/x/gov/client/cli/parse.go b/x/gov/client/cli/parse.go index 90066b645abe..1b8001428d83 100644 --- a/x/gov/client/cli/parse.go +++ b/x/gov/client/cli/parse.go @@ -21,6 +21,7 @@ func parseSubmitProposalFlags(fs *pflag.FlagSet) (*proposal, error) { proposal.Description, _ = fs.GetString(FlagDescription) proposal.Type = govutils.NormalizeProposalType(proposalType) proposal.Deposit, _ = fs.GetString(FlagDeposit) + proposal.TallyRoute, _ = fs.GetString(FlagTallyRoute) return proposal, nil } diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index 16ca7f7c68bc..e4068c377372 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -26,6 +26,7 @@ const ( flagDepositor = "depositor" flagStatus = "status" FlagProposal = "proposal" + FlagTallyRoute = "tallyroute" ) type proposal struct { @@ -33,6 +34,7 @@ type proposal struct { Description string Type string Deposit string + TallyRoute string } // ProposalFlags defines the core required fields of a proposal. It is used to @@ -43,6 +45,7 @@ var ProposalFlags = []string{ FlagDescription, FlagProposalType, FlagDeposit, + FlagTallyRoute, } // NewTxCmd returns the transaction commands for this module @@ -97,7 +100,7 @@ Where proposal.json contains: Which is equivalent to: -$ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome proposal" --type="Text" --deposit="10test" --from mykey +$ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome proposal" --type="Text" --deposit="10test" --tallyroute="staking" --from mykey `, version.AppName, version.AppName, ), @@ -118,7 +121,7 @@ $ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome pr return err } - content := types.ContentFromProposalType(proposal.Title, proposal.Description, proposal.Type) + content := types.ContentFromProposalType(proposal.Title, proposal.Description, proposal.Type, proposal.TallyRoute) msg, err := types.NewMsgSubmitProposal(content, amount, clientCtx.GetFromAddress()) if err != nil { @@ -138,6 +141,7 @@ $ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome pr cmd.Flags().String(FlagProposalType, "", "The proposal Type") cmd.Flags().String(FlagDeposit, "", "The proposal deposit") cmd.Flags().String(FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + cmd.Flags().String(FlagTallyRoute, "", "Tally route for requested tally strategy") flags.AddTxFlagsToCmd(cmd) return cmd diff --git a/x/gov/client/rest/grpc_query_test.go b/x/gov/client/rest/grpc_query_test.go index 8e2d4efa9ffa..6b2d58c7b672 100644 --- a/x/gov/client/rest/grpc_query_test.go +++ b/x/gov/client/rest/grpc_query_test.go @@ -41,7 +41,7 @@ func (s *IntegrationTestSuite) SetupSuite() { // create a proposal with deposit _, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(), - "Text Proposal 1", "Where is the title!?", types.ProposalTypeText, + "Text Proposal 1", "Where is the title!?", types.ProposalTypeText, types.RootTallyRoute, fmt.Sprintf("--%s=%s", cli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, types.DefaultMinDepositTokens).String())) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) @@ -53,7 +53,7 @@ func (s *IntegrationTestSuite) SetupSuite() { // create a proposal without deposit _, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(), - "Text Proposal 2", "Where is the title!?", types.ProposalTypeText) + "Text Proposal 2", "Where is the title!?", types.ProposalTypeText, types.RootTallyRoute) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) s.Require().NoError(err) diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index f11798e96760..4193f8b29ef9 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -43,6 +43,7 @@ type PostProposalReq struct { ProposalType string `json:"proposal_type" yaml:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal } Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` // Address of the proposer InitialDeposit sdk.Coins `json:"initial_deposit" yaml:"initial_deposit"` // Coins to add to the proposal's deposit + TallyRoute string `json:"tally_route" yaml:"tally_route"` // Route for Tally Strategy to use } // DepositReq defines the properties of a deposit request's body. diff --git a/x/gov/client/rest/tx.go b/x/gov/client/rest/tx.go index 284c67148170..9b0136b786fb 100644 --- a/x/gov/client/rest/tx.go +++ b/x/gov/client/rest/tx.go @@ -37,7 +37,7 @@ func newPostProposalHandlerFn(clientCtx client.Context) http.HandlerFunc { } proposalType := gcutils.NormalizeProposalType(req.ProposalType) - content := types.ContentFromProposalType(req.Title, req.Description, proposalType) + content := types.ContentFromProposalType(req.Title, req.Description, proposalType, req.TallyRoute) msg, err := types.NewMsgSubmitProposal(content, req.InitialDeposit, req.Proposer) if rest.CheckBadRequestError(w, err) { diff --git a/x/gov/client/testutil/helpers.go b/x/gov/client/testutil/helpers.go index 535cb8d09ca6..171624b8e67f 100644 --- a/x/gov/client/testutil/helpers.go +++ b/x/gov/client/testutil/helpers.go @@ -18,11 +18,12 @@ var commonArgs = []string{ } // MsgSubmitProposal creates a tx for submit proposal -func MsgSubmitProposal(clientCtx client.Context, from, title, description, proposalType string, extraArgs ...string) (testutil.BufferWriter, error) { +func MsgSubmitProposal(clientCtx client.Context, from, title, description, proposalType, tallyRoute string, extraArgs ...string) (testutil.BufferWriter, error) { args := append([]string{ fmt.Sprintf("--%s=%s", govcli.FlagTitle, title), fmt.Sprintf("--%s=%s", govcli.FlagDescription, description), fmt.Sprintf("--%s=%s", govcli.FlagProposalType, proposalType), + fmt.Sprintf("--%s=%s", govcli.FlagTallyRoute, tallyRoute), fmt.Sprintf("--%s=%s", flags.FlagFrom, from), }, commonArgs...) diff --git a/x/gov/common_test.go b/x/gov/common_test.go index 5ef50ea5db03..dd8db64e51f1 100644 --- a/x/gov/common_test.go +++ b/x/gov/common_test.go @@ -17,7 +17,7 @@ import ( var ( valTokens = sdk.TokensFromConsensusPower(42) - TestProposal = types.NewTextProposal("Test", "description") + TestProposal = types.NewTextProposal("Test", "description", "staking") TestDescription = stakingtypes.NewDescription("T", "E", "S", "T", "Z") TestCommissionRates = stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) ) diff --git a/x/gov/keeper/common_test.go b/x/gov/keeper/common_test.go index c7516eb69773..b402db0bace3 100644 --- a/x/gov/keeper/common_test.go +++ b/x/gov/keeper/common_test.go @@ -14,7 +14,9 @@ import ( ) var ( - TestProposal = types.NewTextProposal("Test", "description") + TestProposal = types.NewTextProposal("Test", "description", "staking") + TestProposalNoRoute = types.NewTextProposal("Test", "description", "") + TestProposalRandRoute = types.NewTextProposal("Test", "description", "Rand") ) func createValidators(t *testing.T, ctx sdk.Context, app *simapp.SimApp, powers []int64) ([]sdk.AccAddress, []sdk.ValAddress) { diff --git a/x/gov/keeper/grpc_query_test.go b/x/gov/keeper/grpc_query_test.go index dc330d734829..2993132b5955 100644 --- a/x/gov/keeper/grpc_query_test.go +++ b/x/gov/keeper/grpc_query_test.go @@ -49,7 +49,7 @@ func (suite *KeeperTestSuite) TestGRPCQueryProposal() { "valid request", func() { req = &types.QueryProposalRequest{ProposalId: 1} - testProposal := types.NewTextProposal("Proposal", "testing proposal") + testProposal := types.NewTextProposal("Proposal", "testing proposal", "staking") submittedProposal, err := app.GovKeeper.SubmitProposal(ctx, testProposal) suite.Require().NoError(err) suite.Require().NotEmpty(submittedProposal) @@ -105,7 +105,7 @@ func (suite *KeeperTestSuite) TestGRPCQueryProposals() { // create 5 test proposals for i := 0; i < 5; i++ { num := strconv.Itoa(i + 1) - testProposal := types.NewTextProposal("Proposal"+num, "testing proposal "+num) + testProposal := types.NewTextProposal("Proposal"+num, "testing proposal "+num, "staking") proposal, err := app.GovKeeper.SubmitProposal(ctx, testProposal) suite.Require().NotEmpty(proposal) suite.Require().NoError(err) diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go index 60db99d32c11..d69a889bfa8f 100644 --- a/x/gov/keeper/keeper.go +++ b/x/gov/keeper/keeper.go @@ -31,6 +31,9 @@ type Keeper struct { // Proposal router router types.Router + + // Tally router + tallyrouter types.TallyRouter } // NewKeeper returns a governance keeper. It handles: @@ -42,7 +45,7 @@ type Keeper struct { // CONTRACT: the parameter Subspace must have the param key table already initialized func NewKeeper( cdc codec.BinaryMarshaler, key sdk.StoreKey, paramSpace types.ParamSubspace, - authKeeper types.AccountKeeper, bankKeeper types.BankKeeper, sk types.StakingKeeper, rtr types.Router, + authKeeper types.AccountKeeper, bankKeeper types.BankKeeper, sk types.StakingKeeper, rtr types.Router, tallyrtr types.TallyRouter, ) Keeper { // ensure governance module account is set @@ -56,13 +59,14 @@ func NewKeeper( rtr.Seal() return Keeper{ - storeKey: key, - paramSpace: paramSpace, - authKeeper: authKeeper, - bankKeeper: bankKeeper, - sk: sk, - cdc: cdc, - router: rtr, + storeKey: key, + paramSpace: paramSpace, + authKeeper: authKeeper, + bankKeeper: bankKeeper, + sk: sk, + cdc: cdc, + router: rtr, + tallyrouter: tallyrtr, } } @@ -76,6 +80,11 @@ func (keeper Keeper) Router() types.Router { return keeper.router } +// TallyRouter returns the gov Keeper's Router +func (keeper Keeper) TallyRouter() types.TallyRouter { + return keeper.tallyrouter +} + // GetGovernanceAccount returns the governance ModuleAccount func (keeper Keeper) GetGovernanceAccount(ctx sdk.Context) authtypes.ModuleAccountI { return keeper.authKeeper.GetModuleAccount(ctx, types.ModuleName) diff --git a/x/gov/keeper/proposal.go b/x/gov/keeper/proposal.go index 14a324d78078..dde85318aa38 100644 --- a/x/gov/keeper/proposal.go +++ b/x/gov/keeper/proposal.go @@ -15,6 +15,10 @@ func (keeper Keeper) SubmitProposal(ctx sdk.Context, content types.Content) (typ return types.Proposal{}, sdkerrors.Wrap(types.ErrNoProposalHandlerExists, content.ProposalRoute()) } + if keeper.TallyRouter().GetRoute(content.TallyRoute()) == nil { + return types.Proposal{}, sdkerrors.Wrap(types.ErrInvalidTallyRoute, content.TallyRoute()) + } + // Execute the proposal content in a new context branch (with branched store) // to validate the actual parameter changes before the proposal proceeds // through the governance process. State is not persisted. diff --git a/x/gov/keeper/proposal_test.go b/x/gov/keeper/proposal_test.go index 1a833737d940..a3b441c95ec7 100644 --- a/x/gov/keeper/proposal_test.go +++ b/x/gov/keeper/proposal_test.go @@ -67,14 +67,15 @@ func TestSubmitProposal(t *testing.T) { content types.Content expectedErr error }{ - {&types.TextProposal{Title: "title", Description: "description"}, nil}, + {&types.TextProposal{Title: "title", Description: "description", Tallystrategy: "root"}, nil}, // Keeper does not check the validity of title and description, no error - {&types.TextProposal{Title: "", Description: "description"}, nil}, - {&types.TextProposal{Title: strings.Repeat("1234567890", 100), Description: "description"}, nil}, - {&types.TextProposal{Title: "title", Description: ""}, nil}, - {&types.TextProposal{Title: "title", Description: strings.Repeat("1234567890", 1000)}, nil}, + {&types.TextProposal{Title: "", Description: "description", Tallystrategy: "root"}, nil}, + {&types.TextProposal{Title: strings.Repeat("1234567890", 100), Description: "description", Tallystrategy: "root"}, nil}, + {&types.TextProposal{Title: "title", Description: "", Tallystrategy: "root"}, nil}, + {&types.TextProposal{Title: "title", Description: strings.Repeat("1234567890", 1000), Tallystrategy: "root"}, nil}, // error only when invalid route {&invalidProposalRoute{}, types.ErrNoProposalHandlerExists}, + {&types.TextProposal{Title: "title", Description: "description", Tallystrategy: ""}, types.ErrInvalidTallyRoute}, } for i, tc := range testCases { diff --git a/x/gov/keeper/tally.go b/x/gov/keeper/tally.go index cddfd59027dd..ef03c25bbf44 100644 --- a/x/gov/keeper/tally.go +++ b/x/gov/keeper/tally.go @@ -3,117 +3,12 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) -// TODO: Break into several smaller functions for clarity - -// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the -// voters -func (keeper Keeper) Tally(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { - results := make(map[types.VoteOption]sdk.Dec) - results[types.OptionYes] = sdk.ZeroDec() - results[types.OptionAbstain] = sdk.ZeroDec() - results[types.OptionNo] = sdk.ZeroDec() - results[types.OptionNoWithVeto] = sdk.ZeroDec() - - totalVotingPower := sdk.ZeroDec() - currValidators := make(map[string]types.ValidatorGovInfo) - - // fetch all the bonded validators, insert them into currValidators - keeper.sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) { - currValidators[validator.GetOperator().String()] = types.NewValidatorGovInfo( - validator.GetOperator(), - validator.GetBondedTokens(), - validator.GetDelegatorShares(), - sdk.ZeroDec(), - types.OptionEmpty, - ) - - return false - }) - - keeper.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool { - // if validator, just record it in the map - voter, err := sdk.AccAddressFromBech32(vote.Voter) - - if err != nil { - panic(err) - } - - valAddrStr := sdk.ValAddress(voter.Bytes()).String() - if val, ok := currValidators[valAddrStr]; ok { - val.Vote = vote.Option - currValidators[valAddrStr] = val - } - - // iterate over all delegations from voter, deduct from any delegated-to validators - keeper.sk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) { - valAddrStr := delegation.GetValidatorAddr().String() - - if val, ok := currValidators[valAddrStr]; ok { - // There is no need to handle the special case that validator address equal to voter address. - // Because voter's voting power will tally again even if there will deduct voter's voting power from validator. - val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares()) - currValidators[valAddrStr] = val - - // delegation shares * bonded / total shares - votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares) - - results[vote.Option] = results[vote.Option].Add(votingPower) - totalVotingPower = totalVotingPower.Add(votingPower) - } - - return false - }) - - keeper.deleteVote(ctx, vote.ProposalId, voter) - return false - }) - - // iterate over the validators again to tally their voting power - for _, val := range currValidators { - if val.Vote == types.OptionEmpty { - continue - } - - sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions) - votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares) - - results[val.Vote] = results[val.Vote].Add(votingPower) - totalVotingPower = totalVotingPower.Add(votingPower) - } - - tallyParams := keeper.GetTallyParams(ctx) - tallyResults = types.NewTallyResultFromMap(results) - - // TODO: Upgrade the spec to cover all of these cases & remove pseudocode. - // If there is no staked coins, the proposal fails - if keeper.sk.TotalBondedTokens(ctx).IsZero() { - return false, false, tallyResults - } - - // If there is not enough quorum of votes, the proposal fails - percentVoting := totalVotingPower.Quo(keeper.sk.TotalBondedTokens(ctx).ToDec()) - if percentVoting.LT(tallyParams.Quorum) { - return false, true, tallyResults +func (k Keeper) Tally(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) { + tally := k.TallyRouter().GetRoute(proposal.TallyRoute()) + if tally == nil { + tally = k.TallyRouter().GetRoute(types.RootTallyRoute) } - - // If no one votes (everyone abstains), proposal fails - if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) { - return false, false, tallyResults - } - - // If more than 1/3 of voters veto, proposal fails - if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(tallyParams.VetoThreshold) { - return false, true, tallyResults - } - - // If more than 1/2 of non-abstaining voters vote Yes, proposal passes - if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(tallyParams.Threshold) { - return true, false, tallyResults - } - - // If more than 1/2 of non-abstaining voters vote No, proposal fails - return false, false, tallyResults + return tally(ctx, proposal) } diff --git a/x/gov/keeper/tally_test.go b/x/gov/keeper/tally_test.go index 77f468cd2c0b..29ca7ea3fc32 100644 --- a/x/gov/keeper/tally_test.go +++ b/x/gov/keeper/tally_test.go @@ -473,3 +473,25 @@ func TestTallyValidatorMultipleDelegations(t *testing.T) { require.True(t, tallyResults.Equals(expectedTallyResult)) } + +func TestTallyNoTallyRouteProposalUsesRootTally(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createValidators(t, ctx, app, []int64{5, 5, 5}) + + tp := TestProposalNoRoute + _, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.Error(t, err) +} + +func TestTallyRandomTallyRouteProposalUsesRootTally(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createValidators(t, ctx, app, []int64{5, 5, 5}) + + tp := TestProposalRandRoute + _, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.Error(t, err) +} diff --git a/x/gov/keeper/vote.go b/x/gov/keeper/vote.go index a35569bc3465..2132697a640d 100644 --- a/x/gov/keeper/vote.go +++ b/x/gov/keeper/vote.go @@ -109,8 +109,8 @@ func (keeper Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vo } } -// deleteVote deletes a vote from a given proposalID and voter from the store -func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { +// DeleteVote deletes a vote from a given proposalID and voter from the store +func (keeper Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { store := ctx.KVStore(keeper.storeKey) store.Delete(types.VoteKey(proposalID, voterAddr)) } diff --git a/x/gov/legacy/v040/migrate_test.go b/x/gov/legacy/v040/migrate_test.go index 049f5b885387..f9589823c891 100644 --- a/x/gov/legacy/v040/migrate_test.go +++ b/x/gov/legacy/v040/migrate_test.go @@ -105,6 +105,7 @@ func TestMigrate(t *testing.T) { "content": { "@type": "/cosmos.gov.v1beta1.TextProposal", "description": "bar_text", + "tallystrategy": "", "title": "foo_text" }, "deposit_end_time": "0001-01-01T00:00:00Z", diff --git a/x/gov/simulation/decoder_test.go b/x/gov/simulation/decoder_test.go index 6160cdfa9f70..4cf1afc965da 100644 --- a/x/gov/simulation/decoder_test.go +++ b/x/gov/simulation/decoder_test.go @@ -26,7 +26,7 @@ func TestDecodeStore(t *testing.T) { dec := simulation.NewDecodeStore(cdc) endTime := time.Now().UTC() - content := types.ContentFromProposalType("test", "test", types.ProposalTypeText) + content := types.ContentFromProposalType("test", "test", types.ProposalTypeText, types.RootTallyRoute) proposal, err := types.NewProposal(content, 1, endTime, endTime.Add(24*time.Hour)) require.NoError(t, err) diff --git a/x/gov/simulation/operations_test.go b/x/gov/simulation/operations_test.go index 9c6e8306f821..f33e046d0b81 100644 --- a/x/gov/simulation/operations_test.go +++ b/x/gov/simulation/operations_test.go @@ -36,6 +36,7 @@ func (m MockWeightedProposalContent) ContentSimulatorFn() simtypes.ContentSimula return types.NewTextProposal( fmt.Sprintf("title-%d: %s", m.n, simtypes.RandStringOfLength(r, 100)), fmt.Sprintf("description-%d: %s", m.n, simtypes.RandStringOfLength(r, 4000)), + fmt.Sprintf("staking"), ) } } @@ -136,7 +137,7 @@ func TestSimulateMsgDeposit(t *testing.T) { accounts := getTestingAccounts(t, r, app, ctx, 3) // setup a proposal - content := types.NewTextProposal("Test", "description") + content := types.NewTextProposal("Test", "description", "staking") submitTime := ctx.BlockHeader().Time depositPeriod := app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod @@ -178,7 +179,7 @@ func TestSimulateMsgVote(t *testing.T) { accounts := getTestingAccounts(t, r, app, ctx, 3) // setup a proposal - content := types.NewTextProposal("Test", "description") + content := types.NewTextProposal("Test", "description", "staking") submitTime := ctx.BlockHeader().Time depositPeriod := app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod diff --git a/x/gov/simulation/proposals.go b/x/gov/simulation/proposals.go index 322774c984eb..f5bef20fa341 100644 --- a/x/gov/simulation/proposals.go +++ b/x/gov/simulation/proposals.go @@ -29,5 +29,6 @@ func SimulateTextProposalContent(r *rand.Rand, _ sdk.Context, _ []simtypes.Accou return types.NewTextProposal( simtypes.RandStringOfLength(r, 140), simtypes.RandStringOfLength(r, 5000), + "staking", ) } diff --git a/x/gov/types/content.go b/x/gov/types/content.go index 44cbe6af7547..a4afa9fecde0 100644 --- a/x/gov/types/content.go +++ b/x/gov/types/content.go @@ -24,6 +24,7 @@ type Content interface { GetDescription() string ProposalRoute() string ProposalType() string + TallyRoute() string ValidateBasic() error String() string } diff --git a/x/gov/types/errors.go b/x/gov/types/errors.go index 96973f1751a2..5b7d1543b6d0 100644 --- a/x/gov/types/errors.go +++ b/x/gov/types/errors.go @@ -14,4 +14,5 @@ var ( ErrInvalidVote = sdkerrors.Register(ModuleName, 7, "invalid vote option") ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis state") ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "no handler exists for proposal type") + ErrInvalidTallyRoute = sdkerrors.Register(ModuleName, 10, "invalid tally route provided") ) diff --git a/x/gov/types/expected_keepers.go b/x/gov/types/expected_keepers.go index f862a9bce563..9e106ea5abf1 100644 --- a/x/gov/types/expected_keepers.go +++ b/x/gov/types/expected_keepers.go @@ -3,6 +3,7 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" + bankexported "github.com/cosmos/cosmos-sdk/x/bank/exported" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -48,4 +49,5 @@ type BankKeeper interface { SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error + GetSupply(ctx sdk.Context) bankexported.SupplyI } diff --git a/x/gov/types/gov.pb.go b/x/gov/types/gov.pb.go index 57730bcb9087..c9eb0c10c234 100644 --- a/x/gov/types/gov.pb.go +++ b/x/gov/types/gov.pb.go @@ -124,8 +124,9 @@ func (ProposalStatus) EnumDescriptor() ([]byte, []int) { // TextProposal defines a standard text proposal whose changes need to be // manually updated in case of approval. type TextProposal struct { - Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Tallystrategy string `protobuf:"bytes,3,opt,name=tallystrategy,proto3" json:"tallystrategy,omitempty"` } func (m *TextProposal) Reset() { *m = TextProposal{} } @@ -464,94 +465,94 @@ func init() { func init() { proto.RegisterFile("cosmos/gov/v1beta1/gov.proto", fileDescriptor_6e82113c1a9a4b7c) } var fileDescriptor_6e82113c1a9a4b7c = []byte{ - // 1377 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0x5f, 0x6c, 0xdb, 0xd4, - 0x17, 0x8e, 0xd3, 0xbf, 0xb9, 0x49, 0x5b, 0xef, 0x36, 0x6b, 0x53, 0xff, 0xf6, 0xb3, 0x8d, 0x41, - 0xa8, 0x9a, 0xb6, 0x74, 0x2b, 0x08, 0x44, 0x27, 0x21, 0x92, 0xc6, 0x63, 0x41, 0x53, 0x12, 0x39, - 0x5e, 0xa6, 0x8d, 0x07, 0xcb, 0x49, 0xee, 0x52, 0x43, 0xec, 0x1b, 0xe2, 0x9b, 0xd2, 0x88, 0x17, - 0x1e, 0xa7, 0x20, 0xa1, 0xbd, 0x31, 0x09, 0x45, 0x9a, 0xc4, 0x1b, 0xcf, 0x3c, 0xf3, 0x5c, 0x21, - 0x24, 0x26, 0x9e, 0x26, 0x90, 0x32, 0xd6, 0x49, 0x68, 0xea, 0x63, 0x1f, 0x78, 0x46, 0xf6, 0xbd, - 0x6e, 0x9c, 0xa4, 0xa2, 0x84, 0xa7, 0xd9, 0xe7, 0x9e, 0xef, 0xfb, 0xce, 0xfd, 0x7c, 0xce, 0xc9, - 0x0a, 0x2e, 0xd5, 0xb0, 0x6b, 0x63, 0x77, 0xab, 0x81, 0xf7, 0xb7, 0xf6, 0xaf, 0x57, 0x11, 0x31, - 0xaf, 0x7b, 0xcf, 0xe9, 0x56, 0x1b, 0x13, 0x0c, 0x21, 0x3d, 0x4d, 0x7b, 0x11, 0x76, 0x2a, 0x88, - 0x0c, 0x51, 0x35, 0x5d, 0x74, 0x0a, 0xa9, 0x61, 0xcb, 0xa1, 0x18, 0x21, 0xd9, 0xc0, 0x0d, 0xec, - 0x3f, 0x6e, 0x79, 0x4f, 0x2c, 0xba, 0x41, 0x51, 0x06, 0x3d, 0x60, 0xb4, 0xf4, 0x48, 0x6a, 0x60, - 0xdc, 0x68, 0xa2, 0x2d, 0xff, 0xad, 0xda, 0x79, 0xb0, 0x45, 0x2c, 0x1b, 0xb9, 0xc4, 0xb4, 0x5b, - 0x01, 0x76, 0x3c, 0xc1, 0x74, 0xba, 0xec, 0x48, 0x1c, 0x3f, 0xaa, 0x77, 0xda, 0x26, 0xb1, 0x30, - 0x2b, 0x46, 0xb9, 0x0b, 0x12, 0x3a, 0x3a, 0x20, 0xa5, 0x36, 0x6e, 0x61, 0xd7, 0x6c, 0xc2, 0x24, - 0x98, 0x23, 0x16, 0x69, 0xa2, 0x14, 0x27, 0x73, 0x9b, 0x31, 0x8d, 0xbe, 0x40, 0x19, 0xc4, 0xeb, - 0xc8, 0xad, 0xb5, 0xad, 0x96, 0x07, 0x4d, 0x45, 0xfd, 0xb3, 0x70, 0x68, 0x67, 0xe5, 0xd5, 0x13, - 0x89, 0xfb, 0xf5, 0x87, 0xab, 0x0b, 0xbb, 0xd8, 0x21, 0xc8, 0x21, 0xca, 0x2f, 0x1c, 0x58, 0xc8, - 0xa1, 0x16, 0x76, 0x2d, 0x02, 0xdf, 0x05, 0xf1, 0x16, 0x13, 0x30, 0xac, 0xba, 0x4f, 0x3d, 0x9b, - 0x5d, 0x3b, 0x19, 0x48, 0xb0, 0x6b, 0xda, 0xcd, 0x1d, 0x25, 0x74, 0xa8, 0x68, 0x20, 0x78, 0xcb, - 0xd7, 0xe1, 0x25, 0x10, 0xab, 0x53, 0x0e, 0xdc, 0x66, 0xaa, 0xc3, 0x00, 0xac, 0x81, 0x79, 0xd3, - 0xc6, 0x1d, 0x87, 0xa4, 0x66, 0xe4, 0x99, 0xcd, 0xf8, 0xf6, 0x46, 0x9a, 0xd9, 0xe6, 0x39, 0x1f, - 0x7c, 0x8e, 0xf4, 0x2e, 0xb6, 0x9c, 0xec, 0xb5, 0xc3, 0x81, 0x14, 0xf9, 0xfe, 0xb9, 0xb4, 0xd9, - 0xb0, 0xc8, 0x5e, 0xa7, 0x9a, 0xae, 0x61, 0x9b, 0x79, 0xcc, 0xfe, 0xb9, 0xea, 0xd6, 0x3f, 0xdd, - 0x22, 0xdd, 0x16, 0x72, 0x7d, 0x80, 0xab, 0x31, 0xea, 0x9d, 0xc5, 0x87, 0x4f, 0xa4, 0xc8, 0xab, - 0x27, 0x52, 0x44, 0xf9, 0x6b, 0x1e, 0x2c, 0x9e, 0xfa, 0xf4, 0xf6, 0x59, 0x57, 0x5a, 0x3d, 0x1e, - 0x48, 0x51, 0xab, 0x7e, 0x32, 0x90, 0x62, 0xf4, 0x62, 0xe3, 0xf7, 0xb9, 0x01, 0x16, 0x6a, 0xd4, - 0x1f, 0xff, 0x36, 0xf1, 0xed, 0x64, 0x9a, 0x7e, 0x9f, 0x74, 0xf0, 0x7d, 0xd2, 0x19, 0xa7, 0x9b, - 0x8d, 0xff, 0x34, 0x34, 0x52, 0x0b, 0x10, 0xb0, 0x02, 0xe6, 0x5d, 0x62, 0x92, 0x8e, 0x9b, 0x9a, - 0x91, 0xb9, 0xcd, 0xe5, 0x6d, 0x25, 0x3d, 0xd9, 0x7c, 0xe9, 0xa0, 0xc0, 0xb2, 0x9f, 0x99, 0x15, - 0x4e, 0x06, 0xd2, 0xda, 0x98, 0xc9, 0x94, 0x44, 0xd1, 0x18, 0x1b, 0x6c, 0x01, 0xf8, 0xc0, 0x72, - 0xcc, 0xa6, 0x41, 0xcc, 0x66, 0xb3, 0x6b, 0xb4, 0x91, 0xdb, 0x69, 0x92, 0xd4, 0xac, 0x5f, 0x9f, - 0x74, 0x96, 0x86, 0xee, 0xe5, 0x69, 0x7e, 0x5a, 0xf6, 0x35, 0xcf, 0xd8, 0x93, 0x81, 0xb4, 0x41, - 0x45, 0x26, 0x89, 0x14, 0x8d, 0xf7, 0x83, 0x21, 0x10, 0xfc, 0x18, 0xc4, 0xdd, 0x4e, 0xd5, 0xb6, - 0x88, 0xe1, 0x75, 0x72, 0x6a, 0xce, 0x97, 0x12, 0x26, 0xac, 0xd0, 0x83, 0x36, 0xcf, 0x8a, 0x4c, - 0x85, 0xf5, 0x4b, 0x08, 0xac, 0x3c, 0x7a, 0x2e, 0x71, 0x1a, 0xa0, 0x11, 0x0f, 0x00, 0x2d, 0xc0, - 0xb3, 0x16, 0x31, 0x90, 0x53, 0xa7, 0x0a, 0xf3, 0xe7, 0x2a, 0xbc, 0xce, 0x14, 0xd6, 0xa9, 0xc2, - 0x38, 0x03, 0x95, 0x59, 0x66, 0x61, 0xd5, 0xa9, 0xfb, 0x52, 0x0f, 0x39, 0xb0, 0x44, 0x30, 0x31, - 0x9b, 0x06, 0x3b, 0x48, 0x2d, 0x9c, 0xd7, 0x88, 0xb7, 0x98, 0x4e, 0x92, 0xea, 0x8c, 0xa0, 0x95, - 0xa9, 0x1a, 0x34, 0xe1, 0x63, 0x83, 0x11, 0x6b, 0x82, 0x0b, 0xfb, 0x98, 0x58, 0x4e, 0xc3, 0xfb, - 0xbc, 0x6d, 0x66, 0xec, 0xe2, 0xb9, 0xd7, 0x7e, 0x83, 0x95, 0x93, 0xa2, 0xe5, 0x4c, 0x50, 0xd0, - 0x7b, 0xaf, 0xd0, 0x78, 0xd9, 0x0b, 0xfb, 0x17, 0x7f, 0x00, 0x58, 0x68, 0x68, 0x71, 0xec, 0x5c, - 0x2d, 0x85, 0x69, 0xad, 0x8d, 0x68, 0x8d, 0x3a, 0xbc, 0x44, 0xa3, 0xcc, 0xe0, 0x9d, 0x59, 0x6f, - 0xab, 0x28, 0x87, 0x51, 0x10, 0x0f, 0xb7, 0xcf, 0x07, 0x60, 0xa6, 0x8b, 0x5c, 0xba, 0xa1, 0xb2, - 0x69, 0x8f, 0xf5, 0xb7, 0x81, 0xf4, 0xe6, 0xbf, 0x30, 0x2e, 0xef, 0x10, 0xcd, 0x83, 0xc2, 0x5b, - 0x60, 0xc1, 0xac, 0xba, 0xc4, 0xb4, 0xd8, 0x2e, 0x9b, 0x9a, 0x25, 0x80, 0xc3, 0xf7, 0x41, 0xd4, - 0xc1, 0xfe, 0x40, 0x4e, 0x4f, 0x12, 0x75, 0x30, 0x6c, 0x80, 0x84, 0x83, 0x8d, 0xcf, 0x2d, 0xb2, - 0x67, 0xec, 0x23, 0x82, 0xfd, 0xb1, 0x8b, 0x65, 0xd5, 0xe9, 0x98, 0x4e, 0x06, 0xd2, 0x2a, 0x35, - 0x35, 0xcc, 0xa5, 0x68, 0xc0, 0xc1, 0x77, 0x2d, 0xb2, 0x57, 0x41, 0x04, 0x33, 0x2b, 0xbf, 0xe1, - 0xc0, 0x6c, 0x05, 0x13, 0xf4, 0xdf, 0x57, 0x72, 0x12, 0xcc, 0xed, 0x63, 0x82, 0x82, 0x75, 0x4c, - 0x5f, 0xe0, 0x3b, 0x60, 0x1e, 0xd3, 0xdf, 0x06, 0xba, 0x9b, 0xc4, 0xb3, 0xf6, 0x86, 0x27, 0x5c, - 0xf4, 0xb3, 0x34, 0x96, 0xbd, 0xb3, 0xf8, 0x38, 0xd8, 0xae, 0x3f, 0x46, 0xc1, 0x12, 0x6b, 0xe6, - 0x92, 0xd9, 0x36, 0x6d, 0x17, 0x7e, 0xcb, 0x81, 0xb8, 0x6d, 0x39, 0xa7, 0xb3, 0xc5, 0x9d, 0x37, - 0x5b, 0x86, 0xe7, 0xda, 0xf1, 0x40, 0xba, 0x18, 0x42, 0x5d, 0xc1, 0xb6, 0x45, 0x90, 0xdd, 0x22, - 0xdd, 0xe1, 0xdd, 0x42, 0xc7, 0xd3, 0x8d, 0x1c, 0xb0, 0x2d, 0x27, 0x18, 0xb8, 0xaf, 0x39, 0x00, - 0x6d, 0xf3, 0x20, 0x20, 0x32, 0x5a, 0xa8, 0x6d, 0xe1, 0x3a, 0x5b, 0xeb, 0x1b, 0x13, 0x63, 0x90, - 0x63, 0x3f, 0xbb, 0xf4, 0xd3, 0x1e, 0x0f, 0xa4, 0x4b, 0x93, 0xe0, 0x91, 0x5a, 0xd9, 0x42, 0x9d, - 0xcc, 0x52, 0x1e, 0x7b, 0x83, 0xc2, 0xdb, 0xe6, 0x41, 0x60, 0x17, 0x0d, 0x7f, 0xc5, 0x81, 0x44, - 0xc5, 0x9f, 0x1e, 0xe6, 0xdf, 0x17, 0x80, 0x4d, 0x53, 0x50, 0x1b, 0x77, 0x5e, 0x6d, 0x37, 0x58, - 0x6d, 0xeb, 0x23, 0xb8, 0x91, 0xb2, 0x92, 0x23, 0xc3, 0x1b, 0xae, 0x28, 0x41, 0x63, 0xac, 0x9a, - 0xdf, 0x83, 0x99, 0x65, 0xc5, 0xdc, 0x07, 0xf3, 0x9f, 0x75, 0x70, 0xbb, 0x63, 0xfb, 0x55, 0x24, - 0xb2, 0xd9, 0x29, 0x3a, 0x3c, 0x87, 0x6a, 0xc7, 0x03, 0x89, 0xa7, 0xf8, 0x61, 0x35, 0x1a, 0x63, - 0x84, 0x35, 0x10, 0x23, 0x7b, 0x6d, 0xe4, 0xee, 0xe1, 0x26, 0xfd, 0x00, 0x89, 0xa9, 0x06, 0x88, - 0xd2, 0xaf, 0x9e, 0x52, 0x84, 0x14, 0x86, 0xbc, 0xb0, 0xc7, 0x81, 0x65, 0x6f, 0xaa, 0x8c, 0xa1, - 0xd4, 0x8c, 0x2f, 0x55, 0x9b, 0x5a, 0x2a, 0x35, 0xca, 0x33, 0xe2, 0xef, 0x45, 0xe6, 0xef, 0x48, - 0x86, 0xa2, 0x2d, 0x79, 0x01, 0x3d, 0x78, 0xbf, 0xfc, 0x27, 0x07, 0xc0, 0x70, 0x9a, 0xe0, 0x15, - 0xb0, 0x5e, 0x29, 0xea, 0xaa, 0x51, 0x2c, 0xe9, 0xf9, 0x62, 0xc1, 0xb8, 0x53, 0x28, 0x97, 0xd4, - 0xdd, 0xfc, 0xcd, 0xbc, 0x9a, 0xe3, 0x23, 0xc2, 0x4a, 0xaf, 0x2f, 0xc7, 0x69, 0xa2, 0xea, 0x89, - 0x40, 0x05, 0xac, 0x84, 0xb3, 0xef, 0xa9, 0x65, 0x9e, 0x13, 0x96, 0x7a, 0x7d, 0x39, 0x46, 0xb3, - 0xee, 0x21, 0x17, 0x5e, 0x06, 0xab, 0xe1, 0x9c, 0x4c, 0xb6, 0xac, 0x67, 0xf2, 0x05, 0x3e, 0x2a, - 0x5c, 0xe8, 0xf5, 0xe5, 0x25, 0x9a, 0x97, 0x61, 0x2b, 0x50, 0x06, 0xcb, 0xe1, 0xdc, 0x42, 0x91, - 0x9f, 0x11, 0x12, 0xbd, 0xbe, 0xbc, 0x48, 0xd3, 0x0a, 0x18, 0x6e, 0x83, 0xd4, 0x68, 0x86, 0x71, - 0x37, 0xaf, 0xdf, 0x32, 0x2a, 0xaa, 0x5e, 0xe4, 0x67, 0x85, 0x64, 0xaf, 0x2f, 0xf3, 0x41, 0x6e, - 0xb0, 0xaf, 0x84, 0xd9, 0x87, 0xdf, 0x89, 0x91, 0xcb, 0x3f, 0x47, 0xc1, 0xf2, 0xe8, 0x7f, 0x69, - 0x60, 0x1a, 0xfc, 0xaf, 0xa4, 0x15, 0x4b, 0xc5, 0x72, 0xe6, 0xb6, 0x51, 0xd6, 0x33, 0xfa, 0x9d, - 0xf2, 0xd8, 0x85, 0xfd, 0xab, 0xd0, 0xe4, 0x82, 0xd5, 0x84, 0x37, 0x80, 0x38, 0x9e, 0x9f, 0x53, - 0x4b, 0xc5, 0x72, 0x5e, 0x37, 0x4a, 0xaa, 0x96, 0x2f, 0xe6, 0x78, 0x4e, 0x58, 0xef, 0xf5, 0xe5, - 0x55, 0x0a, 0x19, 0x19, 0x2a, 0xf8, 0x1e, 0xf8, 0xff, 0x38, 0xb8, 0x52, 0xd4, 0xf3, 0x85, 0x0f, - 0x03, 0x6c, 0x54, 0x58, 0xeb, 0xf5, 0x65, 0x48, 0xb1, 0x95, 0xd0, 0x04, 0xc0, 0x2b, 0x60, 0x6d, - 0x1c, 0x5a, 0xca, 0x94, 0xcb, 0x6a, 0x8e, 0x9f, 0x11, 0xf8, 0x5e, 0x5f, 0x4e, 0x50, 0x4c, 0xc9, - 0x74, 0x5d, 0x54, 0x87, 0xd7, 0x40, 0x6a, 0x3c, 0x5b, 0x53, 0x3f, 0x52, 0x77, 0x75, 0x35, 0xc7, - 0xcf, 0x0a, 0xb0, 0xd7, 0x97, 0x97, 0x69, 0xbe, 0x86, 0x3e, 0x41, 0x35, 0x82, 0xce, 0xe4, 0xbf, - 0x99, 0xc9, 0xdf, 0x56, 0x73, 0xfc, 0x5c, 0x98, 0xff, 0xa6, 0x69, 0x35, 0x51, 0x9d, 0xda, 0x99, - 0x2d, 0x1c, 0xbe, 0x10, 0x23, 0xcf, 0x5e, 0x88, 0x91, 0x2f, 0x8f, 0xc4, 0xc8, 0xe1, 0x91, 0xc8, - 0x3d, 0x3d, 0x12, 0xb9, 0x3f, 0x8e, 0x44, 0xee, 0xd1, 0x4b, 0x31, 0xf2, 0xf4, 0xa5, 0x18, 0x79, - 0xf6, 0x52, 0x8c, 0xdc, 0xff, 0xe7, 0x85, 0x78, 0xe0, 0xff, 0x29, 0xe4, 0xf7, 0x73, 0x75, 0xde, - 0xdf, 0x21, 0x6f, 0xfd, 0x1d, 0x00, 0x00, 0xff, 0xff, 0xef, 0x91, 0x01, 0x67, 0x25, 0x0d, 0x00, - 0x00, + // 1392 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0x4d, 0x68, 0x1b, 0xc7, + 0x17, 0xd7, 0xca, 0xdf, 0x23, 0xc9, 0xde, 0x8c, 0x15, 0x5b, 0xde, 0x7f, 0xfe, 0xbb, 0xea, 0x36, + 0x94, 0x10, 0x12, 0x39, 0x71, 0x4b, 0x4b, 0x1d, 0x28, 0x95, 0xac, 0x4d, 0xa3, 0x12, 0x24, 0xb1, + 0xda, 0x28, 0x24, 0x3d, 0x2c, 0x2b, 0x69, 0x22, 0x6f, 0xab, 0xdd, 0x51, 0xb5, 0x23, 0xd7, 0xa2, + 0x14, 0x7a, 0x0c, 0x2a, 0x94, 0xdc, 0x1a, 0x28, 0x82, 0x40, 0x6f, 0x3d, 0xf7, 0xdc, 0xb3, 0x29, + 0x85, 0x86, 0x9e, 0x42, 0x0b, 0x4a, 0xe3, 0x40, 0x09, 0x3e, 0xfa, 0xd0, 0x73, 0xd9, 0x9d, 0x59, + 0x6b, 0x57, 0x32, 0x75, 0xd5, 0x53, 0x76, 0xde, 0xfc, 0x3e, 0xde, 0xbc, 0x99, 0xf7, 0x14, 0x83, + 0x0b, 0x75, 0xec, 0x58, 0xd8, 0xd9, 0x6c, 0xe2, 0xbd, 0xcd, 0xbd, 0xeb, 0x35, 0x44, 0x8c, 0xeb, + 0xee, 0x77, 0xa6, 0xdd, 0xc1, 0x04, 0x43, 0x48, 0x77, 0x33, 0x6e, 0x84, 0xed, 0x0a, 0x22, 0x63, + 0xd4, 0x0c, 0x07, 0x9d, 0x50, 0xea, 0xd8, 0xb4, 0x29, 0x47, 0x48, 0x36, 0x71, 0x13, 0x7b, 0x9f, + 0x9b, 0xee, 0x17, 0x8b, 0x6e, 0x50, 0x96, 0x4e, 0x37, 0x98, 0x2c, 0xdd, 0x92, 0x9a, 0x18, 0x37, + 0x5b, 0x68, 0xd3, 0x5b, 0xd5, 0xba, 0x0f, 0x36, 0x89, 0x69, 0x21, 0x87, 0x18, 0x56, 0xdb, 0xe7, + 0x8e, 0x03, 0x0c, 0xbb, 0xc7, 0xb6, 0xc4, 0xf1, 0xad, 0x46, 0xb7, 0x63, 0x10, 0x13, 0xb3, 0x64, + 0xe4, 0x2f, 0x40, 0x5c, 0x43, 0xfb, 0xa4, 0xdc, 0xc1, 0x6d, 0xec, 0x18, 0x2d, 0x98, 0x04, 0x73, + 0xc4, 0x24, 0x2d, 0x94, 0xe2, 0xd2, 0xdc, 0xa5, 0x25, 0x95, 0x2e, 0x60, 0x1a, 0xc4, 0x1a, 0xc8, + 0xa9, 0x77, 0xcc, 0xb6, 0x4b, 0x4d, 0x45, 0xbd, 0xbd, 0x60, 0x08, 0x5e, 0x04, 0x09, 0x62, 0xb4, + 0x5a, 0x3d, 0x87, 0x74, 0x0c, 0x82, 0x9a, 0xbd, 0xd4, 0x8c, 0x87, 0x09, 0x07, 0xb7, 0x57, 0x5e, + 0x3d, 0x91, 0xb8, 0x5f, 0x7f, 0xb8, 0xba, 0xb0, 0x83, 0x6d, 0x82, 0x6c, 0x22, 0xff, 0xc2, 0x81, + 0x85, 0x3c, 0x6a, 0x63, 0xc7, 0x24, 0xf0, 0x1d, 0x10, 0x6b, 0xb3, 0x34, 0x74, 0xb3, 0xe1, 0x25, + 0x30, 0x9b, 0x5b, 0x3b, 0x1e, 0x4a, 0xb0, 0x67, 0x58, 0xad, 0x6d, 0x39, 0xb0, 0x29, 0xab, 0xc0, + 0x5f, 0x15, 0x1a, 0xf0, 0x02, 0x58, 0x6a, 0x50, 0x0d, 0xdc, 0x61, 0xb9, 0x8d, 0x02, 0xb0, 0x0e, + 0xe6, 0x0d, 0x0b, 0x77, 0x6d, 0x92, 0x9a, 0x49, 0xcf, 0x5c, 0x8a, 0x6d, 0x6d, 0x64, 0x58, 0x71, + 0xdd, 0xfb, 0xf1, 0x2f, 0x2d, 0xb3, 0x83, 0x4d, 0x3b, 0x77, 0xed, 0x60, 0x28, 0x45, 0xbe, 0x7f, + 0x2e, 0x5d, 0x6a, 0x9a, 0x64, 0xb7, 0x5b, 0xcb, 0xd4, 0xb1, 0xc5, 0x6e, 0x82, 0xfd, 0x73, 0xd5, + 0x69, 0x7c, 0xb2, 0x49, 0x7a, 0x6d, 0xe4, 0x78, 0x04, 0x47, 0x65, 0xd2, 0xdb, 0x8b, 0x0f, 0x9f, + 0x48, 0x91, 0x57, 0x4f, 0xa4, 0x88, 0xfc, 0xd7, 0x3c, 0x58, 0x3c, 0xa9, 0xe6, 0x5b, 0xa7, 0x1d, + 0x69, 0xf5, 0x68, 0x28, 0x45, 0xcd, 0xc6, 0xf1, 0x50, 0x5a, 0xa2, 0x07, 0x1b, 0x3f, 0xcf, 0x0d, + 0xb0, 0x50, 0xa7, 0xf5, 0xf1, 0x4e, 0x13, 0xdb, 0x4a, 0x66, 0xe8, 0x2d, 0x66, 0xfc, 0x5b, 0xcc, + 0x64, 0xed, 0x5e, 0x2e, 0xf6, 0xd3, 0xa8, 0x90, 0xaa, 0xcf, 0x80, 0x55, 0x30, 0xef, 0x10, 0x83, + 0x74, 0x1d, 0xef, 0x06, 0x96, 0xb7, 0xe4, 0xcc, 0xe4, 0x13, 0xcd, 0xf8, 0x09, 0x56, 0x3c, 0x64, + 0x4e, 0x38, 0x1e, 0x4a, 0x6b, 0x63, 0x45, 0xa6, 0x22, 0xb2, 0xca, 0xd4, 0x60, 0x1b, 0xc0, 0x07, + 0xa6, 0x6d, 0xb4, 0x74, 0xef, 0x46, 0xf5, 0x0e, 0x72, 0xba, 0x2d, 0x92, 0x9a, 0xf5, 0xf2, 0x93, + 0x4e, 0xf3, 0xd0, 0x5c, 0x9c, 0xea, 0xc1, 0x72, 0xaf, 0xb9, 0x85, 0x3d, 0x1e, 0x4a, 0x1b, 0xd4, + 0x64, 0x52, 0x48, 0x56, 0x79, 0x2f, 0x18, 0x20, 0xc1, 0x8f, 0x40, 0xcc, 0xe9, 0xd6, 0x2c, 0x93, + 0xe8, 0xee, 0x7b, 0x4f, 0xcd, 0x79, 0x56, 0xc2, 0x44, 0x29, 0x34, 0xbf, 0x19, 0x72, 0x22, 0x73, + 0x61, 0xef, 0x25, 0x40, 0x96, 0x1f, 0x3d, 0x97, 0x38, 0x15, 0xd0, 0x88, 0x4b, 0x80, 0x26, 0xe0, + 0xd9, 0x13, 0xd1, 0x91, 0xdd, 0xa0, 0x0e, 0xf3, 0x67, 0x3a, 0xbc, 0xce, 0x1c, 0xd6, 0xa9, 0xc3, + 0xb8, 0x02, 0xb5, 0x59, 0x66, 0x61, 0xc5, 0x6e, 0x78, 0x56, 0x0f, 0x39, 0x90, 0x20, 0x98, 0x18, + 0x2d, 0x9d, 0x6d, 0xa4, 0x16, 0xce, 0x7a, 0x88, 0xb7, 0x98, 0x4f, 0x92, 0xfa, 0x84, 0xd8, 0xf2, + 0x54, 0x0f, 0x34, 0xee, 0x71, 0xfd, 0x16, 0x6b, 0x81, 0x73, 0x7b, 0x98, 0x98, 0x76, 0xd3, 0xbd, + 0xde, 0x0e, 0x2b, 0xec, 0xe2, 0x99, 0xc7, 0xbe, 0xc8, 0xd2, 0x49, 0xd1, 0x74, 0x26, 0x24, 0xe8, + 0xb9, 0x57, 0x68, 0xbc, 0xe2, 0x86, 0xbd, 0x83, 0x3f, 0x00, 0x2c, 0x34, 0x2a, 0xf1, 0xd2, 0x99, + 0x5e, 0x32, 0xf3, 0x5a, 0x0b, 0x79, 0x85, 0x2b, 0x9c, 0xa0, 0x51, 0x56, 0xe0, 0xed, 0x59, 0x77, + 0xaa, 0xc8, 0x07, 0x51, 0x10, 0x0b, 0x3e, 0x9f, 0xf7, 0xc1, 0x4c, 0x0f, 0x39, 0x74, 0x8e, 0xe5, + 0x32, 0xae, 0xea, 0x6f, 0x43, 0xe9, 0x8d, 0x7f, 0x51, 0xb8, 0x82, 0x4d, 0x54, 0x97, 0x0a, 0x6f, + 0x81, 0x05, 0xa3, 0xe6, 0x10, 0xc3, 0x64, 0x13, 0x6f, 0x6a, 0x15, 0x9f, 0x0e, 0xdf, 0x03, 0x51, + 0x1b, 0xd3, 0x91, 0x38, 0xb5, 0x48, 0xd4, 0xc6, 0xb0, 0x09, 0xe2, 0x36, 0xd6, 0x3f, 0x33, 0xc9, + 0xae, 0xbe, 0x87, 0x08, 0xf6, 0xda, 0x6e, 0x29, 0xa7, 0x4c, 0xa7, 0x74, 0x3c, 0x94, 0x56, 0x69, + 0x51, 0x83, 0x5a, 0xb2, 0x0a, 0x6c, 0x7c, 0xd7, 0x24, 0xbb, 0x55, 0x44, 0x30, 0x2b, 0xe5, 0x37, + 0x1c, 0x98, 0xad, 0x62, 0x82, 0xfe, 0xfb, 0x48, 0x4e, 0x82, 0xb9, 0x3d, 0x4c, 0x90, 0x3f, 0x8e, + 0xe9, 0x02, 0xbe, 0x0d, 0xe6, 0x31, 0xfd, 0x05, 0xa1, 0xb3, 0x49, 0x3c, 0x6d, 0x6e, 0xb8, 0xc6, + 0x25, 0x0f, 0xa5, 0x32, 0xf4, 0xf6, 0xe2, 0x63, 0x7f, 0xba, 0xfe, 0x18, 0x05, 0x09, 0xf6, 0x98, + 0xcb, 0x46, 0xc7, 0xb0, 0x1c, 0xf8, 0x2d, 0x07, 0x62, 0x96, 0x69, 0x9f, 0xf4, 0x16, 0x77, 0x56, + 0x6f, 0xe9, 0x6e, 0xd5, 0x8e, 0x86, 0xd2, 0xf9, 0x00, 0xeb, 0x0a, 0xb6, 0x4c, 0x82, 0xac, 0x36, + 0xe9, 0x8d, 0xce, 0x16, 0xd8, 0x9e, 0xae, 0xe5, 0x80, 0x65, 0xda, 0x7e, 0xc3, 0x7d, 0xcd, 0x01, + 0x68, 0x19, 0xfb, 0xbe, 0x90, 0xde, 0x46, 0x1d, 0x13, 0x37, 0xd8, 0x58, 0xdf, 0x98, 0x68, 0x83, + 0x3c, 0xfb, 0x71, 0xa6, 0x57, 0x7b, 0x34, 0x94, 0x2e, 0x4c, 0x92, 0x43, 0xb9, 0xb2, 0x81, 0x3a, + 0x89, 0x92, 0x1f, 0xbb, 0x8d, 0xc2, 0x5b, 0xc6, 0xbe, 0x5f, 0x2e, 0x1a, 0xfe, 0x8a, 0x03, 0xf1, + 0xaa, 0xd7, 0x3d, 0xac, 0x7e, 0x9f, 0x03, 0xd6, 0x4d, 0x7e, 0x6e, 0xdc, 0x59, 0xb9, 0xdd, 0x60, + 0xb9, 0xad, 0x87, 0x78, 0xa1, 0xb4, 0x92, 0xa1, 0xe6, 0x0d, 0x66, 0x14, 0xa7, 0x31, 0x96, 0xcd, + 0xef, 0x7e, 0xcf, 0xb2, 0x64, 0xee, 0x83, 0xf9, 0x4f, 0xbb, 0xb8, 0xd3, 0xb5, 0xbc, 0x2c, 0xe2, + 0xb9, 0xdc, 0x14, 0x2f, 0x3c, 0x8f, 0xea, 0x47, 0x43, 0x89, 0xa7, 0xfc, 0x51, 0x36, 0x2a, 0x53, + 0x84, 0x75, 0xb0, 0x44, 0x76, 0x3b, 0xc8, 0xd9, 0xc5, 0x2d, 0x7a, 0x01, 0xf1, 0xa9, 0x1a, 0x88, + 0xca, 0xaf, 0x9e, 0x48, 0x04, 0x1c, 0x46, 0xba, 0xb0, 0xcf, 0x81, 0x65, 0xb7, 0xab, 0xf4, 0x91, + 0xd5, 0x8c, 0x67, 0x55, 0x9f, 0xda, 0x2a, 0x15, 0xd6, 0x09, 0xd5, 0xf7, 0x3c, 0xab, 0x6f, 0x08, + 0x21, 0xab, 0x09, 0x37, 0xa0, 0xf9, 0xeb, 0xcb, 0x7f, 0x72, 0x00, 0x8c, 0xba, 0x09, 0x5e, 0x01, + 0xeb, 0xd5, 0x92, 0xa6, 0xe8, 0xa5, 0xb2, 0x56, 0x28, 0x15, 0xf5, 0x3b, 0xc5, 0x4a, 0x59, 0xd9, + 0x29, 0xdc, 0x2c, 0x28, 0x79, 0x3e, 0x22, 0xac, 0xf4, 0x07, 0xe9, 0x18, 0x05, 0x2a, 0xae, 0x09, + 0x94, 0xc1, 0x4a, 0x10, 0x7d, 0x4f, 0xa9, 0xf0, 0x9c, 0x90, 0xe8, 0x0f, 0xd2, 0x4b, 0x14, 0x75, + 0x0f, 0x39, 0xf0, 0x32, 0x58, 0x0d, 0x62, 0xb2, 0xb9, 0x8a, 0x96, 0x2d, 0x14, 0xf9, 0xa8, 0x70, + 0xae, 0x3f, 0x48, 0x27, 0x28, 0x2e, 0xcb, 0x46, 0x60, 0x1a, 0x2c, 0x07, 0xb1, 0xc5, 0x12, 0x3f, + 0x23, 0xc4, 0xfb, 0x83, 0xf4, 0x22, 0x85, 0x15, 0x31, 0xdc, 0x02, 0xa9, 0x30, 0x42, 0xbf, 0x5b, + 0xd0, 0x6e, 0xe9, 0x55, 0x45, 0x2b, 0xf1, 0xb3, 0x42, 0xb2, 0x3f, 0x48, 0xf3, 0x3e, 0xd6, 0x9f, + 0x57, 0xc2, 0xec, 0xc3, 0xef, 0xc4, 0xc8, 0xe5, 0x9f, 0xa3, 0x60, 0x39, 0xfc, 0x5f, 0x1a, 0x98, + 0x01, 0xff, 0x2b, 0xab, 0xa5, 0x72, 0xa9, 0x92, 0xbd, 0xad, 0x57, 0xb4, 0xac, 0x76, 0xa7, 0x32, + 0x76, 0x60, 0xef, 0x28, 0x14, 0x5c, 0x34, 0x5b, 0xf0, 0x06, 0x10, 0xc7, 0xf1, 0x79, 0xa5, 0x5c, + 0xaa, 0x14, 0x34, 0xbd, 0xac, 0xa8, 0x85, 0x52, 0x9e, 0xe7, 0x84, 0xf5, 0xfe, 0x20, 0xbd, 0x4a, + 0x29, 0xa1, 0xa6, 0x82, 0xef, 0x82, 0xff, 0x8f, 0x93, 0xab, 0x25, 0xad, 0x50, 0xfc, 0xc0, 0xe7, + 0x46, 0x85, 0xb5, 0xfe, 0x20, 0x0d, 0x29, 0xb7, 0x1a, 0xe8, 0x00, 0x78, 0x05, 0xac, 0x8d, 0x53, + 0xcb, 0xd9, 0x4a, 0x45, 0xc9, 0xf3, 0x33, 0x02, 0xdf, 0x1f, 0xa4, 0xe3, 0x94, 0x53, 0x36, 0x1c, + 0x07, 0x35, 0xe0, 0x35, 0x90, 0x1a, 0x47, 0xab, 0xca, 0x87, 0xca, 0x8e, 0xa6, 0xe4, 0xf9, 0x59, + 0x01, 0xf6, 0x07, 0xe9, 0x65, 0x8a, 0x57, 0xd1, 0xc7, 0xa8, 0x4e, 0xd0, 0xa9, 0xfa, 0x37, 0xb3, + 0x85, 0xdb, 0x4a, 0x9e, 0x9f, 0x0b, 0xea, 0xdf, 0x34, 0xcc, 0x16, 0x6a, 0xd0, 0x72, 0xe6, 0x8a, + 0x07, 0x2f, 0xc4, 0xc8, 0xb3, 0x17, 0x62, 0xe4, 0xcb, 0x43, 0x31, 0x72, 0x70, 0x28, 0x72, 0x4f, + 0x0f, 0x45, 0xee, 0x8f, 0x43, 0x91, 0x7b, 0xf4, 0x52, 0x8c, 0x3c, 0x7d, 0x29, 0x46, 0x9e, 0xbd, + 0x14, 0x23, 0xf7, 0xff, 0x79, 0x20, 0xee, 0x7b, 0x7f, 0x30, 0x79, 0xef, 0xb9, 0x36, 0xef, 0xcd, + 0x90, 0x37, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x32, 0x6b, 0xb2, 0x73, 0x4b, 0x0d, 0x00, 0x00, } func (this *TextProposal) Equal(that interface{}) bool { @@ -579,6 +580,9 @@ func (this *TextProposal) Equal(that interface{}) bool { if this.Description != that1.Description { return false } + if this.Tallystrategy != that1.Tallystrategy { + return false + } return true } func (this *Proposal) Equal(that interface{}) bool { @@ -687,6 +691,13 @@ func (m *TextProposal) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.Tallystrategy) > 0 { + i -= len(m.Tallystrategy) + copy(dAtA[i:], m.Tallystrategy) + i = encodeVarintGov(dAtA, i, uint64(len(m.Tallystrategy))) + i-- + dAtA[i] = 0x1a + } if len(m.Description) > 0 { i -= len(m.Description) copy(dAtA[i:], m.Description) @@ -1111,6 +1122,10 @@ func (m *TextProposal) Size() (n int) { if l > 0 { n += 1 + l + sovGov(uint64(l)) } + l = len(m.Tallystrategy) + if l > 0 { + n += 1 + l + sovGov(uint64(l)) + } return n } @@ -1349,6 +1364,38 @@ func (m *TextProposal) Unmarshal(dAtA []byte) error { } m.Description = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tallystrategy", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Tallystrategy = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGov(dAtA[iNdEx:]) diff --git a/x/gov/types/msgs_test.go b/x/gov/types/msgs_test.go index 9d7d6a36828a..eddfb3f3562b 100644 --- a/x/gov/types/msgs_test.go +++ b/x/gov/types/msgs_test.go @@ -44,7 +44,7 @@ func TestMsgSubmitProposal(t *testing.T) { for i, tc := range tests { msg, err := NewMsgSubmitProposal( - ContentFromProposalType(tc.title, tc.description, tc.proposalType), + ContentFromProposalType(tc.title, tc.description, tc.proposalType, "test"), tc.initialDeposit, tc.proposerAddr, ) @@ -120,13 +120,13 @@ func TestMsgVote(t *testing.T) { // this tests that Amino JSON MsgSubmitProposal.GetSignBytes() still works with Content as Any using the ModuleCdc func TestMsgSubmitProposal_GetSignBytes(t *testing.T) { - msg, err := NewMsgSubmitProposal(NewTextProposal("test", "abcd"), sdk.NewCoins(), sdk.AccAddress{}) + msg, err := NewMsgSubmitProposal(NewTextProposal("test", "abcd", "test"), sdk.NewCoins(), sdk.AccAddress{}) require.NoError(t, err) var bz []byte require.NotPanics(t, func() { bz = msg.GetSignBytes() }) require.Equal(t, - `{"type":"cosmos-sdk/MsgSubmitProposal","value":{"content":{"type":"cosmos-sdk/TextProposal","value":{"description":"abcd","title":"test"}},"initial_deposit":[]}}`, + `{"type":"cosmos-sdk/MsgSubmitProposal","value":{"content":{"type":"cosmos-sdk/TextProposal","value":{"description":"abcd","tallystrategy":"test","title":"test"}},"initial_deposit":[]}}`, string(bz)) } diff --git a/x/gov/types/proposal.go b/x/gov/types/proposal.go index 0c856647f64b..dbb4d9d80b8c 100644 --- a/x/gov/types/proposal.go +++ b/x/gov/types/proposal.go @@ -73,6 +73,14 @@ func (p Proposal) ProposalRoute() string { return content.ProposalRoute() } +func (p Proposal) TallyRoute() string { + content := p.GetContent() + if content == nil { + return "" + } + return content.TallyRoute() +} + func (p Proposal) GetTitle() string { content := p.GetContent() if content == nil { @@ -188,8 +196,8 @@ const ( var _ Content = &TextProposal{} // NewTextProposal creates a text proposal Content -func NewTextProposal(title, description string) Content { - return &TextProposal{title, description} +func NewTextProposal(title, description string, tallyRoute string) Content { + return &TextProposal{title, description, tallyRoute} } // GetTitle returns the proposal title @@ -204,6 +212,9 @@ func (tp *TextProposal) ProposalRoute() string { return RouterKey } // ProposalType is "Text" func (tp *TextProposal) ProposalType() string { return ProposalTypeText } +// ProposalType is "Text" +func (tp *TextProposal) TallyRoute() string { return tp.Tallystrategy } + // ValidateBasic validates the content's title and description of the proposal func (tp *TextProposal) ValidateBasic() error { return ValidateAbstract(tp) } @@ -228,10 +239,10 @@ func RegisterProposalType(ty string) { } // ContentFromProposalType returns a Content object based on the proposal type. -func ContentFromProposalType(title, desc, ty string) Content { +func ContentFromProposalType(title, desc, ty, tallyRoute string) Content { switch ty { case ProposalTypeText: - return NewTextProposal(title, desc) + return NewTextProposal(title, desc, tallyRoute) default: return nil diff --git a/x/gov/types/tally.go b/x/gov/types/tally.go index a4e9ee908608..0f665b8b06e9 100644 --- a/x/gov/types/tally.go +++ b/x/gov/types/tally.go @@ -6,27 +6,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// ValidatorGovInfo used for tallying -type ValidatorGovInfo struct { - Address sdk.ValAddress // address of the validator operator - BondedTokens sdk.Int // Power of a Validator - DelegatorShares sdk.Dec // Total outstanding delegator shares - DelegatorDeductions sdk.Dec // Delegator deductions from validator's delegators voting independently - Vote VoteOption // Vote of the validator -} - -// NewValidatorGovInfo creates a ValidatorGovInfo instance -func NewValidatorGovInfo(address sdk.ValAddress, bondedTokens sdk.Int, delegatorShares, - delegatorDeductions sdk.Dec, vote VoteOption) ValidatorGovInfo { +// Governance message types and routes +const ( + RootTallyRoute = "root" +) - return ValidatorGovInfo{ - Address: address, - BondedTokens: bondedTokens, - DelegatorShares: delegatorShares, - DelegatorDeductions: delegatorDeductions, - Vote: vote, - } -} +// TallyStrategy defines a function that takes takes a proposal at the end of the Voting period and returns the result +type TallyStrategy func(ctx sdk.Context, proposal Proposal) (passes bool, burnDeposits bool, tallyResults TallyResult) // NewTallyResult creates a new TallyResult instance func NewTallyResult(yes, abstain, no, noWithVeto sdk.Int) TallyResult { diff --git a/x/gov/types/tally_router.go b/x/gov/types/tally_router.go new file mode 100644 index 000000000000..e9dd77722066 --- /dev/null +++ b/x/gov/types/tally_router.go @@ -0,0 +1,62 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ TallyRouter = (*tallyrouter)(nil) + +// TallyRouter implements a governance Tally router. +// +// TODO: Use generic router (ref #3976). +type TallyRouter interface { + AddRoute(r string, h TallyStrategy) (rtr TallyRouter) + HasRoute(r string) bool + GetRoute(path string) (h TallyStrategy) +} + +type tallyrouter struct { + routes map[string]TallyStrategy + sealed bool +} + +// NewTallyRouter creates a new Router interface instance +func NewTallyRouter() TallyRouter { + return &tallyrouter{ + routes: make(map[string]TallyStrategy), + } +} + +// AddRoute adds a governance handler for a given path. It returns the Router +// so AddRoute calls can be linked. It will panic if the router is sealed. +func (rtr *tallyrouter) AddRoute(path string, t TallyStrategy) TallyRouter { + if rtr.sealed { + panic("router sealed; cannot add route handler") + } + + if !sdk.IsAlphaNumeric(path) { + panic("route expressions can only contain alphanumeric characters") + } + if rtr.HasRoute(path) { + panic(fmt.Sprintf("route %s has already been initialized", path)) + } + + rtr.routes[path] = t + return rtr +} + +// HasRoute returns true if the router has a path registered or false otherwise. +func (rtr *tallyrouter) HasRoute(path string) bool { + return rtr.routes[path] != nil +} + +// GetRoute returns a Handler for a given path. +func (rtr *tallyrouter) GetRoute(path string) TallyStrategy { + if !rtr.HasRoute(path) { + return nil + } + + return rtr.routes[path] +} diff --git a/x/ibc/core/02-client/types/proposal.go b/x/ibc/core/02-client/types/proposal.go index 334a9d4599ef..e68a34746b84 100644 --- a/x/ibc/core/02-client/types/proposal.go +++ b/x/ibc/core/02-client/types/proposal.go @@ -44,6 +44,9 @@ func (cup *ClientUpdateProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of a client update proposal. func (cup *ClientUpdateProposal) ProposalType() string { return ProposalTypeClientUpdate } +// TallyRoute returns the tally route of a client update proposal. +func (cup *ClientUpdateProposal) TallyRoute() string { return govtypes.RootTallyRoute } + // ValidateBasic runs basic stateless validity checks func (cup *ClientUpdateProposal) ValidateBasic() error { err := govtypes.ValidateAbstract(cup) diff --git a/x/params/types/proposal/proposal.go b/x/params/types/proposal/proposal.go index 3a2f97a77247..70a8dbaa707a 100644 --- a/x/params/types/proposal/proposal.go +++ b/x/params/types/proposal/proposal.go @@ -38,6 +38,9 @@ func (pcp *ParameterChangeProposal) ProposalRoute() string { return RouterKey } // ProposalType returns the type of a parameter change proposal. func (pcp *ParameterChangeProposal) ProposalType() string { return ProposalTypeChange } +// TallyRoute returns the tally route of a parameter change proposal. +func (pcp *ParameterChangeProposal) TallyRoute() string { return govtypes.RootTallyRoute } + // ValidateBasic validates the parameter change proposal func (pcp *ParameterChangeProposal) ValidateBasic() error { err := govtypes.ValidateAbstract(pcp) diff --git a/x/simulation/params_test.go b/x/simulation/params_test.go index d0b538c26e4d..a0ee90b9b59d 100644 --- a/x/simulation/params_test.go +++ b/x/simulation/params_test.go @@ -50,5 +50,6 @@ func (t testContent) GetTitle() string { return "" } func (t testContent) GetDescription() string { return "" } func (t testContent) ProposalRoute() string { return "" } func (t testContent) ProposalType() string { return "" } +func (t testContent) TallyRoute() string { return "" } func (t testContent) ValidateBasic() error { return nil } func (t testContent) String() string { return "" } diff --git a/x/tallystrategies/stakingtally/common_test.go b/x/tallystrategies/stakingtally/common_test.go new file mode 100644 index 000000000000..774aee806ec2 --- /dev/null +++ b/x/tallystrategies/stakingtally/common_test.go @@ -0,0 +1,58 @@ +package stakingtally_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +var ( + TestProposal = types.NewTextProposal("Test", "description", "staking") +) + +func createValidators(t *testing.T, ctx sdk.Context, app *simapp.SimApp, powers []int64) ([]sdk.AccAddress, []sdk.ValAddress) { + addrs := simapp.AddTestAddrsIncremental(app, ctx, 5, sdk.NewInt(30000000)) + valAddrs := simapp.ConvertAddrsToValAddrs(addrs) + pks := simapp.CreateTestPubKeys(5) + cdc := simapp.MakeTestEncodingConfig().Marshaler + + app.StakingKeeper = stakingkeeper.NewKeeper( + cdc, + app.GetKey(stakingtypes.StoreKey), + app.AccountKeeper, + app.BankKeeper, + app.GetSubspace(stakingtypes.ModuleName), + ) + + val1, err := stakingtypes.NewValidator(valAddrs[0], pks[0], stakingtypes.Description{}) + require.NoError(t, err) + val2, err := stakingtypes.NewValidator(valAddrs[1], pks[1], stakingtypes.Description{}) + require.NoError(t, err) + val3, err := stakingtypes.NewValidator(valAddrs[2], pks[2], stakingtypes.Description{}) + require.NoError(t, err) + + app.StakingKeeper.SetValidator(ctx, val1) + app.StakingKeeper.SetValidator(ctx, val2) + app.StakingKeeper.SetValidator(ctx, val3) + app.StakingKeeper.SetValidatorByConsAddr(ctx, val1) + app.StakingKeeper.SetValidatorByConsAddr(ctx, val2) + app.StakingKeeper.SetValidatorByConsAddr(ctx, val3) + app.StakingKeeper.SetNewValidatorByPowerIndex(ctx, val1) + app.StakingKeeper.SetNewValidatorByPowerIndex(ctx, val2) + app.StakingKeeper.SetNewValidatorByPowerIndex(ctx, val3) + + _, _ = app.StakingKeeper.Delegate(ctx, addrs[0], sdk.TokensFromConsensusPower(powers[0]), stakingtypes.Unbonded, val1, true) + _, _ = app.StakingKeeper.Delegate(ctx, addrs[1], sdk.TokensFromConsensusPower(powers[1]), stakingtypes.Unbonded, val2, true) + _, _ = app.StakingKeeper.Delegate(ctx, addrs[2], sdk.TokensFromConsensusPower(powers[2]), stakingtypes.Unbonded, val3, true) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + return addrs, valAddrs +} diff --git a/x/tallystrategies/stakingtally/stakingtally.go b/x/tallystrategies/stakingtally/stakingtally.go new file mode 100644 index 000000000000..4f89811285af --- /dev/null +++ b/x/tallystrategies/stakingtally/stakingtally.go @@ -0,0 +1,129 @@ +package stakingtally + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// NewStakingTallyHandler creates a new governance tally strategy for a giving voting to delegations +func NewStakingTallyHandler(gk govkeeper.Keeper, sk stakingkeeper.Keeper) govtypes.TallyStrategy { + return func(ctx sdk.Context, proposal govtypes.Proposal) (passes bool, burnDeposits bool, tallyResults govtypes.TallyResult) { + return handleStakingTally(ctx, proposal, gk, sk) + } +} + +// TODO: Break into several smaller functions for clarity + +// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the +// voters +func handleStakingTally(ctx sdk.Context, proposal govtypes.Proposal, gk govkeeper.Keeper, sk stakingkeeper.Keeper) (passes bool, burnDeposits bool, tallyResults govtypes.TallyResult) { + results := make(map[govtypes.VoteOption]sdk.Dec) + results[govtypes.OptionYes] = sdk.ZeroDec() + results[govtypes.OptionAbstain] = sdk.ZeroDec() + results[govtypes.OptionNo] = sdk.ZeroDec() + results[govtypes.OptionNoWithVeto] = sdk.ZeroDec() + + totalVotingPower := sdk.ZeroDec() + currValidators := make(map[string]ValidatorGovInfo) + + // fetch all the bonded validators, insert them into currValidators + sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) { + currValidators[validator.GetOperator().String()] = NewValidatorGovInfo( + validator.GetOperator(), + validator.GetBondedTokens(), + validator.GetDelegatorShares(), + sdk.ZeroDec(), + govtypes.OptionEmpty, + ) + + return false + }) + + gk.IterateVotes(ctx, proposal.ProposalId, func(vote govtypes.Vote) bool { + // if validator, just record it in the map + voter, err := sdk.AccAddressFromBech32(vote.Voter) + + if err != nil { + panic(err) + } + + valAddrStr := sdk.ValAddress(voter.Bytes()).String() + if val, ok := currValidators[valAddrStr]; ok { + val.Vote = vote.Option + currValidators[valAddrStr] = val + } + + // iterate over all delegations from voter, deduct from any delegated-to validators + sk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) { + valAddrStr := delegation.GetValidatorAddr().String() + + if val, ok := currValidators[valAddrStr]; ok { + // There is no need to handle the special case that validator address equal to voter address. + // Because voter's voting power will tally again even if there will deduct voter's voting power from validator. + val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares()) + currValidators[valAddrStr] = val + + // delegation shares * bonded / total shares + votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares) + + results[vote.Option] = results[vote.Option].Add(votingPower) + totalVotingPower = totalVotingPower.Add(votingPower) + } + + return false + }) + + gk.DeleteVote(ctx, vote.ProposalId, voter) + return false + }) + + // iterate over the validators again to tally their voting power + for _, val := range currValidators { + if val.Vote == govtypes.OptionEmpty { + continue + } + + sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions) + votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares) + + results[val.Vote] = results[val.Vote].Add(votingPower) + totalVotingPower = totalVotingPower.Add(votingPower) + } + + tallyParams := gk.GetTallyParams(ctx) + tallyResults = govtypes.NewTallyResultFromMap(results) + + // TODO: Upgrade the spec to cover all of these cases & remove pseudocode. + // If there is no staked coins, the proposal fails + if sk.TotalBondedTokens(ctx).IsZero() { + return false, false, tallyResults + } + + // If there is not enough quorum of votes, the proposal fails + percentVoting := totalVotingPower.Quo(sk.TotalBondedTokens(ctx).ToDec()) + if percentVoting.LT(tallyParams.Quorum) { + return false, true, tallyResults + } + + // If no one votes (everyone abstains), proposal fails + if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) { + return false, false, tallyResults + } + + // If more than 1/3 of voters veto, proposal fails + if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(tallyParams.VetoThreshold) { + return false, true, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote Yes, proposal passes + if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(tallyParams.Threshold) { + return true, false, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote No, proposal fails + return false, false, tallyResults +} diff --git a/x/tallystrategies/stakingtally/tally_test.go b/x/tallystrategies/stakingtally/tally_test.go new file mode 100644 index 000000000000..9a1a5d30970f --- /dev/null +++ b/x/tallystrategies/stakingtally/tally_test.go @@ -0,0 +1,492 @@ +package stakingtally_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtally "github.com/cosmos/cosmos-sdk/x/tallystrategies/stakingtally" +) + +func TestTallyNoOneVotes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createValidators(t, ctx, app, []int64{5, 5, 5}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.True(t, burnDeposits) + require.True(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyNoQuorum(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createValidators(t, ctx, app, []int64{2, 5, 0}) + + addrs := simapp.AddTestAddrsIncremental(app, ctx, 1, sdk.NewInt(10000000)) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + err = app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes) + require.Nil(t, err) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, _ := tally(ctx, proposal) + require.False(t, passes) + require.True(t, burnDeposits) +} + +func TestTallyOnlyValidatorsAllYes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, _ := createValidators(t, ctx, app, []int64{5, 5, 5}) + tp := TestProposal + + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyOnlyValidators51No(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{5, 6, 0}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[1], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, _ := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) +} + +func TestTallyOnlyValidators51Yes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{5, 6, 0}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[0], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[1], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyOnlyValidatorsVetoed(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[2], types.OptionNoWithVeto)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.True(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyOnlyValidatorsAbstainPasses(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[0], types.OptionAbstain)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyOnlyValidatorsAbstainFails(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[0], types.OptionAbstain)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddrs[2], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyOnlyValidatorsNonVoter(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + valAccAddrs, _ := createValidators(t, ctx, app, []int64{5, 6, 7}) + valAccAddr1, valAccAddr2 := valAccAddrs[0], valAccAddrs[1] + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddr1, types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, valAccAddr2, types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyDelgatorOverride(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, valAddrs := createValidators(t, ctx, app, []int64{5, 6, 7}) + + delTokens := sdk.TokensFromConsensusPower(30) + val1, found := app.StakingKeeper.GetValidator(ctx, valAddrs[0]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[4], delTokens, stakingtypes.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[3], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[4], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyDelgatorInherit(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, vals := createValidators(t, ctx, app, []int64{5, 6, 7}) + + delTokens := sdk.TokensFromConsensusPower(30) + val3, found := app.StakingKeeper.GetValidator(ctx, vals[2]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val3, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyDelgatorMultipleOverride(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, vals := createValidators(t, ctx, app, []int64{5, 6, 7}) + + delTokens := sdk.TokensFromConsensusPower(10) + val1, found := app.StakingKeeper.GetValidator(ctx, vals[0]) + require.True(t, found) + val2, found := app.StakingKeeper.GetValidator(ctx, vals[1]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val1, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val2, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[3], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyDelgatorMultipleInherit(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createValidators(t, ctx, app, []int64{25, 6, 7}) + + addrs, vals := createValidators(t, ctx, app, []int64{5, 6, 7}) + + delTokens := sdk.TokensFromConsensusPower(10) + val2, found := app.StakingKeeper.GetValidator(ctx, vals[1]) + require.True(t, found) + val3, found := app.StakingKeeper.GetValidator(ctx, vals[2]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val2, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val3, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyJailedValidator(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, valAddrs := createValidators(t, ctx, app, []int64{25, 6, 7}) + + delTokens := sdk.TokensFromConsensusPower(10) + val2, found := app.StakingKeeper.GetValidator(ctx, valAddrs[1]) + require.True(t, found) + val3, found := app.StakingKeeper.GetValidator(ctx, valAddrs[2]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val2, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, addrs[3], delTokens, stakingtypes.Unbonded, val3, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + consAddr, err := val2.GetConsAddr() + require.NoError(t, err) + app.StakingKeeper.Jail(ctx, sdk.ConsAddress(consAddr.Bytes())) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyValidatorMultipleDelegations(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs, valAddrs := createValidators(t, ctx, app, []int64{10, 10, 10}) + + delTokens := sdk.TokensFromConsensusPower(10) + val2, found := app.StakingKeeper.GetValidator(ctx, valAddrs[1]) + require.True(t, found) + + _, err := app.StakingKeeper.Delegate(ctx, addrs[0], delTokens, stakingtypes.Unbonded, val2, true) + require.NoError(t, err) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := stakingtally.NewStakingTallyHandler(app.GovKeeper, app.StakingKeeper) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + + expectedYes := sdk.TokensFromConsensusPower(30) + expectedAbstain := sdk.TokensFromConsensusPower(0) + expectedNo := sdk.TokensFromConsensusPower(10) + expectedNoWithVeto := sdk.TokensFromConsensusPower(0) + expectedTallyResult := types.NewTallyResult(expectedYes, expectedAbstain, expectedNo, expectedNoWithVeto) + + require.True(t, tallyResults.Equals(expectedTallyResult)) +} diff --git a/x/tallystrategies/stakingtally/types.go b/x/tallystrategies/stakingtally/types.go new file mode 100644 index 000000000000..94a1eeac3247 --- /dev/null +++ b/x/tallystrategies/stakingtally/types.go @@ -0,0 +1,33 @@ +package stakingtally + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +const ( + // TallyRoute determines the default tally route that points to this strategy + TallyRoute = "staking" +) + +// ValidatorGovInfo used for staking tallying +type ValidatorGovInfo struct { + Address sdk.ValAddress // address of the validator operator + BondedTokens sdk.Int // Power of a Validator + DelegatorShares sdk.Dec // Total outstanding delegator shares + DelegatorDeductions sdk.Dec // Delegator deductions from validator's delegators voting independently + Vote govtypes.VoteOption // Vote of the validator +} + +// NewValidatorGovInfo creates a ValidatorGovInfo instance +func NewValidatorGovInfo(address sdk.ValAddress, bondedTokens sdk.Int, delegatorShares, + delegatorDeductions sdk.Dec, vote govtypes.VoteOption) ValidatorGovInfo { + + return ValidatorGovInfo{ + Address: address, + BondedTokens: bondedTokens, + DelegatorShares: delegatorShares, + DelegatorDeductions: delegatorDeductions, + Vote: vote, + } +} diff --git a/x/tallystrategies/tokenbalancetally/common_test.go b/x/tallystrategies/tokenbalancetally/common_test.go new file mode 100644 index 000000000000..5ffa4f55bd4f --- /dev/null +++ b/x/tallystrategies/tokenbalancetally/common_test.go @@ -0,0 +1,80 @@ +package tokenbalancetally_test + +import ( + "bytes" + "strconv" + "testing" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +var ( + TestProposal = types.NewTextProposal("Test", "description", "staking") + govToken = "tbgov" +) + +func createGoverners(t *testing.T, ctx sdk.Context, app *simapp.SimApp, powers []int64) []sdk.AccAddress { + addrs := addTestAddrs(app, ctx, powers, createIncrementalAccounts) + + return addrs +} + +// createIncrementalAccounts is a strategy used by addTestAddrs() in order to generated addresses in ascending order. +func createIncrementalAccounts(accNum int) []sdk.AccAddress { + var addresses []sdk.AccAddress + var buffer bytes.Buffer + + // start at 100 so we can make up to 999 test addresses with valid test addresses + for i := 100; i < (accNum + 100); i++ { + numString := strconv.Itoa(i) + buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string + + buffer.WriteString(numString) //adding on final two digits to make addresses unique + res, _ := sdk.AccAddressFromHex(buffer.String()) + bech := res.String() + addr, _ := simapp.TestAddr(buffer.String(), bech) + + addresses = append(addresses, addr) + buffer.Reset() + } + + return addresses +} + +func addTestAddrs(app *simapp.SimApp, ctx sdk.Context, powers []int64, strategy simapp.GenerateAccountStrategy) []sdk.AccAddress { + accNum := len(powers) + totalPower := int64(0) + for _, power := range powers { + totalPower += power + } + testAddrs := strategy(accNum) + + setGovTokenTotalSupply(app, ctx, sdk.NewInt(totalPower)) + + // fill all the addresses with some coins, set the loose pool tokens simultaneously + for i, addr := range testAddrs { + saveAccount(app, ctx, addr, sdk.NewCoins(sdk.NewCoin(govToken, sdk.NewInt(powers[i])))) + } + + return testAddrs +} + +// saveAccount saves the provided account into the simapp with balance based on initCoins. +func saveAccount(app *simapp.SimApp, ctx sdk.Context, addr sdk.AccAddress, initCoins sdk.Coins) { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr) + app.AccountKeeper.SetAccount(ctx, acc) + err := app.BankKeeper.AddCoins(ctx, addr, initCoins) + if err != nil { + panic(err) + } +} + +// setTotalSupply provides the total supply based on accAmt * totalAccounts. +func setGovTokenTotalSupply(app *simapp.SimApp, ctx sdk.Context, totalAmt sdk.Int) { + totalSupply := sdk.NewCoins(sdk.NewCoin(govToken, totalAmt)) + prevSupply := app.BankKeeper.GetSupply(ctx) + app.BankKeeper.SetSupply(ctx, banktypes.NewSupply(prevSupply.GetTotal().Add(totalSupply...))) +} diff --git a/x/tallystrategies/tokenbalancetally/tally_test.go b/x/tallystrategies/tokenbalancetally/tally_test.go new file mode 100644 index 000000000000..690773ede3ae --- /dev/null +++ b/x/tallystrategies/tokenbalancetally/tally_test.go @@ -0,0 +1,249 @@ +package tokenbalancetally_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + tokenbalancetally "github.com/cosmos/cosmos-sdk/x/tallystrategies/tokenbalancetally" +) + +func TestTallyNoOneVotes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createGoverners(t, ctx, app, []int64{5, 5, 5}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.True(t, burnDeposits) + require.True(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyNoQuorum(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + createGoverners(t, ctx, app, []int64{2, 5, 0}) + + addrs := simapp.AddTestAddrsIncremental(app, ctx, 1, sdk.NewInt(10000000)) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + err = app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes) + require.Nil(t, err) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, _ := tally(ctx, proposal) + require.False(t, passes) + require.True(t, burnDeposits) +} + +func TestTallyGovernersAllYes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addrs := createGoverners(t, ctx, app, []int64{5, 5, 5}) + tp := TestProposal + + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, addrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyGoverners51No(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{5, 6, 0}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[1], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, _ := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) +} + +func TestTallyGoverners51Yes(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{5, 6, 0}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[0], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[1], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyGovernersVetoed(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[0], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[2], types.OptionNoWithVeto)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.True(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyGovernersAbstainPasses(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[0], types.OptionAbstain)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[1], types.OptionNo)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[2], types.OptionYes)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.True(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyGovernersAbstainFails(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{6, 6, 7}) + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[0], types.OptionAbstain)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[1], types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddrs[2], types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} + +func TestTallyGovernersNonVoter(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + govAccAddrs := createGoverners(t, ctx, app, []int64{5, 6, 7}) + govAccAddr1, govAccAddr2 := govAccAddrs[0], govAccAddrs[1] + + tp := TestProposal + proposal, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + proposalID := proposal.ProposalId + proposal.Status = types.StatusVotingPeriod + app.GovKeeper.SetProposal(ctx, proposal) + + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddr1, types.OptionYes)) + require.NoError(t, app.GovKeeper.AddVote(ctx, proposalID, govAccAddr2, types.OptionNo)) + + proposal, ok := app.GovKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + tally := tokenbalancetally.NewTokenBalanceTallyHandler(app.GovKeeper, app.BankKeeper, govToken) + passes, burnDeposits, tallyResults := tally(ctx, proposal) + + require.False(t, passes) + require.False(t, burnDeposits) + require.False(t, tallyResults.Equals(types.EmptyTallyResult())) +} diff --git a/x/tallystrategies/tokenbalancetally/tokenbalance.go b/x/tallystrategies/tokenbalancetally/tokenbalance.go new file mode 100644 index 000000000000..738579eb8d20 --- /dev/null +++ b/x/tallystrategies/tokenbalancetally/tokenbalance.go @@ -0,0 +1,78 @@ +package tokenbalancetally + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +// NewTokenBalanceTallyHandler creates a new governance tally strategy for a giving voting to token balances for a particular denom +func NewTokenBalanceTallyHandler(gk govkeeper.Keeper, bk govtypes.BankKeeper, denom string) govtypes.TallyStrategy { + return func(ctx sdk.Context, proposal govtypes.Proposal) (passes bool, burnDeposits bool, tallyResults govtypes.TallyResult) { + return handleTokenBalanceTally(ctx, proposal, gk, bk, denom) + } +} + +// TODO: Break into several smaller functions for clarity + +// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the +// voters +func handleTokenBalanceTally(ctx sdk.Context, proposal govtypes.Proposal, gk govkeeper.Keeper, bk govtypes.BankKeeper, denom string) (passes bool, burnDeposits bool, tallyResults govtypes.TallyResult) { + results := make(map[govtypes.VoteOption]sdk.Dec) + results[govtypes.OptionYes] = sdk.ZeroDec() + results[govtypes.OptionAbstain] = sdk.ZeroDec() + results[govtypes.OptionNo] = sdk.ZeroDec() + results[govtypes.OptionNoWithVeto] = sdk.ZeroDec() + + totalVotingPower := sdk.ZeroDec() + + gk.IterateVotes(ctx, proposal.ProposalId, func(vote govtypes.Vote) bool { + voter, err := sdk.AccAddressFromBech32(vote.Voter) + + if err != nil { + panic(err) + } + + votingPower := bk.GetBalance(ctx, voter, denom).Amount + + results[vote.Option] = results[vote.Option].Add(votingPower.ToDec()) + totalVotingPower = totalVotingPower.Add(votingPower.ToDec()) + + gk.DeleteVote(ctx, vote.ProposalId, voter) + return false + }) + + tallyParams := gk.GetTallyParams(ctx) + tallyResults = govtypes.NewTallyResultFromMap(results) + + denomTotalSupply := bk.GetSupply(ctx).GetTotal().AmountOf(denom) + // If there is no staked coins, the proposal fails + if denomTotalSupply.IsZero() { + return false, false, tallyResults + } + + // If there is not enough quorum of votes, the proposal fails + percentVoting := totalVotingPower.Quo(denomTotalSupply.ToDec()) + if percentVoting.LT(tallyParams.Quorum) { + return false, true, tallyResults + } + + // If no one votes (everyone abstains), proposal fails + if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) { + return false, false, tallyResults + } + + // If more than 1/3 of voters veto, proposal fails + if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(tallyParams.VetoThreshold) { + return false, true, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote Yes, proposal passes + if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(tallyParams.Threshold) { + return true, false, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote No, proposal fails + return false, false, tallyResults +} diff --git a/x/upgrade/types/proposal.go b/x/upgrade/types/proposal.go index a8ea9b629062..3132b8d6ecdd 100644 --- a/x/upgrade/types/proposal.go +++ b/x/upgrade/types/proposal.go @@ -31,6 +31,7 @@ func (sup *SoftwareUpgradeProposal) GetTitle() string { return sup.Title } func (sup *SoftwareUpgradeProposal) GetDescription() string { return sup.Description } func (sup *SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } func (sup *SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } +func (sup *SoftwareUpgradeProposal) TallyRoute() string { return gov.RootTallyRoute } func (sup *SoftwareUpgradeProposal) ValidateBasic() error { if err := sup.Plan.ValidateBasic(); err != nil { return err @@ -63,6 +64,7 @@ func (csup *CancelSoftwareUpgradeProposal) ProposalRoute() string { return Rout func (csup *CancelSoftwareUpgradeProposal) ProposalType() string { return ProposalTypeCancelSoftwareUpgrade } +func (csup *CancelSoftwareUpgradeProposal) TallyRoute() string { return gov.RootTallyRoute } func (csup *CancelSoftwareUpgradeProposal) ValidateBasic() error { return gov.ValidateAbstract(csup) }