Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(x/gov): add MaxVoteOptionsLen #20087

Merged
merged 12 commits into from
Apr 22, 2024
105 changes: 83 additions & 22 deletions api/cosmos/gov/module/v1/module.pulsar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions x/gov/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* [#20087](https://github.com/cosmos/cosmos-sdk/pull/20087) add `MaxVoteOptionsLen`
* [#19592](https://github.com/cosmos/cosmos-sdk/pull/19592) Add custom tally function.
* [#19304](https://github.com/cosmos/cosmos-sdk/pull/19304) Add `MsgSudoExec` for allowing executing any message as a sudo.
* [#19101](https://github.com/cosmos/cosmos-sdk/pull/19101) Add message based params configuration.
Expand Down
2 changes: 2 additions & 0 deletions x/gov/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/g

For a weighted vote to be valid, the `options` field must not contain duplicate vote options, and the sum of weights of all options must be equal to 1.

The maximum number of weighted vote options can be limited by the developer via a config parameter, named `MaxVoteOptionsLen`, which gets passed into the gov keeper.

### Quorum

Quorum is defined as the minimum percentage of voting power that needs to be
Expand Down
69 changes: 69 additions & 0 deletions x/gov/keeper/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,75 @@ func setupGovKeeper(t *testing.T, expectations ...func(sdk.Context, mocks)) (
return govKeeper, m, encCfg, ctx
}

// setupGovKeeperWithMaxVoteOptionsLen creates a govKeeper with a defined maxVoteOptionsLen, as well as all its dependencies.
func setupGovKeeperWithMaxVoteOptionsLen(t *testing.T, maxVoteOptionsLen uint64, expectations ...func(sdk.Context, mocks)) (
*keeper.Keeper,
mocks,
moduletestutil.TestEncodingConfig,
sdk.Context,
) {
t.Helper()
key := storetypes.NewKVStoreKey(types.StoreKey)
storeService := runtime.NewKVStoreService(key)
testCtx := testutil.DefaultContextWithDB(t, key, storetypes.NewTransientStoreKey("transient_test"))
ctx := testCtx.Ctx.WithHeaderInfo(header.Info{Time: time.Now()})
encCfg := moduletestutil.MakeTestEncodingConfig(codectestutil.CodecOptions{})
v1.RegisterInterfaces(encCfg.InterfaceRegistry)
v1beta1.RegisterInterfaces(encCfg.InterfaceRegistry)
banktypes.RegisterInterfaces(encCfg.InterfaceRegistry)

baseApp := baseapp.NewBaseApp(
"authz",
log.NewNopLogger(),
testCtx.DB,
encCfg.TxConfig.TxDecoder(),
)
baseApp.SetCMS(testCtx.CMS)
baseApp.SetInterfaceRegistry(encCfg.InterfaceRegistry)

environment := runtime.NewEnvironment(storeService, log.NewNopLogger(), runtime.EnvWithRouterService(baseApp.GRPCQueryRouter(), baseApp.MsgServiceRouter()))

// gomock initializations
ctrl := gomock.NewController(t)
m := mocks{
acctKeeper: govtestutil.NewMockAccountKeeper(ctrl),
bankKeeper: govtestutil.NewMockBankKeeper(ctrl),
stakingKeeper: govtestutil.NewMockStakingKeeper(ctrl),
poolKeeper: govtestutil.NewMockPoolKeeper(ctrl),
}
if len(expectations) == 0 {
err := mockDefaultExpectations(ctx, m)
require.NoError(t, err)
} else {
for _, exp := range expectations {
exp(ctx, m)
}
}

govAddr, err := m.acctKeeper.AddressCodec().BytesToString(govAcct)
require.NoError(t, err)

config := keeper.DefaultConfig()
config.MaxVoteOptionsLen = maxVoteOptionsLen

// Gov keeper initializations
govKeeper := keeper.NewKeeper(encCfg.Codec, environment, m.acctKeeper, m.bankKeeper, m.stakingKeeper, m.poolKeeper, config, govAddr)
require.NoError(t, govKeeper.ProposalID.Set(ctx, 1))
govRouter := v1beta1.NewRouter() // Also register legacy gov handlers to test them too.
govRouter.AddRoute(types.RouterKey, v1beta1.ProposalHandler)
govKeeper.SetLegacyRouter(govRouter)
err = govKeeper.Params.Set(ctx, v1.DefaultParams())
require.NoError(t, err)
err = govKeeper.Constitution.Set(ctx, "constitution")
require.NoError(t, err)

// Register all handlers for the MegServiceRouter.
v1.RegisterMsgServer(baseApp.MsgServiceRouter(), keeper.NewMsgServerImpl(govKeeper))
banktypes.RegisterMsgServer(baseApp.MsgServiceRouter(), nil) // Nil is fine here as long as we never execute the proposal's Msgs.

return govKeeper, m, encCfg, ctx
}

// trackMockBalances sets up expected calls on the Mock BankKeeper, and also
// locally tracks accounts balances (not modules balances).
func trackMockBalances(bankKeeper *govtestutil.MockBankKeeper) error {
Expand Down
5 changes: 5 additions & 0 deletions x/gov/keeper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ type Config struct {
MaxMetadataLen uint64
// MaxSummaryLen defines the amount of characters that can be used for proposal summary
MaxSummaryLen uint64
// MaxVoteOptionsLen defines the maximum number of vote options a proposal can have.
// This only applies to WeightedVoteOption messages and not to the VoteOption messages
// 0 means this param is disabled, hence all supported options are allowed
MaxVoteOptionsLen uint64
// CalculateVoteResultsAndVotingPowerFn is a function signature for calculating vote results and voting power
// Keeping it nil will use the default implementation
CalculateVoteResultsAndVotingPowerFn CalculateVoteResultsAndVotingPowerFn
Expand All @@ -37,6 +41,7 @@ func DefaultConfig() Config {
MaxTitleLen: 255,
MaxMetadataLen: 255,
MaxSummaryLen: 10200,
MaxVoteOptionsLen: 0, // 0 means this param is disabled, hence all supported options are allowed
CalculateVoteResultsAndVotingPowerFn: nil,
}
}
15 changes: 15 additions & 0 deletions x/gov/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ func NewKeeper(
if config.MaxSummaryLen == 0 {
config.MaxSummaryLen = defaultConfig.MaxSummaryLen
}
// If MaxVoteOptionsLen not set by app developer, set to default value, meaning all supported options are allowed
if config.MaxVoteOptionsLen == 0 {
config.MaxVoteOptionsLen = defaultConfig.MaxVoteOptionsLen
}

sb := collections.NewSchemaBuilder(env.KVStoreService)
k := &Keeper{
Expand Down Expand Up @@ -230,3 +234,14 @@ func (k Keeper) assertSummaryLength(summary string) error {
}
return nil
}

// assertVoteOptionsLen returns an error if given vote options length
// is greater than a pre-defined MaxVoteOptionsLen.
// It's only being checked when config.MaxVoteOptionsLen > 0 (param enabled)
func (k Keeper) assertVoteOptionsLen(options v1.WeightedVoteOptions) error {
maxVoteOptionsLen := k.config.MaxVoteOptionsLen
if maxVoteOptionsLen > 0 && uint64(len(options)) > maxVoteOptionsLen {
return types.ErrTooManyVoteOptions.Wrapf("got %d weighted vote options, maximum allowed is %d", len(options), k.config.MaxVoteOptionsLen)
}
return nil
}
5 changes: 5 additions & 0 deletions x/gov/keeper/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ func (k Keeper) AddVote(ctx context.Context, proposalID uint64, voterAddr sdk.Ac
return err
}

err = k.assertVoteOptionsLen(options)
if err != nil {
return err
}

for _, option := range options {
switch proposal.ProposalType {
case v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC:
Expand Down
Loading
Loading