From 2645e1cda95faee3ea843c9d288ed3c15c303f1a Mon Sep 17 00:00:00 2001 From: Marcello Ardizzone Date: Mon, 22 Apr 2024 11:46:53 +0200 Subject: [PATCH] feat(x/gov): add MaxVoteOptionsLen (#20087) --- api/cosmos/gov/module/v1/module.pulsar.go | 105 ++++++++++++++---- x/gov/CHANGELOG.md | 1 + x/gov/README.md | 2 + x/gov/keeper/common_test.go | 69 ++++++++++++ x/gov/keeper/config.go | 5 + x/gov/keeper/keeper.go | 15 +++ x/gov/keeper/vote.go | 5 + x/gov/keeper/vote_test.go | 60 ++++++++++ x/gov/proto/cosmos/gov/module/v1/module.proto | 4 + x/gov/types/errors.go | 1 + 10 files changed, 245 insertions(+), 22 deletions(-) diff --git a/api/cosmos/gov/module/v1/module.pulsar.go b/api/cosmos/gov/module/v1/module.pulsar.go index 2df55ac73881..5355b463d7d8 100644 --- a/api/cosmos/gov/module/v1/module.pulsar.go +++ b/api/cosmos/gov/module/v1/module.pulsar.go @@ -14,11 +14,12 @@ import ( ) var ( - md_Module protoreflect.MessageDescriptor - fd_Module_max_metadata_len protoreflect.FieldDescriptor - fd_Module_authority protoreflect.FieldDescriptor - fd_Module_max_title_len protoreflect.FieldDescriptor - fd_Module_max_summary_len protoreflect.FieldDescriptor + md_Module protoreflect.MessageDescriptor + fd_Module_max_metadata_len protoreflect.FieldDescriptor + fd_Module_authority protoreflect.FieldDescriptor + fd_Module_max_title_len protoreflect.FieldDescriptor + fd_Module_max_summary_len protoreflect.FieldDescriptor + fd_Module_max_vote_options_len protoreflect.FieldDescriptor ) func init() { @@ -28,6 +29,7 @@ func init() { fd_Module_authority = md_Module.Fields().ByName("authority") fd_Module_max_title_len = md_Module.Fields().ByName("max_title_len") fd_Module_max_summary_len = md_Module.Fields().ByName("max_summary_len") + fd_Module_max_vote_options_len = md_Module.Fields().ByName("max_vote_options_len") } var _ protoreflect.Message = (*fastReflection_Module)(nil) @@ -119,6 +121,12 @@ func (x *fastReflection_Module) Range(f func(protoreflect.FieldDescriptor, proto return } } + if x.MaxVoteOptionsLen != uint64(0) { + value := protoreflect.ValueOfUint64(x.MaxVoteOptionsLen) + if !f(fd_Module_max_vote_options_len, value) { + return + } + } } // Has reports whether a field is populated. @@ -142,6 +150,8 @@ func (x *fastReflection_Module) Has(fd protoreflect.FieldDescriptor) bool { return x.MaxTitleLen != uint64(0) case "cosmos.gov.module.v1.Module.max_summary_len": return x.MaxSummaryLen != uint64(0) + case "cosmos.gov.module.v1.Module.max_vote_options_len": + return x.MaxVoteOptionsLen != uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -166,6 +176,8 @@ func (x *fastReflection_Module) Clear(fd protoreflect.FieldDescriptor) { x.MaxTitleLen = uint64(0) case "cosmos.gov.module.v1.Module.max_summary_len": x.MaxSummaryLen = uint64(0) + case "cosmos.gov.module.v1.Module.max_vote_options_len": + x.MaxVoteOptionsLen = uint64(0) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -194,6 +206,9 @@ func (x *fastReflection_Module) Get(descriptor protoreflect.FieldDescriptor) pro case "cosmos.gov.module.v1.Module.max_summary_len": value := x.MaxSummaryLen return protoreflect.ValueOfUint64(value) + case "cosmos.gov.module.v1.Module.max_vote_options_len": + value := x.MaxVoteOptionsLen + return protoreflect.ValueOfUint64(value) default: if descriptor.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -222,6 +237,8 @@ func (x *fastReflection_Module) Set(fd protoreflect.FieldDescriptor, value proto x.MaxTitleLen = value.Uint() case "cosmos.gov.module.v1.Module.max_summary_len": x.MaxSummaryLen = value.Uint() + case "cosmos.gov.module.v1.Module.max_vote_options_len": + x.MaxVoteOptionsLen = value.Uint() default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -250,6 +267,8 @@ func (x *fastReflection_Module) Mutable(fd protoreflect.FieldDescriptor) protore panic(fmt.Errorf("field max_title_len of message cosmos.gov.module.v1.Module is not mutable")) case "cosmos.gov.module.v1.Module.max_summary_len": panic(fmt.Errorf("field max_summary_len of message cosmos.gov.module.v1.Module is not mutable")) + case "cosmos.gov.module.v1.Module.max_vote_options_len": + panic(fmt.Errorf("field max_vote_options_len of message cosmos.gov.module.v1.Module is not mutable")) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -271,6 +290,8 @@ func (x *fastReflection_Module) NewField(fd protoreflect.FieldDescriptor) protor return protoreflect.ValueOfUint64(uint64(0)) case "cosmos.gov.module.v1.Module.max_summary_len": return protoreflect.ValueOfUint64(uint64(0)) + case "cosmos.gov.module.v1.Module.max_vote_options_len": + return protoreflect.ValueOfUint64(uint64(0)) default: if fd.IsExtension() { panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.gov.module.v1.Module")) @@ -353,6 +374,9 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { if x.MaxSummaryLen != 0 { n += 1 + runtime.Sov(uint64(x.MaxSummaryLen)) } + if x.MaxVoteOptionsLen != 0 { + n += 1 + runtime.Sov(uint64(x.MaxVoteOptionsLen)) + } if x.unknownFields != nil { n += len(x.unknownFields) } @@ -382,6 +406,11 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { i -= len(x.unknownFields) copy(dAtA[i:], x.unknownFields) } + if x.MaxVoteOptionsLen != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.MaxVoteOptionsLen)) + i-- + dAtA[i] = 0x28 + } if x.MaxSummaryLen != 0 { i = runtime.EncodeVarint(dAtA, i, uint64(x.MaxSummaryLen)) i-- @@ -542,6 +571,25 @@ func (x *fastReflection_Module) ProtoMethods() *protoiface.Methods { break } } + case 5: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MaxVoteOptionsLen", wireType) + } + x.MaxVoteOptionsLen = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.MaxVoteOptionsLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := runtime.Skip(dAtA[iNdEx:]) @@ -607,6 +655,9 @@ type Module struct { // max_summary_len defines the maximum proposal summary length. // Defaults to 10200 if not explicitly set. MaxSummaryLen uint64 `protobuf:"varint,4,opt,name=max_summary_len,json=maxSummaryLen,proto3" json:"max_summary_len,omitempty"` + // max_vote_options_len defines the maximum number of vote options a proposal can have. + // Defaults to 0 if not explicitly set. + MaxVoteOptionsLen uint64 `protobuf:"varint,5,opt,name=max_vote_options_len,json=maxVoteOptionsLen,proto3" json:"max_vote_options_len,omitempty"` } func (x *Module) Reset() { @@ -657,6 +708,13 @@ func (x *Module) GetMaxSummaryLen() uint64 { return 0 } +func (x *Module) GetMaxVoteOptionsLen() uint64 { + if x != nil { + return x.MaxVoteOptionsLen + } + return 0 +} + var File_cosmos_gov_module_v1_module_proto protoreflect.FileDescriptor var file_cosmos_gov_module_v1_module_proto_rawDesc = []byte{ @@ -665,7 +723,7 @@ var file_cosmos_gov_module_v1_module_proto_rawDesc = []byte{ 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x6f, 0x76, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x20, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x6d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb8, 0x01, 0x0a, 0x06, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe9, 0x01, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4c, 0x65, 0x6e, @@ -675,22 +733,25 @@ var file_cosmos_gov_module_v1_module_proto_rawDesc = []byte{ 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x4c, 0x65, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6c, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6d, 0x61, 0x78, - 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x4c, 0x65, 0x6e, 0x3a, 0x1a, 0xba, 0xc0, 0x96, 0xda, - 0x01, 0x14, 0x0a, 0x12, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, - 0x2f, 0x78, 0x2f, 0x67, 0x6f, 0x76, 0x42, 0xca, 0x01, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x63, - 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x6f, 0x76, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x76, 0x2f, - 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, - 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47, 0x4d, 0xaa, 0x02, 0x14, 0x43, 0x6f, 0x73, 0x6d, 0x6f, - 0x73, 0x2e, 0x47, 0x6f, 0x76, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0xca, - 0x02, 0x14, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x6f, 0x76, 0x5c, 0x4d, 0x6f, 0x64, - 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x20, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, - 0x47, 0x6f, 0x76, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, - 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x17, 0x43, 0x6f, 0x73, 0x6d, - 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x6f, 0x76, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x3a, - 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x4c, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x61, + 0x78, 0x5f, 0x76, 0x6f, 0x74, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x6c, + 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x56, 0x6f, 0x74, + 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4c, 0x65, 0x6e, 0x3a, 0x1a, 0xba, 0xc0, 0x96, + 0xda, 0x01, 0x14, 0x0a, 0x12, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, + 0x6f, 0x2f, 0x78, 0x2f, 0x67, 0x6f, 0x76, 0x42, 0xca, 0x01, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x6f, 0x76, 0x2e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, + 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x76, + 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47, 0x4d, 0xaa, 0x02, 0x14, 0x43, 0x6f, 0x73, 0x6d, + 0x6f, 0x73, 0x2e, 0x47, 0x6f, 0x76, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x14, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x6f, 0x76, 0x5c, 0x4d, 0x6f, + 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x20, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, + 0x5c, 0x47, 0x6f, 0x76, 0x5c, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, + 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x17, 0x43, 0x6f, 0x73, + 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x6f, 0x76, 0x3a, 0x3a, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/x/gov/CHANGELOG.md b/x/gov/CHANGELOG.md index 722c5f5f780b..28b3668741b3 100644 --- a/x/gov/CHANGELOG.md +++ b/x/gov/CHANGELOG.md @@ -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. diff --git a/x/gov/README.md b/x/gov/README.md index 919afe01ec72..fdfdc4632623 100644 --- a/x/gov/README.md +++ b/x/gov/README.md @@ -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 diff --git a/x/gov/keeper/common_test.go b/x/gov/keeper/common_test.go index 3e1be306d15f..c8eb206c0d7d 100644 --- a/x/gov/keeper/common_test.go +++ b/x/gov/keeper/common_test.go @@ -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 { diff --git a/x/gov/keeper/config.go b/x/gov/keeper/config.go index 5e8f3a7d0c6a..ea01891974a5 100644 --- a/x/gov/keeper/config.go +++ b/x/gov/keeper/config.go @@ -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 @@ -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, } } diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go index 57464eb01181..264f9fc0722a 100644 --- a/x/gov/keeper/keeper.go +++ b/x/gov/keeper/keeper.go @@ -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{ @@ -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 +} diff --git a/x/gov/keeper/vote.go b/x/gov/keeper/vote.go index f1f13d7d71c8..25d11e514722 100644 --- a/x/gov/keeper/vote.go +++ b/x/gov/keeper/vote.go @@ -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: diff --git a/x/gov/keeper/vote_test.go b/x/gov/keeper/vote_test.go index b008f804f7e3..502cb8569ad4 100644 --- a/x/gov/keeper/vote_test.go +++ b/x/gov/keeper/vote_test.go @@ -166,3 +166,63 @@ func TestVotes_MultipleChoiceProposal(t *testing.T) { require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[1], v1.NewNonSplitVoteOption(v1.OptionTwo), "")) require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(v1.OptionThree), "")) } + +func TestVotes_CustomMaxVoteOptionsLen(t *testing.T) { + maxVoteOptionsLen := 3 + govKeeper, mocks, _, ctx := setupGovKeeperWithMaxVoteOptionsLen(t, uint64(maxVoteOptionsLen)) + authKeeper, bankKeeper, stakingKeeper := mocks.acctKeeper, mocks.bankKeeper, mocks.stakingKeeper + addrs := simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 2, sdkmath.NewInt(10000000)) + authKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes() + + addrs1Str, err := authKeeper.AddressCodec().BytesToString(addrs[1]) + require.NoError(t, err) + + tp := TestProposal + proposal, err := govKeeper.SubmitProposal(ctx, tp, "", "title", "description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r"), v1.ProposalType_PROPOSAL_TYPE_STANDARD) + require.NoError(t, err) + proposalID := proposal.Id + metadata := "metadata" + + require.Error(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), metadata), "proposal not on voting period") + require.Error(t, govKeeper.AddVote(ctx, 10, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""), "invalid proposal ID") + + proposal.Status = v1.StatusVotingPeriod + err = govKeeper.Proposals.Set(ctx, proposal.Id, proposal) + require.NoError(t, err) + + // not exceeding MaxVoteOptionsLen, no errors should be thrown + require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[1], v1.WeightedVoteOptions{ + v1.NewWeightedVoteOption(v1.OptionYes, sdkmath.LegacyNewDecWithPrec(60, 2)), + v1.NewWeightedVoteOption(v1.OptionNo, sdkmath.LegacyNewDecWithPrec(30, 2)), + v1.NewWeightedVoteOption(v1.OptionAbstain, sdkmath.LegacyNewDecWithPrec(10, 2)), + }, "")) + vote, err := govKeeper.Votes.Get(ctx, collections.Join(proposalID, addrs[1])) + require.Nil(t, err) + require.Equal(t, addrs1Str, vote.Voter) + require.Equal(t, proposalID, vote.ProposalId) + require.True(t, len(vote.Options) == 3) + require.Equal(t, v1.OptionYes, vote.Options[0].Option) + require.Equal(t, v1.OptionNo, vote.Options[1].Option) + require.Equal(t, v1.OptionAbstain, vote.Options[2].Option) + require.Equal(t, vote.Options[0].Weight, sdkmath.LegacyNewDecWithPrec(60, 2).String()) + require.Equal(t, vote.Options[1].Weight, sdkmath.LegacyNewDecWithPrec(30, 2).String()) + require.Equal(t, vote.Options[2].Weight, sdkmath.LegacyNewDecWithPrec(10, 2).String()) + + // exceeding MaxVoteOptionsLen, an error should be thrown + err = govKeeper.AddVote(ctx, proposalID, addrs[1], v1.WeightedVoteOptions{ + v1.NewWeightedVoteOption(v1.OptionYes, sdkmath.LegacyNewDecWithPrec(60, 2)), + v1.NewWeightedVoteOption(v1.OptionNo, sdkmath.LegacyNewDecWithPrec(30, 2)), + v1.NewWeightedVoteOption(v1.OptionAbstain, sdkmath.LegacyNewDecWithPrec(5, 2)), + v1.NewWeightedVoteOption(v1.OptionNoWithVeto, sdkmath.LegacyNewDecWithPrec(5, 2)), + }, "") + require.Error(t, err) + require.Contains(t, err.Error(), "too many weighted vote options") + + // only one vote should have gone through + var votes v1.Votes + require.NoError(t, govKeeper.Votes.Walk(ctx, nil, func(_ collections.Pair[uint64, sdk.AccAddress], value v1.Vote) (stop bool, err error) { + votes = append(votes, &value) + return false, nil + })) + require.Len(t, votes, 1) +} diff --git a/x/gov/proto/cosmos/gov/module/v1/module.proto b/x/gov/proto/cosmos/gov/module/v1/module.proto index a6803c67c4b9..7eb865841348 100644 --- a/x/gov/proto/cosmos/gov/module/v1/module.proto +++ b/x/gov/proto/cosmos/gov/module/v1/module.proto @@ -24,4 +24,8 @@ message Module { // max_summary_len defines the maximum proposal summary length. // Defaults to 10200 if not explicitly set. uint64 max_summary_len = 4; + + // max_vote_options_len defines the maximum number of vote options a proposal can have. + // Defaults to 0 if not explicitly set. + uint64 max_vote_options_len = 5; } diff --git a/x/gov/types/errors.go b/x/gov/types/errors.go index 95e06916092c..ba6b81b68934 100644 --- a/x/gov/types/errors.go +++ b/x/gov/types/errors.go @@ -27,4 +27,5 @@ var ( ErrInvalidDepositDenom = errors.Register(ModuleName, 23, "invalid deposit denom") ErrTitleTooLong = errors.Register(ModuleName, 24, "title too long") ErrTooLateToCancel = errors.Register(ModuleName, 25, "too late to cancel proposal") + ErrTooManyVoteOptions = errors.Register(ModuleName, 26, "too many weighted vote options") )