github.com/cosmos/cosmos-sdk@v0.50.10/x/group/keeper/keeper_test.go (about)

     1  package keeper_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/binary"
     6  	"testing"
     7  	"time"
     8  
     9  	cmttime "github.com/cometbft/cometbft/types/time"
    10  	"github.com/golang/mock/gomock"
    11  	"github.com/stretchr/testify/suite"
    12  
    13  	"cosmossdk.io/log"
    14  	storetypes "cosmossdk.io/store/types"
    15  
    16  	"github.com/cosmos/cosmos-sdk/baseapp"
    17  	"github.com/cosmos/cosmos-sdk/codec/address"
    18  	"github.com/cosmos/cosmos-sdk/testutil"
    19  	simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
    20  	sdk "github.com/cosmos/cosmos-sdk/types"
    21  	moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
    22  	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    23  	"github.com/cosmos/cosmos-sdk/x/bank"
    24  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    25  	"github.com/cosmos/cosmos-sdk/x/group"
    26  	"github.com/cosmos/cosmos-sdk/x/group/keeper"
    27  	"github.com/cosmos/cosmos-sdk/x/group/module"
    28  	grouptestutil "github.com/cosmos/cosmos-sdk/x/group/testutil"
    29  	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
    30  )
    31  
    32  const minExecutionPeriod = 5 * time.Second
    33  
    34  type TestSuite struct {
    35  	suite.Suite
    36  
    37  	sdkCtx          sdk.Context
    38  	ctx             context.Context
    39  	addrs           []sdk.AccAddress
    40  	groupID         uint64
    41  	groupPolicyAddr sdk.AccAddress
    42  	policy          group.DecisionPolicy
    43  	groupKeeper     keeper.Keeper
    44  	blockTime       time.Time
    45  	bankKeeper      *grouptestutil.MockBankKeeper
    46  	accountKeeper   *grouptestutil.MockAccountKeeper
    47  }
    48  
    49  func (s *TestSuite) SetupTest() {
    50  	s.blockTime = cmttime.Now()
    51  	key := storetypes.NewKVStoreKey(group.StoreKey)
    52  
    53  	testCtx := testutil.DefaultContextWithDB(s.T(), key, storetypes.NewTransientStoreKey("transient_test"))
    54  	encCfg := moduletestutil.MakeTestEncodingConfig(module.AppModuleBasic{}, bank.AppModuleBasic{})
    55  	s.addrs = simtestutil.CreateIncrementalAccounts(6)
    56  
    57  	// setup gomock and initialize some globally expected executions
    58  	ctrl := gomock.NewController(s.T())
    59  	s.accountKeeper = grouptestutil.NewMockAccountKeeper(ctrl)
    60  	for i := range s.addrs {
    61  		s.accountKeeper.EXPECT().GetAccount(gomock.Any(), s.addrs[i]).Return(authtypes.NewBaseAccountWithAddress(s.addrs[i])).AnyTimes()
    62  	}
    63  	s.accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes()
    64  
    65  	s.bankKeeper = grouptestutil.NewMockBankKeeper(ctrl)
    66  
    67  	bApp := baseapp.NewBaseApp(
    68  		"group",
    69  		log.NewNopLogger(),
    70  		testCtx.DB,
    71  		encCfg.TxConfig.TxDecoder(),
    72  	)
    73  	bApp.SetInterfaceRegistry(encCfg.InterfaceRegistry)
    74  	banktypes.RegisterMsgServer(bApp.MsgServiceRouter(), s.bankKeeper)
    75  
    76  	config := group.DefaultConfig()
    77  	s.groupKeeper = keeper.NewKeeper(key, encCfg.Codec, bApp.MsgServiceRouter(), s.accountKeeper, config)
    78  	s.ctx = testCtx.Ctx.WithBlockTime(s.blockTime)
    79  	s.sdkCtx = sdk.UnwrapSDKContext(s.ctx)
    80  
    81  	// Initial group, group policy and balance setup
    82  	members := []group.MemberRequest{
    83  		{Address: s.addrs[4].String(), Weight: "1"}, {Address: s.addrs[1].String(), Weight: "2"},
    84  	}
    85  
    86  	s.setNextAccount()
    87  
    88  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
    89  		Admin:   s.addrs[0].String(),
    90  		Members: members,
    91  	})
    92  	s.Require().NoError(err)
    93  	s.groupID = groupRes.GroupId
    94  
    95  	policy := group.NewThresholdDecisionPolicy(
    96  		"2",
    97  		time.Second,
    98  		minExecutionPeriod, // Must wait 5 seconds before executing proposal
    99  	)
   100  	policyReq := &group.MsgCreateGroupPolicy{
   101  		Admin:   s.addrs[0].String(),
   102  		GroupId: s.groupID,
   103  	}
   104  	err = policyReq.SetDecisionPolicy(policy)
   105  	s.Require().NoError(err)
   106  	s.setNextAccount()
   107  
   108  	groupSeq := s.groupKeeper.GetGroupSequence(s.sdkCtx)
   109  	s.Require().Equal(groupSeq, uint64(1))
   110  
   111  	policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
   112  	s.Require().NoError(err)
   113  
   114  	addrbz, err := address.NewBech32Codec("cosmos").StringToBytes(policyRes.Address)
   115  	s.Require().NoError(err)
   116  	s.policy = policy
   117  	s.groupPolicyAddr = addrbz
   118  
   119  	s.bankKeeper.EXPECT().MintCoins(s.sdkCtx, minttypes.ModuleName, sdk.Coins{sdk.NewInt64Coin("test", 100000)}).Return(nil).AnyTimes()
   120  	s.bankKeeper.MintCoins(s.sdkCtx, minttypes.ModuleName, sdk.Coins{sdk.NewInt64Coin("test", 100000)})
   121  	s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)}).Return(nil).AnyTimes()
   122  	s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)})
   123  }
   124  
   125  func (s *TestSuite) setNextAccount() {
   126  	nextAccVal := s.groupKeeper.GetGroupPolicySeq(s.sdkCtx) + 1
   127  	derivationKey := make([]byte, 8)
   128  	binary.BigEndian.PutUint64(derivationKey, nextAccVal)
   129  
   130  	ac, err := authtypes.NewModuleCredential(group.ModuleName, []byte{keeper.GroupPolicyTablePrefix}, derivationKey)
   131  	s.Require().NoError(err)
   132  
   133  	groupPolicyAcc, err := authtypes.NewBaseAccountWithPubKey(ac)
   134  	s.Require().NoError(err)
   135  
   136  	groupPolicyAccBumpAccountNumber, err := authtypes.NewBaseAccountWithPubKey(ac)
   137  	s.Require().NoError(err)
   138  	groupPolicyAccBumpAccountNumber.SetAccountNumber(nextAccVal)
   139  
   140  	s.Require().NoError(err)
   141  
   142  	s.accountKeeper.EXPECT().GetAccount(gomock.Any(), sdk.AccAddress(ac.Address())).Return(nil).AnyTimes()
   143  	s.accountKeeper.EXPECT().NewAccount(gomock.Any(), groupPolicyAcc).Return(groupPolicyAccBumpAccountNumber).AnyTimes()
   144  	s.accountKeeper.EXPECT().SetAccount(gomock.Any(), sdk.AccountI(groupPolicyAccBumpAccountNumber)).Return().AnyTimes()
   145  }
   146  
   147  func TestKeeperTestSuite(t *testing.T) {
   148  	suite.Run(t, new(TestSuite))
   149  }
   150  
   151  func (s *TestSuite) TestProposalsByVPEnd() {
   152  	addrs := s.addrs
   153  	addr2 := addrs[1]
   154  
   155  	votingPeriod := s.policy.GetVotingPeriod()
   156  	ctx := s.sdkCtx
   157  	now := time.Now()
   158  
   159  	msgSend := &banktypes.MsgSend{
   160  		FromAddress: s.groupPolicyAddr.String(),
   161  		ToAddress:   addr2.String(),
   162  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
   163  	}
   164  
   165  	proposers := []string{addr2.String()}
   166  
   167  	specs := map[string]struct {
   168  		preRun     func(sdkCtx sdk.Context) uint64
   169  		proposalID uint64
   170  		admin      string
   171  		expErrMsg  string
   172  		newCtx     sdk.Context
   173  		tallyRes   group.TallyResult
   174  		expStatus  group.ProposalStatus
   175  	}{
   176  		"tally updated after voting period end": {
   177  			preRun: func(sdkCtx sdk.Context) uint64 {
   178  				return submitProposal(sdkCtx, s, []sdk.Msg{msgSend}, proposers)
   179  			},
   180  			admin:     proposers[0],
   181  			newCtx:    ctx.WithBlockTime(now.Add(votingPeriod).Add(time.Hour)),
   182  			tallyRes:  group.DefaultTallyResult(),
   183  			expStatus: group.PROPOSAL_STATUS_REJECTED,
   184  		},
   185  		"tally within voting period": {
   186  			preRun: func(sdkCtx sdk.Context) uint64 {
   187  				return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
   188  			},
   189  			admin:     proposers[0],
   190  			newCtx:    ctx,
   191  			tallyRes:  group.DefaultTallyResult(),
   192  			expStatus: group.PROPOSAL_STATUS_SUBMITTED,
   193  		},
   194  		"tally within voting period (with votes)": {
   195  			preRun: func(sdkCtx sdk.Context) uint64 {
   196  				return submitProposalAndVote(s.ctx, s, []sdk.Msg{msgSend}, proposers, group.VOTE_OPTION_YES)
   197  			},
   198  			admin:     proposers[0],
   199  			newCtx:    ctx,
   200  			tallyRes:  group.DefaultTallyResult(),
   201  			expStatus: group.PROPOSAL_STATUS_SUBMITTED,
   202  		},
   203  		"tally after voting period (with votes)": {
   204  			preRun: func(sdkCtx sdk.Context) uint64 {
   205  				return submitProposalAndVote(s.ctx, s, []sdk.Msg{msgSend}, proposers, group.VOTE_OPTION_YES)
   206  			},
   207  			admin:  proposers[0],
   208  			newCtx: ctx.WithBlockTime(now.Add(votingPeriod).Add(time.Hour)),
   209  			tallyRes: group.TallyResult{
   210  				YesCount:        "2",
   211  				NoCount:         "0",
   212  				NoWithVetoCount: "0",
   213  				AbstainCount:    "0",
   214  			},
   215  			expStatus: group.PROPOSAL_STATUS_ACCEPTED,
   216  		},
   217  		"tally after voting period (not passing)": {
   218  			preRun: func(sdkCtx sdk.Context) uint64 {
   219  				// `s.addrs[4]` has weight 1
   220  				return submitProposalAndVote(s.ctx, s, []sdk.Msg{msgSend}, []string{s.addrs[4].String()}, group.VOTE_OPTION_YES)
   221  			},
   222  			admin:  proposers[0],
   223  			newCtx: ctx.WithBlockTime(now.Add(votingPeriod).Add(time.Hour)),
   224  			tallyRes: group.TallyResult{
   225  				YesCount:        "1",
   226  				NoCount:         "0",
   227  				NoWithVetoCount: "0",
   228  				AbstainCount:    "0",
   229  			},
   230  			expStatus: group.PROPOSAL_STATUS_REJECTED,
   231  		},
   232  		"tally of withdrawn proposal": {
   233  			preRun: func(sdkCtx sdk.Context) uint64 {
   234  				pID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
   235  				_, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{
   236  					ProposalId: pID,
   237  					Address:    proposers[0],
   238  				})
   239  
   240  				s.Require().NoError(err)
   241  				return pID
   242  			},
   243  			admin:     proposers[0],
   244  			newCtx:    ctx,
   245  			tallyRes:  group.DefaultTallyResult(),
   246  			expStatus: group.PROPOSAL_STATUS_WITHDRAWN,
   247  		},
   248  		"tally of withdrawn proposal (with votes)": {
   249  			preRun: func(sdkCtx sdk.Context) uint64 {
   250  				pID := submitProposalAndVote(s.ctx, s, []sdk.Msg{msgSend}, proposers, group.VOTE_OPTION_YES)
   251  				_, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{
   252  					ProposalId: pID,
   253  					Address:    proposers[0],
   254  				})
   255  
   256  				s.Require().NoError(err)
   257  				return pID
   258  			},
   259  			admin:     proposers[0],
   260  			newCtx:    ctx,
   261  			tallyRes:  group.DefaultTallyResult(),
   262  			expStatus: group.PROPOSAL_STATUS_WITHDRAWN,
   263  		},
   264  	}
   265  
   266  	for msg, spec := range specs {
   267  		spec := spec
   268  		s.Run(msg, func() {
   269  			pID := spec.preRun(s.sdkCtx)
   270  
   271  			module.EndBlocker(spec.newCtx, s.groupKeeper)
   272  			resp, err := s.groupKeeper.Proposal(spec.newCtx, &group.QueryProposalRequest{
   273  				ProposalId: pID,
   274  			})
   275  
   276  			if spec.expErrMsg != "" {
   277  				s.Require().Error(err)
   278  				s.Require().Contains(err.Error(), spec.expErrMsg)
   279  				return
   280  			}
   281  
   282  			s.Require().NoError(err)
   283  			s.Require().Equal(resp.GetProposal().FinalTallyResult, spec.tallyRes)
   284  			s.Require().Equal(resp.GetProposal().Status, spec.expStatus)
   285  		})
   286  	}
   287  }
   288  
   289  func (s *TestSuite) TestPruneProposals() {
   290  	addrs := s.addrs
   291  	expirationTime := time.Hour * 24 * 15 // 15 days
   292  	groupID := s.groupID
   293  	accountAddr := s.groupPolicyAddr
   294  
   295  	msgSend := &banktypes.MsgSend{
   296  		FromAddress: s.groupPolicyAddr.String(),
   297  		ToAddress:   addrs[0].String(),
   298  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
   299  	}
   300  
   301  	policyReq := &group.MsgCreateGroupPolicy{
   302  		Admin:   addrs[0].String(),
   303  		GroupId: groupID,
   304  	}
   305  
   306  	policy := group.NewThresholdDecisionPolicy("100", time.Microsecond, time.Microsecond)
   307  	err := policyReq.SetDecisionPolicy(policy)
   308  	s.Require().NoError(err)
   309  
   310  	s.setNextAccount()
   311  
   312  	_, err = s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
   313  	s.Require().NoError(err)
   314  
   315  	req := &group.MsgSubmitProposal{
   316  		GroupPolicyAddress: accountAddr.String(),
   317  		Proposers:          []string{addrs[1].String()},
   318  	}
   319  	err = req.SetMsgs([]sdk.Msg{msgSend})
   320  	s.Require().NoError(err)
   321  	submittedProposal, err := s.groupKeeper.SubmitProposal(s.ctx, req)
   322  	s.Require().NoError(err)
   323  	queryProposal := group.QueryProposalRequest{ProposalId: submittedProposal.ProposalId}
   324  	prePrune, err := s.groupKeeper.Proposal(s.ctx, &queryProposal)
   325  	s.Require().NoError(err)
   326  	s.Require().Equal(prePrune.Proposal.Id, submittedProposal.ProposalId)
   327  	// Move Forward in time for 15 days, after voting period end + max_execution_period
   328  	s.sdkCtx = s.sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(expirationTime))
   329  
   330  	// Prune Expired Proposals
   331  	err = s.groupKeeper.PruneProposals(s.sdkCtx)
   332  	s.Require().NoError(err)
   333  	postPrune, err := s.groupKeeper.Proposal(s.ctx, &queryProposal)
   334  	s.Require().Nil(postPrune)
   335  	s.Require().Error(err)
   336  	s.Require().Contains(err.Error(), "load proposal: not found")
   337  }
   338  
   339  func submitProposal(
   340  	ctx context.Context, s *TestSuite, msgs []sdk.Msg,
   341  	proposers []string,
   342  ) uint64 {
   343  	proposalReq := &group.MsgSubmitProposal{
   344  		GroupPolicyAddress: s.groupPolicyAddr.String(),
   345  		Proposers:          proposers,
   346  	}
   347  	err := proposalReq.SetMsgs(msgs)
   348  	s.Require().NoError(err)
   349  
   350  	proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
   351  	s.Require().NoError(err)
   352  	return proposalRes.ProposalId
   353  }
   354  
   355  func submitProposalAndVote(
   356  	ctx context.Context, s *TestSuite, msgs []sdk.Msg,
   357  	proposers []string, voteOption group.VoteOption,
   358  ) uint64 {
   359  	s.Require().Greater(len(proposers), 0)
   360  	myProposalID := submitProposal(ctx, s, msgs, proposers)
   361  
   362  	_, err := s.groupKeeper.Vote(ctx, &group.MsgVote{
   363  		ProposalId: myProposalID,
   364  		Voter:      proposers[0],
   365  		Option:     voteOption,
   366  	})
   367  	s.Require().NoError(err)
   368  	return myProposalID
   369  }
   370  
   371  func (s *TestSuite) createGroupAndGroupPolicy(
   372  	admin sdk.AccAddress,
   373  	members []group.MemberRequest,
   374  	policy group.DecisionPolicy,
   375  ) (policyAddr string, groupID uint64) {
   376  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
   377  		Admin:   admin.String(),
   378  		Members: members,
   379  	})
   380  	s.Require().NoError(err)
   381  
   382  	groupID = groupRes.GroupId
   383  	groupPolicy := &group.MsgCreateGroupPolicy{
   384  		Admin:   admin.String(),
   385  		GroupId: groupID,
   386  	}
   387  
   388  	if policy != nil {
   389  		err = groupPolicy.SetDecisionPolicy(policy)
   390  		s.Require().NoError(err)
   391  
   392  		s.setNextAccount()
   393  
   394  		groupPolicyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, groupPolicy)
   395  		s.Require().NoError(err)
   396  		policyAddr = groupPolicyRes.Address
   397  	}
   398  
   399  	return policyAddr, groupID
   400  }
   401  
   402  func (s *TestSuite) TestTallyProposalsAtVPEnd() {
   403  	addrs := s.addrs
   404  	addr1 := addrs[0]
   405  	addr2 := addrs[1]
   406  	votingPeriod := 4 * time.Minute
   407  	minExecutionPeriod := votingPeriod + group.DefaultConfig().MaxExecutionPeriod
   408  
   409  	groupMsg := &group.MsgCreateGroupWithPolicy{
   410  		Admin: addr1.String(),
   411  		Members: []group.MemberRequest{
   412  			{Address: addr1.String(), Weight: "1"},
   413  			{Address: addr2.String(), Weight: "1"},
   414  		},
   415  	}
   416  	policy := group.NewThresholdDecisionPolicy(
   417  		"1",
   418  		votingPeriod,
   419  		minExecutionPeriod,
   420  	)
   421  	s.Require().NoError(groupMsg.SetDecisionPolicy(policy))
   422  
   423  	s.setNextAccount()
   424  	groupRes, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, groupMsg)
   425  	s.Require().NoError(err)
   426  	accountAddr := groupRes.GetGroupPolicyAddress()
   427  	groupPolicy, err := s.accountKeeper.AddressCodec().StringToBytes(accountAddr)
   428  	s.Require().NoError(err)
   429  	s.Require().NotNil(groupPolicy)
   430  
   431  	proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, &group.MsgSubmitProposal{
   432  		GroupPolicyAddress: accountAddr,
   433  		Proposers:          []string{addr1.String()},
   434  		Messages:           nil,
   435  	})
   436  	s.Require().NoError(err)
   437  
   438  	_, err = s.groupKeeper.Vote(s.ctx, &group.MsgVote{
   439  		ProposalId: proposalRes.ProposalId,
   440  		Voter:      addr1.String(),
   441  		Option:     group.VOTE_OPTION_YES,
   442  	})
   443  	s.Require().NoError(err)
   444  
   445  	// move forward in time
   446  	ctx := s.sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(votingPeriod + 1))
   447  
   448  	result, err := s.groupKeeper.TallyResult(ctx, &group.QueryTallyResultRequest{
   449  		ProposalId: proposalRes.ProposalId,
   450  	})
   451  	s.Require().Equal("1", result.Tally.YesCount)
   452  	s.Require().NoError(err)
   453  
   454  	s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctx))
   455  	s.NotPanics(func() { module.EndBlocker(ctx, s.groupKeeper) })
   456  }
   457  
   458  // TestTallyProposalsAtVPEnd_GroupMemberLeaving test that the node doesn't
   459  // panic if a member leaves after the voting period end.
   460  func (s *TestSuite) TestTallyProposalsAtVPEnd_GroupMemberLeaving() {
   461  	addrs := s.addrs
   462  	addr1 := addrs[0]
   463  	addr2 := addrs[1]
   464  	addr3 := addrs[2]
   465  	votingPeriod := 4 * time.Minute
   466  	minExecutionPeriod := votingPeriod + group.DefaultConfig().MaxExecutionPeriod
   467  
   468  	groupMsg := &group.MsgCreateGroupWithPolicy{
   469  		Admin: addr1.String(),
   470  		Members: []group.MemberRequest{
   471  			{Address: addr1.String(), Weight: "0.3"},
   472  			{Address: addr2.String(), Weight: "7"},
   473  			{Address: addr3.String(), Weight: "0.6"},
   474  		},
   475  	}
   476  	policy := group.NewThresholdDecisionPolicy(
   477  		"3",
   478  		votingPeriod,
   479  		minExecutionPeriod,
   480  	)
   481  	s.Require().NoError(groupMsg.SetDecisionPolicy(policy))
   482  
   483  	s.setNextAccount()
   484  	groupRes, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, groupMsg)
   485  	s.Require().NoError(err)
   486  	accountAddr := groupRes.GetGroupPolicyAddress()
   487  	groupPolicy, err := sdk.AccAddressFromBech32(accountAddr)
   488  	s.Require().NoError(err)
   489  	s.Require().NotNil(groupPolicy)
   490  
   491  	proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, &group.MsgSubmitProposal{
   492  		GroupPolicyAddress: accountAddr,
   493  		Proposers:          []string{addr1.String()},
   494  		Messages:           nil,
   495  	})
   496  	s.Require().NoError(err)
   497  
   498  	// group members vote
   499  	_, err = s.groupKeeper.Vote(s.ctx, &group.MsgVote{
   500  		ProposalId: proposalRes.ProposalId,
   501  		Voter:      addr1.String(),
   502  		Option:     group.VOTE_OPTION_NO,
   503  	})
   504  	s.Require().NoError(err)
   505  	_, err = s.groupKeeper.Vote(s.ctx, &group.MsgVote{
   506  		ProposalId: proposalRes.ProposalId,
   507  		Voter:      addr2.String(),
   508  		Option:     group.VOTE_OPTION_NO,
   509  	})
   510  	s.Require().NoError(err)
   511  
   512  	// move forward in time
   513  	ctx := s.sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(votingPeriod + 1))
   514  
   515  	// Tally the result. This saves the tally result to state.
   516  	s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctx))
   517  	s.NotPanics(func() { module.EndBlocker(ctx, s.groupKeeper) })
   518  
   519  	// member 2 (high weight) leaves group.
   520  	_, err = s.groupKeeper.LeaveGroup(ctx, &group.MsgLeaveGroup{
   521  		Address: addr2.String(),
   522  		GroupId: groupRes.GroupId,
   523  	})
   524  	s.Require().NoError(err)
   525  
   526  	s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctx))
   527  	s.NotPanics(func() { module.EndBlocker(ctx, s.groupKeeper) })
   528  }