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

     1  package keeper_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  
    11  	abci "github.com/cometbft/cometbft/abci/types"
    12  	"github.com/golang/mock/gomock"
    13  
    14  	"github.com/cosmos/cosmos-sdk/codec/address"
    15  	simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
    16  	"github.com/cosmos/cosmos-sdk/testutil/testdata"
    17  	sdk "github.com/cosmos/cosmos-sdk/types"
    18  	"github.com/cosmos/cosmos-sdk/types/query"
    19  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    20  	"github.com/cosmos/cosmos-sdk/x/group"
    21  	"github.com/cosmos/cosmos-sdk/x/group/internal/math"
    22  	"github.com/cosmos/cosmos-sdk/x/group/keeper"
    23  	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
    24  )
    25  
    26  var EventProposalPruned = "cosmos.group.v1.EventProposalPruned"
    27  
    28  func (s *TestSuite) TestCreateGroupWithLotsOfMembers() {
    29  	for i := 50; i < 70; i++ {
    30  		membersResp := s.createGroupAndGetMembers(i)
    31  		s.Require().Equal(len(membersResp), i)
    32  	}
    33  }
    34  
    35  func (s *TestSuite) createGroupAndGetMembers(numMembers int) []*group.GroupMember {
    36  	addressPool := simtestutil.CreateIncrementalAccounts(numMembers)
    37  	members := make([]group.MemberRequest, numMembers)
    38  	for i := 0; i < len(members); i++ {
    39  		members[i] = group.MemberRequest{
    40  			Address: addressPool[i].String(),
    41  			Weight:  "1",
    42  		}
    43  		s.accountKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes()
    44  	}
    45  
    46  	g, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
    47  		Admin:   members[0].Address,
    48  		Members: members,
    49  	})
    50  	s.Require().NoErrorf(err, "failed to create group with %d members", len(members))
    51  	s.T().Logf("group %d created with %d members", g.GroupId, len(members))
    52  
    53  	groupMemberResp, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: g.GroupId})
    54  	s.Require().NoError(err)
    55  
    56  	s.T().Logf("got %d members from group %d", len(groupMemberResp.Members), g.GroupId)
    57  
    58  	return groupMemberResp.Members
    59  }
    60  
    61  func (s *TestSuite) TestCreateGroup() {
    62  	addrs := s.addrs
    63  	addr1 := addrs[0]
    64  	addr3 := addrs[2]
    65  	addr5 := addrs[4]
    66  	addr6 := addrs[5]
    67  
    68  	members := []group.MemberRequest{{
    69  		Address: addr5.String(),
    70  		Weight:  "1",
    71  	}, {
    72  		Address: addr6.String(),
    73  		Weight:  "2",
    74  	}}
    75  
    76  	expGroups := []*group.GroupInfo{
    77  		{
    78  			Id:          s.groupID,
    79  			Version:     1,
    80  			Admin:       addr1.String(),
    81  			TotalWeight: "3",
    82  			CreatedAt:   s.blockTime,
    83  		},
    84  		{
    85  			Id:          2,
    86  			Version:     1,
    87  			Admin:       addr1.String(),
    88  			TotalWeight: "3",
    89  			CreatedAt:   s.blockTime,
    90  		},
    91  	}
    92  
    93  	specs := map[string]struct {
    94  		req       *group.MsgCreateGroup
    95  		expErr    bool
    96  		expErrMsg string
    97  		expGroups []*group.GroupInfo
    98  	}{
    99  		"all good": {
   100  			req: &group.MsgCreateGroup{
   101  				Admin:   addr1.String(),
   102  				Members: members,
   103  			},
   104  			expGroups: expGroups,
   105  		},
   106  		"group metadata too long": {
   107  			req: &group.MsgCreateGroup{
   108  				Admin:    addr1.String(),
   109  				Members:  members,
   110  				Metadata: strings.Repeat("a", 256),
   111  			},
   112  			expErr:    true,
   113  			expErrMsg: "group metadata: limit exceeded",
   114  		},
   115  		"invalid member address": {
   116  			req: &group.MsgCreateGroup{
   117  				Admin: addr1.String(),
   118  				Members: []group.MemberRequest{{
   119  					Address: "invalid",
   120  					Weight:  "1",
   121  				}},
   122  			},
   123  			expErr:    true,
   124  			expErrMsg: "member address invalid",
   125  		},
   126  		"member metadata too long": {
   127  			req: &group.MsgCreateGroup{
   128  				Admin: addr1.String(),
   129  				Members: []group.MemberRequest{{
   130  					Address:  addr3.String(),
   131  					Weight:   "1",
   132  					Metadata: strings.Repeat("a", 256),
   133  				}},
   134  			},
   135  			expErr:    true,
   136  			expErrMsg: "metadata: limit exceeded",
   137  		},
   138  		"zero member weight": {
   139  			req: &group.MsgCreateGroup{
   140  				Admin: addr1.String(),
   141  				Members: []group.MemberRequest{{
   142  					Address: addr3.String(),
   143  					Weight:  "0",
   144  				}},
   145  			},
   146  			expErr:    true,
   147  			expErrMsg: "expected a positive decimal",
   148  		},
   149  		"invalid member weight - Inf": {
   150  			req: &group.MsgCreateGroup{
   151  				Admin: addr1.String(),
   152  				Members: []group.MemberRequest{{
   153  					Address: addr3.String(),
   154  					Weight:  "inf",
   155  				}},
   156  			},
   157  			expErr:    true,
   158  			expErrMsg: "expected a finite decimal",
   159  		},
   160  		"invalid member weight - NaN": {
   161  			req: &group.MsgCreateGroup{
   162  				Admin: addr1.String(),
   163  				Members: []group.MemberRequest{{
   164  					Address: addr3.String(),
   165  					Weight:  "NaN",
   166  				}},
   167  			},
   168  			expErr:    true,
   169  			expErrMsg: "expected a finite decimal",
   170  		},
   171  	}
   172  
   173  	var seq uint32 = 1
   174  	for msg, spec := range specs {
   175  		spec := spec
   176  		s.Run(msg, func() {
   177  			blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime()
   178  			res, err := s.groupKeeper.CreateGroup(s.ctx, spec.req)
   179  			if spec.expErr {
   180  				s.Require().Error(err)
   181  				s.Require().Contains(err.Error(), spec.expErrMsg)
   182  				_, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: uint64(seq + 1)})
   183  				s.Require().Error(err)
   184  				return
   185  			}
   186  
   187  			s.Require().NoError(err)
   188  			id := res.GroupId
   189  
   190  			seq++
   191  			s.Assert().Equal(uint64(seq), id)
   192  
   193  			// then all data persisted
   194  			loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id})
   195  			s.Require().NoError(err)
   196  			s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin)
   197  			s.Assert().Equal(spec.req.Metadata, loadedGroupRes.Info.Metadata)
   198  			s.Assert().Equal(id, loadedGroupRes.Info.Id)
   199  			s.Assert().Equal(uint64(1), loadedGroupRes.Info.Version)
   200  
   201  			// and members are stored as well
   202  			membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id})
   203  			s.Require().NoError(err)
   204  			loadedMembers := membersRes.Members
   205  			s.Require().Equal(len(members), len(loadedMembers))
   206  			// we reorder members by address to be able to compare them
   207  			sort.Slice(members, func(i, j int) bool {
   208  				addri, err := sdk.AccAddressFromBech32(members[i].Address)
   209  				s.Require().NoError(err)
   210  				addrj, err := sdk.AccAddressFromBech32(members[j].Address)
   211  				s.Require().NoError(err)
   212  				return bytes.Compare(addri, addrj) < 0
   213  			})
   214  			for i := range loadedMembers {
   215  				s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata)
   216  				s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address)
   217  				s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight)
   218  				s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt)
   219  				s.Assert().Equal(id, loadedMembers[i].GroupId)
   220  			}
   221  
   222  			// query groups by admin
   223  			groupsRes, err := s.groupKeeper.GroupsByAdmin(s.ctx, &group.QueryGroupsByAdminRequest{Admin: addr1.String()})
   224  			s.Require().NoError(err)
   225  			loadedGroups := groupsRes.Groups
   226  			s.Require().Equal(len(spec.expGroups), len(loadedGroups))
   227  			for i := range loadedGroups {
   228  				s.Assert().Equal(spec.expGroups[i].Metadata, loadedGroups[i].Metadata)
   229  				s.Assert().Equal(spec.expGroups[i].Admin, loadedGroups[i].Admin)
   230  				s.Assert().Equal(spec.expGroups[i].TotalWeight, loadedGroups[i].TotalWeight)
   231  				s.Assert().Equal(spec.expGroups[i].Id, loadedGroups[i].Id)
   232  				s.Assert().Equal(spec.expGroups[i].Version, loadedGroups[i].Version)
   233  				s.Assert().Equal(spec.expGroups[i].CreatedAt, loadedGroups[i].CreatedAt)
   234  			}
   235  		})
   236  	}
   237  }
   238  
   239  func (s *TestSuite) TestUpdateGroupMembers() {
   240  	addrs := s.addrs
   241  	addr3 := addrs[2]
   242  	addr4 := addrs[3]
   243  	addr5 := addrs[4]
   244  	addr6 := addrs[5]
   245  
   246  	member1 := addr5.String()
   247  	member2 := addr6.String()
   248  	members := []group.MemberRequest{{
   249  		Address: member1,
   250  		Weight:  "1",
   251  	}}
   252  
   253  	myAdmin := addr4.String()
   254  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
   255  		Admin:   myAdmin,
   256  		Members: members,
   257  	})
   258  	s.Require().NoError(err)
   259  	groupID := groupRes.GroupId
   260  
   261  	specs := map[string]struct {
   262  		req        *group.MsgUpdateGroupMembers
   263  		expErr     bool
   264  		expErrMsg  string
   265  		expGroup   *group.GroupInfo
   266  		expMembers []*group.GroupMember
   267  	}{
   268  		"empty group id": {
   269  			req: &group.MsgUpdateGroupMembers{
   270  				GroupId: 0,
   271  				Admin:   myAdmin,
   272  				MemberUpdates: []group.MemberRequest{{
   273  					Address: member2,
   274  					Weight:  "2",
   275  				}},
   276  			},
   277  			expErr:    true,
   278  			expErrMsg: "value is empty",
   279  		},
   280  		"no new members": {
   281  			req: &group.MsgUpdateGroupMembers{
   282  				GroupId:       groupID,
   283  				Admin:         myAdmin,
   284  				MemberUpdates: []group.MemberRequest{},
   285  			},
   286  			expErr:    true,
   287  			expErrMsg: "value is empty",
   288  		},
   289  		"invalid member": {
   290  			req: &group.MsgUpdateGroupMembers{
   291  				GroupId: groupID,
   292  				Admin:   myAdmin,
   293  				MemberUpdates: []group.MemberRequest{
   294  					{},
   295  				},
   296  			},
   297  			expErr:    true,
   298  			expErrMsg: "empty address string is not allowed",
   299  		},
   300  		"invalid member metadata too long": {
   301  			req: &group.MsgUpdateGroupMembers{
   302  				GroupId: groupID,
   303  				Admin:   myAdmin,
   304  				MemberUpdates: []group.MemberRequest{
   305  					{
   306  						Address:  member2,
   307  						Weight:   "2",
   308  						Metadata: strings.Repeat("a", 256),
   309  					},
   310  				},
   311  			},
   312  			expErr:    true,
   313  			expErrMsg: "group member metadata: limit exceeded",
   314  		},
   315  		"add new member": {
   316  			req: &group.MsgUpdateGroupMembers{
   317  				GroupId: groupID,
   318  				Admin:   myAdmin,
   319  				MemberUpdates: []group.MemberRequest{{
   320  					Address: member2,
   321  					Weight:  "2",
   322  				}},
   323  			},
   324  			expGroup: &group.GroupInfo{
   325  				Id:          groupID,
   326  				Admin:       myAdmin,
   327  				TotalWeight: "3",
   328  				Version:     2,
   329  				CreatedAt:   s.blockTime,
   330  			},
   331  			expMembers: []*group.GroupMember{
   332  				{
   333  					Member: &group.Member{
   334  						Address: member2,
   335  						Weight:  "2",
   336  						AddedAt: s.sdkCtx.BlockTime(),
   337  					},
   338  					GroupId: groupID,
   339  				},
   340  				{
   341  					Member: &group.Member{
   342  						Address: member1,
   343  						Weight:  "1",
   344  						AddedAt: s.blockTime,
   345  					},
   346  					GroupId: groupID,
   347  				},
   348  			},
   349  		},
   350  		"update member": {
   351  			req: &group.MsgUpdateGroupMembers{
   352  				GroupId: groupID,
   353  				Admin:   myAdmin,
   354  				MemberUpdates: []group.MemberRequest{{
   355  					Address: member1,
   356  					Weight:  "2",
   357  				}},
   358  			},
   359  			expGroup: &group.GroupInfo{
   360  				Id:          groupID,
   361  				Admin:       myAdmin,
   362  				TotalWeight: "2",
   363  				Version:     2,
   364  				CreatedAt:   s.blockTime,
   365  			},
   366  			expMembers: []*group.GroupMember{
   367  				{
   368  					GroupId: groupID,
   369  					Member: &group.Member{
   370  						Address: member1,
   371  						Weight:  "2",
   372  						AddedAt: s.blockTime,
   373  					},
   374  				},
   375  			},
   376  		},
   377  		"update member with same data": {
   378  			req: &group.MsgUpdateGroupMembers{
   379  				GroupId: groupID,
   380  				Admin:   myAdmin,
   381  				MemberUpdates: []group.MemberRequest{{
   382  					Address: member1,
   383  					Weight:  "1",
   384  				}},
   385  			},
   386  			expGroup: &group.GroupInfo{
   387  				Id:          groupID,
   388  				Admin:       myAdmin,
   389  				TotalWeight: "1",
   390  				Version:     2,
   391  				CreatedAt:   s.blockTime,
   392  			},
   393  			expMembers: []*group.GroupMember{
   394  				{
   395  					GroupId: groupID,
   396  					Member: &group.Member{
   397  						Address: member1,
   398  						Weight:  "1",
   399  						AddedAt: s.blockTime,
   400  					},
   401  				},
   402  			},
   403  		},
   404  		"replace member": {
   405  			req: &group.MsgUpdateGroupMembers{
   406  				GroupId: groupID,
   407  				Admin:   myAdmin,
   408  				MemberUpdates: []group.MemberRequest{
   409  					{
   410  						Address: member1,
   411  						Weight:  "0",
   412  					},
   413  					{
   414  						Address: member2,
   415  						Weight:  "1",
   416  					},
   417  				},
   418  			},
   419  			expGroup: &group.GroupInfo{
   420  				Id:          groupID,
   421  				Admin:       myAdmin,
   422  				TotalWeight: "1",
   423  				Version:     2,
   424  				CreatedAt:   s.blockTime,
   425  			},
   426  			expMembers: []*group.GroupMember{{
   427  				GroupId: groupID,
   428  				Member: &group.Member{
   429  					Address: member2,
   430  					Weight:  "1",
   431  					AddedAt: s.sdkCtx.BlockTime(),
   432  				},
   433  			}},
   434  		},
   435  		"remove existing member": {
   436  			req: &group.MsgUpdateGroupMembers{
   437  				GroupId: groupID,
   438  				Admin:   myAdmin,
   439  				MemberUpdates: []group.MemberRequest{{
   440  					Address: member1,
   441  					Weight:  "0",
   442  				}},
   443  			},
   444  			expGroup: &group.GroupInfo{
   445  				Id:          groupID,
   446  				Admin:       myAdmin,
   447  				TotalWeight: "0",
   448  				Version:     2,
   449  				CreatedAt:   s.blockTime,
   450  			},
   451  			expMembers: []*group.GroupMember{},
   452  		},
   453  		"remove unknown member": {
   454  			req: &group.MsgUpdateGroupMembers{
   455  				GroupId: groupID,
   456  				Admin:   myAdmin,
   457  				MemberUpdates: []group.MemberRequest{{
   458  					Address: addr4.String(),
   459  					Weight:  "0",
   460  				}},
   461  			},
   462  			expErr: true,
   463  			expGroup: &group.GroupInfo{
   464  				Id:          groupID,
   465  				Admin:       myAdmin,
   466  				TotalWeight: "1",
   467  				Version:     1,
   468  				CreatedAt:   s.blockTime,
   469  			},
   470  			expMembers: []*group.GroupMember{{
   471  				GroupId: groupID,
   472  				Member: &group.Member{
   473  					Address: member1,
   474  					Weight:  "1",
   475  				},
   476  			}},
   477  		},
   478  		"with wrong admin": {
   479  			req: &group.MsgUpdateGroupMembers{
   480  				GroupId: groupID,
   481  				Admin:   addr3.String(),
   482  				MemberUpdates: []group.MemberRequest{{
   483  					Address: member1,
   484  					Weight:  "2",
   485  				}},
   486  			},
   487  			expErr:    true,
   488  			expErrMsg: "not group admin",
   489  			expGroup: &group.GroupInfo{
   490  				Id:          groupID,
   491  				Admin:       myAdmin,
   492  				TotalWeight: "1",
   493  				Version:     1,
   494  				CreatedAt:   s.blockTime,
   495  			},
   496  			expMembers: []*group.GroupMember{{
   497  				GroupId: groupID,
   498  				Member: &group.Member{
   499  					Address: member1,
   500  					Weight:  "1",
   501  				},
   502  			}},
   503  		},
   504  		"with unknown groupID": {
   505  			req: &group.MsgUpdateGroupMembers{
   506  				GroupId: 999,
   507  				Admin:   myAdmin,
   508  				MemberUpdates: []group.MemberRequest{{
   509  					Address: member1,
   510  					Weight:  "2",
   511  				}},
   512  			},
   513  			expErr:    true,
   514  			expErrMsg: "not found",
   515  			expGroup: &group.GroupInfo{
   516  				Id:          groupID,
   517  				Admin:       myAdmin,
   518  				TotalWeight: "1",
   519  				Version:     1,
   520  				CreatedAt:   s.blockTime,
   521  			},
   522  			expMembers: []*group.GroupMember{{
   523  				GroupId: groupID,
   524  				Member: &group.Member{
   525  					Address: member1,
   526  					Weight:  "1",
   527  				},
   528  			}},
   529  		},
   530  	}
   531  	for msg, spec := range specs {
   532  		spec := spec
   533  		s.Run(msg, func() {
   534  			sdkCtx, _ := s.sdkCtx.CacheContext()
   535  			_, err := s.groupKeeper.UpdateGroupMembers(sdkCtx, spec.req)
   536  			if spec.expErr {
   537  				s.Require().Error(err)
   538  				s.Require().Contains(err.Error(), spec.expErrMsg)
   539  				return
   540  			}
   541  			s.Require().NoError(err)
   542  
   543  			// then
   544  			res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID})
   545  			s.Require().NoError(err)
   546  			s.Assert().Equal(spec.expGroup, res.Info)
   547  
   548  			// and members persisted
   549  			membersRes, err := s.groupKeeper.GroupMembers(sdkCtx, &group.QueryGroupMembersRequest{GroupId: groupID})
   550  			s.Require().NoError(err)
   551  			loadedMembers := membersRes.Members
   552  			s.Require().Equal(len(spec.expMembers), len(loadedMembers))
   553  			// we reorder group members by address to be able to compare them
   554  			sort.Slice(spec.expMembers, func(i, j int) bool {
   555  				addri, err := sdk.AccAddressFromBech32(spec.expMembers[i].Member.Address)
   556  				s.Require().NoError(err)
   557  				addrj, err := sdk.AccAddressFromBech32(spec.expMembers[j].Member.Address)
   558  				s.Require().NoError(err)
   559  				return bytes.Compare(addri, addrj) < 0
   560  			})
   561  			for i := range loadedMembers {
   562  				s.Assert().Equal(spec.expMembers[i].Member.Metadata, loadedMembers[i].Member.Metadata)
   563  				s.Assert().Equal(spec.expMembers[i].Member.Address, loadedMembers[i].Member.Address)
   564  				s.Assert().Equal(spec.expMembers[i].Member.Weight, loadedMembers[i].Member.Weight)
   565  				s.Assert().Equal(spec.expMembers[i].Member.AddedAt, loadedMembers[i].Member.AddedAt)
   566  				s.Assert().Equal(spec.expMembers[i].GroupId, loadedMembers[i].GroupId)
   567  			}
   568  
   569  			events := sdkCtx.EventManager().ABCIEvents()
   570  			s.Require().Len(events, 1) // EventUpdateGroup
   571  		})
   572  	}
   573  }
   574  
   575  func (s *TestSuite) TestUpdateGroupAdmin() {
   576  	addrs := s.addrs
   577  	addr1 := addrs[0]
   578  	addr2 := addrs[1]
   579  	addr3 := addrs[2]
   580  	addr4 := addrs[3]
   581  
   582  	members := []group.MemberRequest{{
   583  		Address: addr1.String(),
   584  		Weight:  "1",
   585  	}}
   586  	oldAdmin := addr2.String()
   587  	newAdmin := addr3.String()
   588  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
   589  		Admin:   oldAdmin,
   590  		Members: members,
   591  	})
   592  	s.Require().NoError(err)
   593  	groupID := groupRes.GroupId
   594  	specs := map[string]struct {
   595  		req       *group.MsgUpdateGroupAdmin
   596  		expStored *group.GroupInfo
   597  		expErr    bool
   598  		expErrMsg string
   599  	}{
   600  		"with no groupID": {
   601  			req: &group.MsgUpdateGroupAdmin{
   602  				GroupId:  0,
   603  				Admin:    oldAdmin,
   604  				NewAdmin: newAdmin,
   605  			},
   606  			expErr:    true,
   607  			expErrMsg: "value is empty",
   608  		},
   609  		"with identical admin and new admin": {
   610  			req: &group.MsgUpdateGroupAdmin{
   611  				GroupId:  groupID,
   612  				Admin:    oldAdmin,
   613  				NewAdmin: oldAdmin,
   614  			},
   615  			expErr:    true,
   616  			expErrMsg: "new and old admin are the same",
   617  		},
   618  		"with correct admin": {
   619  			req: &group.MsgUpdateGroupAdmin{
   620  				GroupId:  groupID,
   621  				Admin:    oldAdmin,
   622  				NewAdmin: newAdmin,
   623  			},
   624  			expStored: &group.GroupInfo{
   625  				Id:          groupID,
   626  				Admin:       newAdmin,
   627  				TotalWeight: "1",
   628  				Version:     2,
   629  				CreatedAt:   s.blockTime,
   630  			},
   631  		},
   632  		"with wrong admin": {
   633  			req: &group.MsgUpdateGroupAdmin{
   634  				GroupId:  groupID,
   635  				Admin:    addr4.String(),
   636  				NewAdmin: newAdmin,
   637  			},
   638  			expErr:    true,
   639  			expErrMsg: "not group admin",
   640  			expStored: &group.GroupInfo{
   641  				Id:          groupID,
   642  				Admin:       oldAdmin,
   643  				TotalWeight: "1",
   644  				Version:     1,
   645  				CreatedAt:   s.blockTime,
   646  			},
   647  		},
   648  		"with unknown groupID": {
   649  			req: &group.MsgUpdateGroupAdmin{
   650  				GroupId:  999,
   651  				Admin:    oldAdmin,
   652  				NewAdmin: newAdmin,
   653  			},
   654  			expErr:    true,
   655  			expErrMsg: "not found",
   656  			expStored: &group.GroupInfo{
   657  				Id:          groupID,
   658  				Admin:       oldAdmin,
   659  				TotalWeight: "1",
   660  				Version:     1,
   661  				CreatedAt:   s.blockTime,
   662  			},
   663  		},
   664  		"with invalid new admin address": {
   665  			req: &group.MsgUpdateGroupAdmin{
   666  				GroupId:  groupID,
   667  				Admin:    oldAdmin,
   668  				NewAdmin: "%s",
   669  			},
   670  			expErr:    true,
   671  			expErrMsg: "new admin address",
   672  		},
   673  	}
   674  	for msg, spec := range specs {
   675  		spec := spec
   676  		s.Run(msg, func() {
   677  			_, err := s.groupKeeper.UpdateGroupAdmin(s.ctx, spec.req)
   678  			if spec.expErr {
   679  				s.Require().Error(err)
   680  				s.Require().Contains(err.Error(), spec.expErrMsg)
   681  				return
   682  			}
   683  			s.Require().NoError(err)
   684  
   685  			// then
   686  			res, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: groupID})
   687  			s.Require().NoError(err)
   688  			s.Assert().Equal(spec.expStored, res.Info)
   689  		})
   690  	}
   691  }
   692  
   693  func (s *TestSuite) TestUpdateGroupMetadata() {
   694  	addrs := s.addrs
   695  	addr1 := addrs[0]
   696  	addr3 := addrs[2]
   697  
   698  	oldAdmin := addr1.String()
   699  	groupID := s.groupID
   700  
   701  	specs := map[string]struct {
   702  		req       *group.MsgUpdateGroupMetadata
   703  		expErr    bool
   704  		expStored *group.GroupInfo
   705  	}{
   706  		"with correct admin": {
   707  			req: &group.MsgUpdateGroupMetadata{
   708  				GroupId: groupID,
   709  				Admin:   oldAdmin,
   710  			},
   711  			expStored: &group.GroupInfo{
   712  				Id:          groupID,
   713  				Admin:       oldAdmin,
   714  				TotalWeight: "3",
   715  				Version:     2,
   716  				CreatedAt:   s.blockTime,
   717  			},
   718  		},
   719  		"with wrong admin": {
   720  			req: &group.MsgUpdateGroupMetadata{
   721  				GroupId: groupID,
   722  				Admin:   addr3.String(),
   723  			},
   724  			expErr: true,
   725  			expStored: &group.GroupInfo{
   726  				Id:          groupID,
   727  				Admin:       oldAdmin,
   728  				TotalWeight: "1",
   729  				Version:     1,
   730  				CreatedAt:   s.blockTime,
   731  			},
   732  		},
   733  		"with unknown groupid": {
   734  			req: &group.MsgUpdateGroupMetadata{
   735  				GroupId: 999,
   736  				Admin:   oldAdmin,
   737  			},
   738  			expErr: true,
   739  			expStored: &group.GroupInfo{
   740  				Id:          groupID,
   741  				Admin:       oldAdmin,
   742  				TotalWeight: "1",
   743  				Version:     1,
   744  				CreatedAt:   s.blockTime,
   745  			},
   746  		},
   747  	}
   748  	for msg, spec := range specs {
   749  		spec := spec
   750  		s.Run(msg, func() {
   751  			sdkCtx, _ := s.sdkCtx.CacheContext()
   752  			_, err := s.groupKeeper.UpdateGroupMetadata(sdkCtx, spec.req)
   753  			if spec.expErr {
   754  				s.Require().Error(err)
   755  				return
   756  			}
   757  			s.Require().NoError(err)
   758  
   759  			// then
   760  			res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID})
   761  			s.Require().NoError(err)
   762  			s.Assert().Equal(spec.expStored, res.Info)
   763  
   764  			events := sdkCtx.EventManager().ABCIEvents()
   765  			s.Require().Len(events, 1) // EventUpdateGroup
   766  		})
   767  	}
   768  }
   769  
   770  func (s *TestSuite) TestCreateGroupWithPolicy() {
   771  	addrs := s.addrs
   772  	addr1 := addrs[0]
   773  	addr3 := addrs[2]
   774  	addr5 := addrs[4]
   775  	addr6 := addrs[5]
   776  
   777  	s.setNextAccount()
   778  
   779  	members := []group.MemberRequest{{
   780  		Address: addr5.String(),
   781  		Weight:  "1",
   782  	}, {
   783  		Address: addr6.String(),
   784  		Weight:  "2",
   785  	}}
   786  
   787  	specs := map[string]struct {
   788  		req       *group.MsgCreateGroupWithPolicy
   789  		policy    group.DecisionPolicy
   790  		malleate  func()
   791  		expErr    bool
   792  		expErrMsg string
   793  	}{
   794  		"all good": {
   795  			req: &group.MsgCreateGroupWithPolicy{
   796  				Admin:              addr1.String(),
   797  				Members:            members,
   798  				GroupPolicyAsAdmin: false,
   799  			},
   800  			malleate: func() {
   801  				s.setNextAccount()
   802  			},
   803  			policy: group.NewThresholdDecisionPolicy(
   804  				"1",
   805  				time.Second,
   806  				0,
   807  			),
   808  		},
   809  		"group policy as admin is true": {
   810  			req: &group.MsgCreateGroupWithPolicy{
   811  				Admin:              addr1.String(),
   812  				Members:            members,
   813  				GroupPolicyAsAdmin: true,
   814  			},
   815  			malleate: func() {
   816  				s.setNextAccount()
   817  			},
   818  			policy: group.NewThresholdDecisionPolicy(
   819  				"1",
   820  				time.Second,
   821  				0,
   822  			),
   823  		},
   824  		"group metadata too long": {
   825  			req: &group.MsgCreateGroupWithPolicy{
   826  				Admin:              addr1.String(),
   827  				Members:            members,
   828  				GroupPolicyAsAdmin: false,
   829  				GroupMetadata:      strings.Repeat("a", 256),
   830  			},
   831  			policy: group.NewThresholdDecisionPolicy(
   832  				"1",
   833  				time.Second,
   834  				0,
   835  			),
   836  			expErr:    true,
   837  			expErrMsg: "group metadata: limit exceeded",
   838  		},
   839  		"group policy metadata too long": {
   840  			req: &group.MsgCreateGroupWithPolicy{
   841  				Admin:               addr1.String(),
   842  				Members:             members,
   843  				GroupPolicyAsAdmin:  false,
   844  				GroupPolicyMetadata: strings.Repeat("a", 256),
   845  			},
   846  			policy: group.NewThresholdDecisionPolicy(
   847  				"1",
   848  				time.Second,
   849  				0,
   850  			),
   851  			expErr:    true,
   852  			expErrMsg: "group policy metadata: limit exceeded",
   853  		},
   854  		"member metadata too long": {
   855  			req: &group.MsgCreateGroupWithPolicy{
   856  				Admin: addr1.String(),
   857  				Members: []group.MemberRequest{{
   858  					Address:  addr3.String(),
   859  					Weight:   "1",
   860  					Metadata: strings.Repeat("a", 256),
   861  				}},
   862  				GroupPolicyAsAdmin: false,
   863  			},
   864  			policy: group.NewThresholdDecisionPolicy(
   865  				"1",
   866  				time.Second,
   867  				0,
   868  			),
   869  			expErr:    true,
   870  			expErrMsg: "member metadata: limit exceeded",
   871  		},
   872  		"zero member weight": {
   873  			req: &group.MsgCreateGroupWithPolicy{
   874  				Admin: addr1.String(),
   875  				Members: []group.MemberRequest{{
   876  					Address: addr3.String(),
   877  					Weight:  "0",
   878  				}},
   879  				GroupPolicyAsAdmin: false,
   880  			},
   881  			policy: group.NewThresholdDecisionPolicy(
   882  				"1",
   883  				time.Second,
   884  				0,
   885  			),
   886  			expErr:    true,
   887  			expErrMsg: "expected a positive decimal",
   888  		},
   889  		"invalid member address": {
   890  			req: &group.MsgCreateGroupWithPolicy{
   891  				Admin: addr1.String(),
   892  				Members: []group.MemberRequest{{
   893  					Address: "invalid",
   894  					Weight:  "1",
   895  				}},
   896  				GroupPolicyAsAdmin: false,
   897  			},
   898  			policy: group.NewThresholdDecisionPolicy(
   899  				"1",
   900  				time.Second,
   901  				0,
   902  			),
   903  			expErr:    true,
   904  			expErrMsg: "decoding bech32 failed",
   905  		},
   906  		"decision policy threshold > total group weight": {
   907  			req: &group.MsgCreateGroupWithPolicy{
   908  				Admin:              addr1.String(),
   909  				Members:            members,
   910  				GroupPolicyAsAdmin: false,
   911  			},
   912  			malleate: func() {
   913  				s.setNextAccount()
   914  			},
   915  			policy: group.NewThresholdDecisionPolicy(
   916  				"10",
   917  				time.Second,
   918  				0,
   919  			),
   920  			expErr: false,
   921  		},
   922  	}
   923  
   924  	for msg, spec := range specs {
   925  		spec := spec
   926  		s.Run(msg, func() {
   927  			s.setNextAccount()
   928  			err := spec.req.SetDecisionPolicy(spec.policy)
   929  			s.Require().NoError(err)
   930  
   931  			blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime()
   932  			res, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, spec.req)
   933  			if spec.expErr {
   934  				s.Require().Error(err)
   935  				s.Require().Contains(err.Error(), spec.expErrMsg)
   936  				return
   937  			}
   938  			s.Require().NoError(err)
   939  			id := res.GroupId
   940  			groupPolicyAddr := res.GroupPolicyAddress
   941  
   942  			// then all data persisted in group
   943  			loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id})
   944  			s.Require().NoError(err)
   945  			s.Assert().Equal(spec.req.GroupMetadata, loadedGroupRes.Info.Metadata)
   946  			s.Assert().Equal(id, loadedGroupRes.Info.Id)
   947  			if spec.req.GroupPolicyAsAdmin {
   948  				s.Assert().NotEqual(spec.req.Admin, loadedGroupRes.Info.Admin)
   949  				s.Assert().Equal(groupPolicyAddr, loadedGroupRes.Info.Admin)
   950  			} else {
   951  				s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin)
   952  			}
   953  
   954  			// and members are stored as well
   955  			membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id})
   956  			s.Require().NoError(err)
   957  			loadedMembers := membersRes.Members
   958  			s.Require().Equal(len(members), len(loadedMembers))
   959  			// we reorder members by address to be able to compare them
   960  			sort.Slice(members, func(i, j int) bool {
   961  				addri, err := sdk.AccAddressFromBech32(members[i].Address)
   962  				s.Require().NoError(err)
   963  				addrj, err := sdk.AccAddressFromBech32(members[j].Address)
   964  				s.Require().NoError(err)
   965  				return bytes.Compare(addri, addrj) < 0
   966  			})
   967  			for i := range loadedMembers {
   968  				s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata)
   969  				s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address)
   970  				s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight)
   971  				s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt)
   972  				s.Assert().Equal(id, loadedMembers[i].GroupId)
   973  			}
   974  
   975  			// then all data persisted in group policy
   976  			groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: groupPolicyAddr})
   977  			s.Require().NoError(err)
   978  
   979  			groupPolicy := groupPolicyRes.Info
   980  			s.Assert().Equal(groupPolicyAddr, groupPolicy.Address)
   981  			s.Assert().Equal(id, groupPolicy.GroupId)
   982  			s.Assert().Equal(spec.req.GroupPolicyMetadata, groupPolicy.Metadata)
   983  			dp, err := groupPolicy.GetDecisionPolicy()
   984  			s.Assert().NoError(err)
   985  			s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp)
   986  			if spec.req.GroupPolicyAsAdmin {
   987  				s.Assert().NotEqual(spec.req.Admin, groupPolicy.Admin)
   988  				s.Assert().Equal(groupPolicyAddr, groupPolicy.Admin)
   989  			} else {
   990  				s.Assert().Equal(spec.req.Admin, groupPolicy.Admin)
   991  			}
   992  		})
   993  	}
   994  }
   995  
   996  func (s *TestSuite) TestCreateGroupPolicy() {
   997  	addrs := s.addrs
   998  	addr1 := addrs[0]
   999  	addr4 := addrs[3]
  1000  
  1001  	s.setNextAccount()
  1002  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
  1003  		Admin:   addr1.String(),
  1004  		Members: nil,
  1005  	})
  1006  	s.Require().NoError(err)
  1007  	myGroupID := groupRes.GroupId
  1008  
  1009  	specs := map[string]struct {
  1010  		req       *group.MsgCreateGroupPolicy
  1011  		policy    group.DecisionPolicy
  1012  		expErr    bool
  1013  		expErrMsg string
  1014  	}{
  1015  		"all good": {
  1016  			req: &group.MsgCreateGroupPolicy{
  1017  				Admin:   addr1.String(),
  1018  				GroupId: myGroupID,
  1019  			},
  1020  			policy: group.NewThresholdDecisionPolicy(
  1021  				"1",
  1022  				time.Second,
  1023  				0,
  1024  			),
  1025  		},
  1026  		"all good with percentage decision policy": {
  1027  			req: &group.MsgCreateGroupPolicy{
  1028  				Admin:   addr1.String(),
  1029  				GroupId: myGroupID,
  1030  			},
  1031  			policy: group.NewPercentageDecisionPolicy(
  1032  				"0.5",
  1033  				time.Second,
  1034  				0,
  1035  			),
  1036  		},
  1037  		"decision policy threshold > total group weight": {
  1038  			req: &group.MsgCreateGroupPolicy{
  1039  				Admin:   addr1.String(),
  1040  				GroupId: myGroupID,
  1041  			},
  1042  			policy: group.NewThresholdDecisionPolicy(
  1043  				"10",
  1044  				time.Second,
  1045  				0,
  1046  			),
  1047  		},
  1048  		"group id does not exists": {
  1049  			req: &group.MsgCreateGroupPolicy{
  1050  				Admin:   addr1.String(),
  1051  				GroupId: 9999,
  1052  			},
  1053  			policy: group.NewThresholdDecisionPolicy(
  1054  				"1",
  1055  				time.Second,
  1056  				0,
  1057  			),
  1058  			expErr:    true,
  1059  			expErrMsg: "not found",
  1060  		},
  1061  		"admin not group admin": {
  1062  			req: &group.MsgCreateGroupPolicy{
  1063  				Admin:   addr4.String(),
  1064  				GroupId: myGroupID,
  1065  			},
  1066  			policy: group.NewThresholdDecisionPolicy(
  1067  				"1",
  1068  				time.Second,
  1069  				0,
  1070  			),
  1071  			expErr:    true,
  1072  			expErrMsg: "not group admin",
  1073  		},
  1074  		"metadata too long": {
  1075  			req: &group.MsgCreateGroupPolicy{
  1076  				Admin:    addr1.String(),
  1077  				GroupId:  myGroupID,
  1078  				Metadata: strings.Repeat("a", 256),
  1079  			},
  1080  			policy: group.NewThresholdDecisionPolicy(
  1081  				"1",
  1082  				time.Second,
  1083  				0,
  1084  			),
  1085  			expErr:    true,
  1086  			expErrMsg: "limit exceeded",
  1087  		},
  1088  		"percentage decision policy with negative value": {
  1089  			req: &group.MsgCreateGroupPolicy{
  1090  				Admin:   addr1.String(),
  1091  				GroupId: myGroupID,
  1092  			},
  1093  			policy: group.NewPercentageDecisionPolicy(
  1094  				"-0.5",
  1095  				time.Second,
  1096  				0,
  1097  			),
  1098  			expErr:    true,
  1099  			expErrMsg: "expected a positive decimal",
  1100  		},
  1101  		"percentage decision policy with value greater than 1": {
  1102  			req: &group.MsgCreateGroupPolicy{
  1103  				Admin:   addr1.String(),
  1104  				GroupId: myGroupID,
  1105  			},
  1106  			policy: group.NewPercentageDecisionPolicy(
  1107  				"2",
  1108  				time.Second,
  1109  				0,
  1110  			),
  1111  			expErr:    true,
  1112  			expErrMsg: "percentage must be > 0 and <= 1",
  1113  		},
  1114  	}
  1115  	for msg, spec := range specs {
  1116  		spec := spec
  1117  		s.Run(msg, func() {
  1118  			err := spec.req.SetDecisionPolicy(spec.policy)
  1119  			s.Require().NoError(err)
  1120  
  1121  			s.setNextAccount()
  1122  
  1123  			res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, spec.req)
  1124  			if spec.expErr {
  1125  				s.Require().Error(err)
  1126  				s.Require().Contains(err.Error(), spec.expErrMsg)
  1127  				return
  1128  			}
  1129  			s.Require().NoError(err)
  1130  			addr := res.Address
  1131  
  1132  			// then all data persisted
  1133  			groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: addr})
  1134  			s.Require().NoError(err)
  1135  
  1136  			groupPolicy := groupPolicyRes.Info
  1137  			s.Assert().Equal(addr, groupPolicy.Address)
  1138  			s.Assert().Equal(myGroupID, groupPolicy.GroupId)
  1139  			s.Assert().Equal(spec.req.Admin, groupPolicy.Admin)
  1140  			s.Assert().Equal(spec.req.Metadata, groupPolicy.Metadata)
  1141  			s.Assert().Equal(uint64(1), groupPolicy.Version)
  1142  			percentageDecisionPolicy, ok := spec.policy.(*group.PercentageDecisionPolicy)
  1143  			if ok {
  1144  				dp, err := groupPolicy.GetDecisionPolicy()
  1145  				s.Assert().NoError(err)
  1146  				s.Assert().Equal(percentageDecisionPolicy, dp)
  1147  			} else {
  1148  				dp, err := groupPolicy.GetDecisionPolicy()
  1149  				s.Assert().NoError(err)
  1150  				s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp)
  1151  			}
  1152  		})
  1153  	}
  1154  }
  1155  
  1156  func (s *TestSuite) TestUpdateGroupPolicyAdmin() {
  1157  	addrs := s.addrs
  1158  	addr1 := addrs[0]
  1159  	addr2 := addrs[1]
  1160  	addr5 := addrs[4]
  1161  
  1162  	admin, newAdmin := addr1, addr2
  1163  	policy := group.NewThresholdDecisionPolicy(
  1164  		"1",
  1165  		time.Second,
  1166  		0,
  1167  	)
  1168  	s.setNextAccount()
  1169  	groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy)
  1170  
  1171  	specs := map[string]struct {
  1172  		req            *group.MsgUpdateGroupPolicyAdmin
  1173  		expGroupPolicy *group.GroupPolicyInfo
  1174  		expErr         bool
  1175  		expErrMsg      string
  1176  	}{
  1177  		"with wrong admin": {
  1178  			req: &group.MsgUpdateGroupPolicyAdmin{
  1179  				Admin:              addr5.String(),
  1180  				GroupPolicyAddress: groupPolicyAddr,
  1181  				NewAdmin:           newAdmin.String(),
  1182  			},
  1183  			expGroupPolicy: &group.GroupPolicyInfo{
  1184  				Admin:          admin.String(),
  1185  				Address:        groupPolicyAddr,
  1186  				GroupId:        myGroupID,
  1187  				Version:        2,
  1188  				DecisionPolicy: nil,
  1189  				CreatedAt:      s.blockTime,
  1190  			},
  1191  			expErr:    true,
  1192  			expErrMsg: "not group policy admin: unauthorized",
  1193  		},
  1194  		"with wrong group policy": {
  1195  			req: &group.MsgUpdateGroupPolicyAdmin{
  1196  				Admin:              admin.String(),
  1197  				GroupPolicyAddress: addr5.String(),
  1198  				NewAdmin:           newAdmin.String(),
  1199  			},
  1200  			expGroupPolicy: &group.GroupPolicyInfo{
  1201  				Admin:          admin.String(),
  1202  				Address:        groupPolicyAddr,
  1203  				GroupId:        myGroupID,
  1204  				Version:        2,
  1205  				DecisionPolicy: nil,
  1206  				CreatedAt:      s.blockTime,
  1207  			},
  1208  			expErr:    true,
  1209  			expErrMsg: "load group policy: not found",
  1210  		},
  1211  		"correct data": {
  1212  			req: &group.MsgUpdateGroupPolicyAdmin{
  1213  				Admin:              admin.String(),
  1214  				GroupPolicyAddress: groupPolicyAddr,
  1215  				NewAdmin:           newAdmin.String(),
  1216  			},
  1217  			expGroupPolicy: &group.GroupPolicyInfo{
  1218  				Admin:          newAdmin.String(),
  1219  				Address:        groupPolicyAddr,
  1220  				GroupId:        myGroupID,
  1221  				Version:        2,
  1222  				DecisionPolicy: nil,
  1223  				CreatedAt:      s.blockTime,
  1224  			},
  1225  			expErr: false,
  1226  		},
  1227  		"with invalid new admin address": {
  1228  			req: &group.MsgUpdateGroupPolicyAdmin{
  1229  				Admin:              admin.String(),
  1230  				GroupPolicyAddress: groupPolicyAddr,
  1231  				NewAdmin:           "%s",
  1232  			},
  1233  			expGroupPolicy: &group.GroupPolicyInfo{
  1234  				Admin:          admin.String(),
  1235  				Address:        groupPolicyAddr,
  1236  				GroupId:        myGroupID,
  1237  				Version:        2,
  1238  				DecisionPolicy: nil,
  1239  				CreatedAt:      s.blockTime,
  1240  			},
  1241  			expErr:    true,
  1242  			expErrMsg: "new admin address",
  1243  		},
  1244  	}
  1245  	for msg, spec := range specs {
  1246  		spec := spec
  1247  		err := spec.expGroupPolicy.SetDecisionPolicy(policy)
  1248  		s.Require().NoError(err)
  1249  
  1250  		s.Run(msg, func() {
  1251  			_, err := s.groupKeeper.UpdateGroupPolicyAdmin(s.ctx, spec.req)
  1252  			if spec.expErr {
  1253  				s.Require().Error(err)
  1254  				s.Require().Contains(err.Error(), spec.expErrMsg)
  1255  				return
  1256  			}
  1257  			s.Require().NoError(err)
  1258  			res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{
  1259  				Address: groupPolicyAddr,
  1260  			})
  1261  			s.Require().NoError(err)
  1262  			s.Assert().Equal(spec.expGroupPolicy, res.Info)
  1263  		})
  1264  	}
  1265  }
  1266  
  1267  func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() {
  1268  	addrs := s.addrs
  1269  	addr1 := addrs[0]
  1270  	addr5 := addrs[4]
  1271  
  1272  	admin := addr1
  1273  	policy := group.NewThresholdDecisionPolicy(
  1274  		"1",
  1275  		time.Second,
  1276  		0,
  1277  	)
  1278  
  1279  	s.setNextAccount()
  1280  	groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy)
  1281  
  1282  	specs := map[string]struct {
  1283  		preRun         func(admin sdk.AccAddress) (policyAddr string, groupId uint64)
  1284  		req            *group.MsgUpdateGroupPolicyDecisionPolicy
  1285  		policy         group.DecisionPolicy
  1286  		expGroupPolicy *group.GroupPolicyInfo
  1287  		expErr         bool
  1288  		expErrMsg      string
  1289  	}{
  1290  		"with wrong admin": {
  1291  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1292  				Admin:              addr5.String(),
  1293  				GroupPolicyAddress: groupPolicyAddr,
  1294  			},
  1295  			policy:         policy,
  1296  			expGroupPolicy: &group.GroupPolicyInfo{},
  1297  			expErr:         true,
  1298  			expErrMsg:      "not group policy admin: unauthorized",
  1299  		},
  1300  		"with wrong group policy": {
  1301  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1302  				Admin:              admin.String(),
  1303  				GroupPolicyAddress: addr5.String(),
  1304  			},
  1305  			policy:         policy,
  1306  			expGroupPolicy: &group.GroupPolicyInfo{},
  1307  			expErr:         true,
  1308  			expErrMsg:      "load group policy: not found",
  1309  		},
  1310  		"invalid percentage decision policy with negative value": {
  1311  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1312  				Admin:              admin.String(),
  1313  				GroupPolicyAddress: groupPolicyAddr,
  1314  			},
  1315  			policy: group.NewPercentageDecisionPolicy(
  1316  				"-0.5",
  1317  				time.Duration(1)*time.Second,
  1318  				0,
  1319  			),
  1320  			expGroupPolicy: &group.GroupPolicyInfo{
  1321  				Admin:          admin.String(),
  1322  				Address:        groupPolicyAddr,
  1323  				GroupId:        myGroupID,
  1324  				Version:        2,
  1325  				DecisionPolicy: nil,
  1326  				CreatedAt:      s.blockTime,
  1327  			},
  1328  			expErr:    true,
  1329  			expErrMsg: "expected a positive decimal",
  1330  		},
  1331  		"invalid percentage decision policy with value greater than 1": {
  1332  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1333  				Admin:              admin.String(),
  1334  				GroupPolicyAddress: groupPolicyAddr,
  1335  			},
  1336  			policy: group.NewPercentageDecisionPolicy(
  1337  				"2",
  1338  				time.Duration(1)*time.Second,
  1339  				0,
  1340  			),
  1341  			expGroupPolicy: &group.GroupPolicyInfo{
  1342  				Admin:          admin.String(),
  1343  				Address:        groupPolicyAddr,
  1344  				GroupId:        myGroupID,
  1345  				Version:        2,
  1346  				DecisionPolicy: nil,
  1347  				CreatedAt:      s.blockTime,
  1348  			},
  1349  			expErr:    true,
  1350  			expErrMsg: "percentage must be > 0 and <= 1",
  1351  		},
  1352  		"correct data": {
  1353  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1354  				Admin:              admin.String(),
  1355  				GroupPolicyAddress: groupPolicyAddr,
  1356  			},
  1357  			policy: group.NewThresholdDecisionPolicy(
  1358  				"2",
  1359  				time.Duration(2)*time.Second,
  1360  				0,
  1361  			),
  1362  			expGroupPolicy: &group.GroupPolicyInfo{
  1363  				Admin:          admin.String(),
  1364  				Address:        groupPolicyAddr,
  1365  				GroupId:        myGroupID,
  1366  				Version:        2,
  1367  				DecisionPolicy: nil,
  1368  				CreatedAt:      s.blockTime,
  1369  			},
  1370  			expErr: false,
  1371  		},
  1372  		"correct data with percentage decision policy": {
  1373  			preRun: func(admin sdk.AccAddress) (string, uint64) {
  1374  				s.setNextAccount()
  1375  				return s.createGroupAndGroupPolicy(admin, nil, policy)
  1376  			},
  1377  			req: &group.MsgUpdateGroupPolicyDecisionPolicy{
  1378  				Admin:              admin.String(),
  1379  				GroupPolicyAddress: groupPolicyAddr,
  1380  			},
  1381  			policy: group.NewPercentageDecisionPolicy(
  1382  				"0.5",
  1383  				time.Duration(2)*time.Second,
  1384  				0,
  1385  			),
  1386  			expGroupPolicy: &group.GroupPolicyInfo{
  1387  				Admin:          admin.String(),
  1388  				DecisionPolicy: nil,
  1389  				Version:        2,
  1390  				CreatedAt:      s.blockTime,
  1391  			},
  1392  			expErr: false,
  1393  		},
  1394  	}
  1395  	for msg, spec := range specs {
  1396  		spec := spec
  1397  		policyAddr := groupPolicyAddr
  1398  		err := spec.expGroupPolicy.SetDecisionPolicy(spec.policy)
  1399  		s.Require().NoError(err)
  1400  		if spec.preRun != nil {
  1401  			policyAddr1, groupID := spec.preRun(admin)
  1402  			policyAddr = policyAddr1
  1403  
  1404  			// update the expected info with new group policy details
  1405  			spec.expGroupPolicy.Address = policyAddr1
  1406  			spec.expGroupPolicy.GroupId = groupID
  1407  
  1408  			// update req with new group policy addr
  1409  			spec.req.GroupPolicyAddress = policyAddr1
  1410  		}
  1411  
  1412  		err = spec.req.SetDecisionPolicy(spec.policy)
  1413  		s.Require().NoError(err)
  1414  
  1415  		s.Run(msg, func() {
  1416  			_, err := s.groupKeeper.UpdateGroupPolicyDecisionPolicy(s.ctx, spec.req)
  1417  			if spec.expErr {
  1418  				s.Require().Error(err)
  1419  				s.Require().Contains(err.Error(), spec.expErrMsg)
  1420  				return
  1421  			}
  1422  			s.Require().NoError(err)
  1423  			res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{
  1424  				Address: policyAddr,
  1425  			})
  1426  			s.Require().NoError(err)
  1427  			s.Assert().Equal(spec.expGroupPolicy, res.Info)
  1428  		})
  1429  	}
  1430  }
  1431  
  1432  func (s *TestSuite) TestUpdateGroupPolicyMetadata() {
  1433  	addrs := s.addrs
  1434  	addr1 := addrs[0]
  1435  	addr5 := addrs[4]
  1436  
  1437  	admin := addr1
  1438  	policy := group.NewThresholdDecisionPolicy(
  1439  		"1",
  1440  		time.Second,
  1441  		0,
  1442  	)
  1443  
  1444  	s.setNextAccount()
  1445  	groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy)
  1446  
  1447  	specs := map[string]struct {
  1448  		req            *group.MsgUpdateGroupPolicyMetadata
  1449  		expGroupPolicy *group.GroupPolicyInfo
  1450  		expErr         bool
  1451  		expErrMsg      string
  1452  	}{
  1453  		"with wrong admin": {
  1454  			req: &group.MsgUpdateGroupPolicyMetadata{
  1455  				Admin:              addr5.String(),
  1456  				GroupPolicyAddress: groupPolicyAddr,
  1457  			},
  1458  			expGroupPolicy: &group.GroupPolicyInfo{},
  1459  			expErr:         true,
  1460  			expErrMsg:      "not group policy admin: unauthorized",
  1461  		},
  1462  		"with wrong group policy": {
  1463  			req: &group.MsgUpdateGroupPolicyMetadata{
  1464  				Admin:              admin.String(),
  1465  				GroupPolicyAddress: addr5.String(),
  1466  			},
  1467  			expGroupPolicy: &group.GroupPolicyInfo{},
  1468  			expErr:         true,
  1469  			expErrMsg:      "load group policy: not found",
  1470  		},
  1471  		"with metadata too long": {
  1472  			req: &group.MsgUpdateGroupPolicyMetadata{
  1473  				Admin:              admin.String(),
  1474  				GroupPolicyAddress: groupPolicyAddr,
  1475  				Metadata:           strings.Repeat("a", 1001),
  1476  			},
  1477  			expGroupPolicy: &group.GroupPolicyInfo{},
  1478  			expErr:         true,
  1479  			expErrMsg:      "group policy metadata: limit exceeded",
  1480  		},
  1481  		"correct data": {
  1482  			req: &group.MsgUpdateGroupPolicyMetadata{
  1483  				Admin:              admin.String(),
  1484  				GroupPolicyAddress: groupPolicyAddr,
  1485  			},
  1486  			expGroupPolicy: &group.GroupPolicyInfo{
  1487  				Admin:          admin.String(),
  1488  				Address:        groupPolicyAddr,
  1489  				GroupId:        myGroupID,
  1490  				Version:        2,
  1491  				DecisionPolicy: nil,
  1492  				CreatedAt:      s.blockTime,
  1493  			},
  1494  			expErr: false,
  1495  		},
  1496  	}
  1497  	for msg, spec := range specs {
  1498  		spec := spec
  1499  		err := spec.expGroupPolicy.SetDecisionPolicy(policy)
  1500  		s.Require().NoError(err)
  1501  
  1502  		s.Run(msg, func() {
  1503  			_, err := s.groupKeeper.UpdateGroupPolicyMetadata(s.ctx, spec.req)
  1504  			if spec.expErr {
  1505  				s.Require().Error(err)
  1506  				s.Require().Contains(err.Error(), spec.expErrMsg)
  1507  				return
  1508  			}
  1509  			s.Require().NoError(err)
  1510  
  1511  			res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{
  1512  				Address: groupPolicyAddr,
  1513  			})
  1514  			s.Require().NoError(err)
  1515  			s.Assert().Equal(spec.expGroupPolicy, res.Info)
  1516  
  1517  			// check events
  1518  			var hasUpdateGroupPolicyEvent bool
  1519  			events := s.ctx.(sdk.Context).EventManager().ABCIEvents()
  1520  			for _, event := range events {
  1521  				event, err := sdk.ParseTypedEvent(event)
  1522  				s.Require().NoError(err)
  1523  
  1524  				if e, ok := event.(*group.EventUpdateGroupPolicy); ok {
  1525  					s.Require().Equal(e.Address, groupPolicyAddr)
  1526  					hasUpdateGroupPolicyEvent = true
  1527  					break
  1528  				}
  1529  			}
  1530  
  1531  			s.Require().True(hasUpdateGroupPolicyEvent)
  1532  		})
  1533  	}
  1534  }
  1535  
  1536  func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() {
  1537  	addrs := s.addrs
  1538  	addr2 := addrs[1]
  1539  
  1540  	admin := addr2
  1541  
  1542  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
  1543  		Admin:   admin.String(),
  1544  		Members: nil,
  1545  	})
  1546  	s.Require().NoError(err)
  1547  	myGroupID := groupRes.GroupId
  1548  
  1549  	policies := []group.DecisionPolicy{
  1550  		group.NewThresholdDecisionPolicy(
  1551  			"1",
  1552  			time.Second,
  1553  			0,
  1554  		),
  1555  		group.NewThresholdDecisionPolicy(
  1556  			"10",
  1557  			time.Second,
  1558  			0,
  1559  		),
  1560  		group.NewPercentageDecisionPolicy(
  1561  			"0.5",
  1562  			time.Second,
  1563  			0,
  1564  		),
  1565  	}
  1566  
  1567  	count := 3
  1568  	expectAccs := make([]*group.GroupPolicyInfo, count)
  1569  	for i := range expectAccs {
  1570  		req := &group.MsgCreateGroupPolicy{
  1571  			Admin:   admin.String(),
  1572  			GroupId: myGroupID,
  1573  		}
  1574  		err := req.SetDecisionPolicy(policies[i])
  1575  		s.Require().NoError(err)
  1576  
  1577  		s.setNextAccount()
  1578  		res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, req)
  1579  		s.Require().NoError(err)
  1580  
  1581  		expectAcc := &group.GroupPolicyInfo{
  1582  			Address:   res.Address,
  1583  			Admin:     admin.String(),
  1584  			GroupId:   myGroupID,
  1585  			Version:   uint64(1),
  1586  			CreatedAt: s.blockTime,
  1587  		}
  1588  		err = expectAcc.SetDecisionPolicy(policies[i])
  1589  		s.Require().NoError(err)
  1590  		expectAccs[i] = expectAcc
  1591  	}
  1592  	sort.Slice(expectAccs, func(i, j int) bool { return expectAccs[i].Address < expectAccs[j].Address })
  1593  
  1594  	// query group policy by group
  1595  	policiesByGroupRes, err := s.groupKeeper.GroupPoliciesByGroup(s.ctx, &group.QueryGroupPoliciesByGroupRequest{
  1596  		GroupId: myGroupID,
  1597  	})
  1598  	s.Require().NoError(err)
  1599  	policyAccs := policiesByGroupRes.GroupPolicies
  1600  	s.Require().Equal(len(policyAccs), count)
  1601  	// we reorder policyAccs by address to be able to compare them
  1602  	sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address })
  1603  	for i := range policyAccs {
  1604  		s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address)
  1605  		s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId)
  1606  		s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin)
  1607  		s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata)
  1608  		s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version)
  1609  		s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt)
  1610  		dp1, err := policyAccs[i].GetDecisionPolicy()
  1611  		s.Assert().NoError(err)
  1612  		dp2, err := expectAccs[i].GetDecisionPolicy()
  1613  		s.Assert().NoError(err)
  1614  		s.Assert().Equal(dp1, dp2)
  1615  	}
  1616  
  1617  	// no group policy
  1618  	noPolicies, err := s.groupKeeper.GroupPoliciesByAdmin(s.ctx, &group.QueryGroupPoliciesByAdminRequest{
  1619  		Admin: addrs[2].String(),
  1620  	})
  1621  	s.Require().NoError(err)
  1622  	policyAccs = noPolicies.GroupPolicies
  1623  	s.Require().Equal(len(policyAccs), 0)
  1624  
  1625  	// query group policy by admin
  1626  	policiesByAdminRes, err := s.groupKeeper.GroupPoliciesByAdmin(s.ctx, &group.QueryGroupPoliciesByAdminRequest{
  1627  		Admin: admin.String(),
  1628  	})
  1629  	s.Require().NoError(err)
  1630  	policyAccs = policiesByAdminRes.GroupPolicies
  1631  	s.Require().Equal(len(policyAccs), count)
  1632  	// we reorder policyAccs by address to be able to compare them
  1633  	sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address })
  1634  	for i := range policyAccs {
  1635  		s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address)
  1636  		s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId)
  1637  		s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin)
  1638  		s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata)
  1639  		s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version)
  1640  		s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt)
  1641  		dp1, err := policyAccs[i].GetDecisionPolicy()
  1642  		s.Assert().NoError(err)
  1643  		dp2, err := expectAccs[i].GetDecisionPolicy()
  1644  		s.Assert().NoError(err)
  1645  		s.Assert().Equal(dp1, dp2)
  1646  	}
  1647  }
  1648  
  1649  func (s *TestSuite) TestSubmitProposal() {
  1650  	addrs := s.addrs
  1651  	addr1 := addrs[0]
  1652  	addr2 := addrs[1] // Has weight 2
  1653  	addr4 := addrs[3]
  1654  	addr5 := addrs[4] // Has weight 1
  1655  
  1656  	myGroupID := s.groupID
  1657  	accountAddr := s.groupPolicyAddr
  1658  
  1659  	// Create a new group policy to test TRY_EXEC
  1660  	policyReq := &group.MsgCreateGroupPolicy{
  1661  		Admin:   addr1.String(),
  1662  		GroupId: myGroupID,
  1663  	}
  1664  	noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy(
  1665  		"2",
  1666  		time.Second,
  1667  		0, // no MinExecutionPeriod to test TRY_EXEC
  1668  	)
  1669  	err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy)
  1670  	s.Require().NoError(err)
  1671  	s.setNextAccount()
  1672  	res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
  1673  	s.Require().NoError(err)
  1674  
  1675  	noMinExecPeriodPolicyAddr, err := s.accountKeeper.AddressCodec().StringToBytes(res.Address)
  1676  	s.Require().NoError(err)
  1677  
  1678  	// Create a new group policy with super high threshold
  1679  	bigThresholdPolicy := group.NewThresholdDecisionPolicy(
  1680  		"100",
  1681  		time.Second,
  1682  		minExecutionPeriod,
  1683  	)
  1684  	s.setNextAccount()
  1685  	err = policyReq.SetDecisionPolicy(bigThresholdPolicy)
  1686  	s.Require().NoError(err)
  1687  	bigThresholdRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
  1688  	s.Require().NoError(err)
  1689  	bigThresholdAddr := bigThresholdRes.Address
  1690  
  1691  	msgSend := &banktypes.MsgSend{
  1692  		FromAddress: res.Address,
  1693  		ToAddress:   addr2.String(),
  1694  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  1695  	}
  1696  	defaultProposal := group.Proposal{
  1697  		GroupPolicyAddress: accountAddr.String(),
  1698  		Status:             group.PROPOSAL_STATUS_SUBMITTED,
  1699  		FinalTallyResult: group.TallyResult{
  1700  			YesCount:        "0",
  1701  			NoCount:         "0",
  1702  			AbstainCount:    "0",
  1703  			NoWithVetoCount: "0",
  1704  		},
  1705  		ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  1706  	}
  1707  	specs := map[string]struct {
  1708  		req         *group.MsgSubmitProposal
  1709  		msgs        []sdk.Msg
  1710  		expProposal group.Proposal
  1711  		expErr      bool
  1712  		expErrMsg   string
  1713  		postRun     func(sdkCtx sdk.Context)
  1714  		preRun      func(msg []sdk.Msg)
  1715  	}{
  1716  		"all good with minimal fields set": {
  1717  			req: &group.MsgSubmitProposal{
  1718  				GroupPolicyAddress: accountAddr.String(),
  1719  				Proposers:          []string{addr2.String()},
  1720  			},
  1721  			expProposal: defaultProposal,
  1722  			postRun:     func(sdkCtx sdk.Context) {},
  1723  		},
  1724  		"all good with good msg payload": {
  1725  			req: &group.MsgSubmitProposal{
  1726  				GroupPolicyAddress: accountAddr.String(),
  1727  				Proposers:          []string{addr2.String()},
  1728  			},
  1729  			msgs: []sdk.Msg{&banktypes.MsgSend{
  1730  				FromAddress: accountAddr.String(),
  1731  				ToAddress:   addr2.String(),
  1732  				Amount:      sdk.Coins{sdk.NewInt64Coin("token", 100)},
  1733  			}},
  1734  			expProposal: defaultProposal,
  1735  			postRun:     func(sdkCtx sdk.Context) {},
  1736  		},
  1737  		"title != metadata.title": {
  1738  			req: &group.MsgSubmitProposal{
  1739  				GroupPolicyAddress: accountAddr.String(),
  1740  				Proposers:          []string{addr2.String()},
  1741  				Metadata:           "{\"title\":\"title\",\"summary\":\"description\"}",
  1742  				Title:              "title2",
  1743  				Summary:            "description",
  1744  			},
  1745  			expErr:    true,
  1746  			expErrMsg: "metadata title 'title' must equal proposal title 'title2'",
  1747  			postRun:   func(sdkCtx sdk.Context) {},
  1748  		},
  1749  		"summary != metadata.summary": {
  1750  			req: &group.MsgSubmitProposal{
  1751  				GroupPolicyAddress: accountAddr.String(),
  1752  				Proposers:          []string{addr2.String()},
  1753  				Metadata:           "{\"title\":\"title\",\"summary\":\"description of proposal\"}",
  1754  				Title:              "title",
  1755  				Summary:            "description",
  1756  			},
  1757  			expErr:    true,
  1758  			expErrMsg: "metadata summary 'description of proposal' must equal proposal summary 'description'",
  1759  			postRun:   func(sdkCtx sdk.Context) {},
  1760  		},
  1761  		"metadata too long": {
  1762  			req: &group.MsgSubmitProposal{
  1763  				GroupPolicyAddress: accountAddr.String(),
  1764  				Proposers:          []string{addr2.String()},
  1765  				Metadata:           strings.Repeat("a", 256),
  1766  			},
  1767  			expErr:    true,
  1768  			expErrMsg: "limit exceeded",
  1769  			postRun:   func(sdkCtx sdk.Context) {},
  1770  		},
  1771  		"summary too long": {
  1772  			req: &group.MsgSubmitProposal{
  1773  				GroupPolicyAddress: accountAddr.String(),
  1774  				Proposers:          []string{addr2.String()},
  1775  				Metadata:           "{\"title\":\"title\",\"summary\":\"description\"}",
  1776  				Summary:            strings.Repeat("a", 256*40),
  1777  			},
  1778  			expErr:    true,
  1779  			expErrMsg: "limit exceeded",
  1780  			postRun:   func(sdkCtx sdk.Context) {},
  1781  		},
  1782  		"group policy required": {
  1783  			req: &group.MsgSubmitProposal{
  1784  				Proposers: []string{addr2.String()},
  1785  			},
  1786  			expErr:    true,
  1787  			expErrMsg: "empty address string is not allowed",
  1788  			postRun:   func(sdkCtx sdk.Context) {},
  1789  		},
  1790  		"existing group policy required": {
  1791  			req: &group.MsgSubmitProposal{
  1792  				GroupPolicyAddress: addr1.String(),
  1793  				Proposers:          []string{addr2.String()},
  1794  			},
  1795  			expErr:    true,
  1796  			expErrMsg: "not found",
  1797  			postRun:   func(sdkCtx sdk.Context) {},
  1798  		},
  1799  		"decision policy threshold > total group weight": {
  1800  			req: &group.MsgSubmitProposal{
  1801  				GroupPolicyAddress: bigThresholdAddr,
  1802  				Proposers:          []string{addr2.String()},
  1803  			},
  1804  			expErr: false,
  1805  			expProposal: group.Proposal{
  1806  				GroupPolicyAddress: bigThresholdAddr,
  1807  				Status:             group.PROPOSAL_STATUS_SUBMITTED,
  1808  				FinalTallyResult:   group.DefaultTallyResult(),
  1809  				ExecutorResult:     group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  1810  			},
  1811  			postRun: func(sdkCtx sdk.Context) {},
  1812  		},
  1813  		"only group members can create a proposal": {
  1814  			req: &group.MsgSubmitProposal{
  1815  				GroupPolicyAddress: accountAddr.String(),
  1816  				Proposers:          []string{addr4.String()},
  1817  			},
  1818  			expErr:    true,
  1819  			expErrMsg: "not in group",
  1820  			postRun:   func(sdkCtx sdk.Context) {},
  1821  		},
  1822  		"all proposers must be in group": {
  1823  			req: &group.MsgSubmitProposal{
  1824  				GroupPolicyAddress: accountAddr.String(),
  1825  				Proposers:          []string{addr2.String(), addr4.String()},
  1826  			},
  1827  			expErr:    true,
  1828  			expErrMsg: "not in group",
  1829  			postRun:   func(sdkCtx sdk.Context) {},
  1830  		},
  1831  		"admin that is not a group member can not create proposal": {
  1832  			req: &group.MsgSubmitProposal{
  1833  				GroupPolicyAddress: accountAddr.String(),
  1834  				Proposers:          []string{addr1.String()},
  1835  			},
  1836  			expErr:    true,
  1837  			expErrMsg: "not in group",
  1838  			postRun:   func(sdkCtx sdk.Context) {},
  1839  		},
  1840  		"reject msgs that are not authz by group policy": {
  1841  			req: &group.MsgSubmitProposal{
  1842  				GroupPolicyAddress: accountAddr.String(),
  1843  				Proposers:          []string{addr2.String()},
  1844  			},
  1845  			msgs:      []sdk.Msg{&testdata.TestMsg{Signers: []string{addr1.String()}}},
  1846  			expErr:    true,
  1847  			expErrMsg: "msg does not have group policy authorization",
  1848  			postRun:   func(sdkCtx sdk.Context) {},
  1849  		},
  1850  		"with try exec": {
  1851  			preRun: func(msgs []sdk.Msg) {
  1852  				for i := 0; i < len(msgs); i++ {
  1853  					s.bankKeeper.EXPECT().Send(gomock.Any(), msgs[i]).Return(nil, nil)
  1854  				}
  1855  			},
  1856  			req: &group.MsgSubmitProposal{
  1857  				GroupPolicyAddress: res.Address,
  1858  				Proposers:          []string{addr2.String()},
  1859  				Exec:               group.Exec_EXEC_TRY,
  1860  			},
  1861  			msgs: []sdk.Msg{msgSend},
  1862  			expProposal: group.Proposal{
  1863  				GroupPolicyAddress: res.Address,
  1864  				Status:             group.PROPOSAL_STATUS_ACCEPTED,
  1865  				FinalTallyResult: group.TallyResult{
  1866  					YesCount:        "2",
  1867  					NoCount:         "0",
  1868  					AbstainCount:    "0",
  1869  					NoWithVetoCount: "0",
  1870  				},
  1871  				ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  1872  			},
  1873  			postRun: func(sdkCtx sdk.Context) {
  1874  				s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900)))
  1875  				s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100)))
  1876  
  1877  				fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr)
  1878  				s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900))
  1879  				toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2)
  1880  				s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100))
  1881  				events := sdkCtx.EventManager().ABCIEvents()
  1882  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  1883  			},
  1884  		},
  1885  		"with try exec, not enough yes votes for proposal to pass": {
  1886  			req: &group.MsgSubmitProposal{
  1887  				GroupPolicyAddress: res.Address,
  1888  				Proposers:          []string{addr5.String()},
  1889  				Exec:               group.Exec_EXEC_TRY,
  1890  			},
  1891  			msgs: []sdk.Msg{msgSend},
  1892  			expProposal: group.Proposal{
  1893  				GroupPolicyAddress: res.Address,
  1894  				Status:             group.PROPOSAL_STATUS_SUBMITTED,
  1895  				FinalTallyResult: group.TallyResult{
  1896  					YesCount:        "0", // Since tally doesn't pass Allow(), we consider the proposal not final
  1897  					NoCount:         "0",
  1898  					AbstainCount:    "0",
  1899  					NoWithVetoCount: "0",
  1900  				},
  1901  				ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  1902  			},
  1903  			postRun: func(sdkCtx sdk.Context) {},
  1904  		},
  1905  	}
  1906  	for msg, spec := range specs {
  1907  		spec := spec
  1908  		s.Run(msg, func() {
  1909  			err := spec.req.SetMsgs(spec.msgs)
  1910  			s.Require().NoError(err)
  1911  
  1912  			if spec.preRun != nil {
  1913  				spec.preRun(spec.msgs)
  1914  			}
  1915  
  1916  			res, err := s.groupKeeper.SubmitProposal(s.ctx, spec.req)
  1917  			if spec.expErr {
  1918  				s.Require().Error(err)
  1919  				s.Require().Contains(err.Error(), spec.expErrMsg)
  1920  				return
  1921  			}
  1922  			s.Require().NoError(err)
  1923  			id := res.ProposalId
  1924  
  1925  			if !(spec.expProposal.ExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) {
  1926  				// then all data persisted
  1927  				proposalRes, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: id})
  1928  				s.Require().NoError(err)
  1929  				proposal := proposalRes.Proposal
  1930  
  1931  				s.Assert().Equal(spec.expProposal.GroupPolicyAddress, proposal.GroupPolicyAddress)
  1932  				s.Assert().Equal(spec.req.Metadata, proposal.Metadata)
  1933  				s.Assert().Equal(spec.req.Proposers, proposal.Proposers)
  1934  				s.Assert().Equal(s.blockTime, proposal.SubmitTime)
  1935  				s.Assert().Equal(uint64(1), proposal.GroupVersion)
  1936  				s.Assert().Equal(uint64(1), proposal.GroupPolicyVersion)
  1937  				s.Assert().Equal(spec.expProposal.Status, proposal.Status)
  1938  				s.Assert().Equal(spec.expProposal.FinalTallyResult, proposal.FinalTallyResult)
  1939  				s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult)
  1940  				s.Assert().Equal(s.blockTime.Add(time.Second), proposal.VotingPeriodEnd)
  1941  
  1942  				msgs, err := proposal.GetMsgs()
  1943  				s.Assert().NoError(err)
  1944  				if spec.msgs == nil { // then empty list is ok
  1945  					s.Assert().Len(msgs, 0)
  1946  				} else {
  1947  					s.Assert().Equal(spec.msgs, msgs)
  1948  				}
  1949  			}
  1950  
  1951  			spec.postRun(s.sdkCtx)
  1952  		})
  1953  	}
  1954  }
  1955  
  1956  func (s *TestSuite) TestWithdrawProposal() {
  1957  	addrs := s.addrs
  1958  	addr2 := addrs[1]
  1959  	addr5 := addrs[4]
  1960  
  1961  	msgSend := &banktypes.MsgSend{
  1962  		FromAddress: s.groupPolicyAddr.String(),
  1963  		ToAddress:   addr2.String(),
  1964  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  1965  	}
  1966  
  1967  	proposers := []string{addr2.String()}
  1968  	proposalID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
  1969  
  1970  	specs := map[string]struct {
  1971  		preRun     func(sdkCtx sdk.Context) uint64
  1972  		proposalID uint64
  1973  		admin      string
  1974  		expErrMsg  string
  1975  		postRun    func(sdkCtx sdk.Context)
  1976  	}{
  1977  		"wrong admin": {
  1978  			preRun: func(sdkCtx sdk.Context) uint64 {
  1979  				return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
  1980  			},
  1981  			admin:     addr5.String(),
  1982  			expErrMsg: "unauthorized",
  1983  			postRun:   func(sdkCtx sdk.Context) {},
  1984  		},
  1985  		"wrong proposal id": {
  1986  			preRun: func(sdkCtx sdk.Context) uint64 {
  1987  				return 1111
  1988  			},
  1989  			admin:     proposers[0],
  1990  			expErrMsg: "not found",
  1991  			postRun:   func(sdkCtx sdk.Context) {},
  1992  		},
  1993  		"happy case with proposer": {
  1994  			preRun: func(sdkCtx sdk.Context) uint64 {
  1995  				return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
  1996  			},
  1997  			proposalID: proposalID,
  1998  			admin:      proposers[0],
  1999  			postRun:    func(sdkCtx sdk.Context) {},
  2000  		},
  2001  		"already closed proposal": {
  2002  			preRun: func(sdkCtx sdk.Context) uint64 {
  2003  				pID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
  2004  				_, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{
  2005  					ProposalId: pID,
  2006  					Address:    proposers[0],
  2007  				})
  2008  				s.Require().NoError(err)
  2009  				return pID
  2010  			},
  2011  			proposalID: proposalID,
  2012  			admin:      proposers[0],
  2013  			expErrMsg:  "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN",
  2014  			postRun:    func(sdkCtx sdk.Context) {},
  2015  		},
  2016  		"happy case with group admin address": {
  2017  			preRun: func(sdkCtx sdk.Context) uint64 {
  2018  				return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers)
  2019  			},
  2020  			proposalID: proposalID,
  2021  			admin:      proposers[0],
  2022  			postRun: func(sdkCtx sdk.Context) {
  2023  				resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: proposalID})
  2024  				s.Require().NoError(err)
  2025  				vpe := resp.Proposal.VotingPeriodEnd
  2026  				timeDiff := vpe.Sub(s.sdkCtx.BlockTime())
  2027  				ctxVPE := sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(timeDiff).Add(time.Second * 1))
  2028  				s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctxVPE))
  2029  				events := ctxVPE.EventManager().ABCIEvents()
  2030  
  2031  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  2032  			},
  2033  		},
  2034  	}
  2035  	for msg, spec := range specs {
  2036  		spec := spec
  2037  		s.Run(msg, func() {
  2038  			pID := spec.preRun(s.sdkCtx)
  2039  
  2040  			_, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{
  2041  				ProposalId: pID,
  2042  				Address:    spec.admin,
  2043  			})
  2044  
  2045  			if spec.expErrMsg != "" {
  2046  				s.Require().Error(err)
  2047  				s.Require().Contains(err.Error(), spec.expErrMsg)
  2048  				return
  2049  			}
  2050  
  2051  			s.Require().NoError(err)
  2052  			resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: pID})
  2053  			s.Require().NoError(err)
  2054  			s.Require().Equal(resp.GetProposal().Status, group.PROPOSAL_STATUS_WITHDRAWN)
  2055  		})
  2056  		spec.postRun(s.sdkCtx)
  2057  	}
  2058  }
  2059  
  2060  func (s *TestSuite) TestVote() {
  2061  	addrs := s.addrs
  2062  	addr1 := addrs[0]
  2063  	addr2 := addrs[1]
  2064  	addr3 := addrs[2]
  2065  	addr4 := addrs[3]
  2066  	addr5 := addrs[4]
  2067  	members := []group.MemberRequest{
  2068  		{Address: addr4.String(), Weight: "1"},
  2069  		{Address: addr3.String(), Weight: "2"},
  2070  	}
  2071  
  2072  	groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
  2073  		Admin:   addr1.String(),
  2074  		Members: members,
  2075  	})
  2076  	s.Require().NoError(err)
  2077  	myGroupID := groupRes.GroupId
  2078  
  2079  	policy := group.NewThresholdDecisionPolicy(
  2080  		"2",
  2081  		time.Duration(2),
  2082  		0,
  2083  	)
  2084  	policyReq := &group.MsgCreateGroupPolicy{
  2085  		Admin:   addr1.String(),
  2086  		GroupId: myGroupID,
  2087  	}
  2088  	err = policyReq.SetDecisionPolicy(policy)
  2089  	s.Require().NoError(err)
  2090  
  2091  	s.setNextAccount()
  2092  	policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
  2093  	s.Require().NoError(err)
  2094  	accountAddr := policyRes.Address
  2095  	// module account will be created and returned
  2096  	groupPolicy, err := s.accountKeeper.AddressCodec().StringToBytes(accountAddr)
  2097  	s.Require().NoError(err)
  2098  	s.Require().NotNil(groupPolicy)
  2099  
  2100  	s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)}).Return(nil).AnyTimes()
  2101  	s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
  2102  
  2103  	req := &group.MsgSubmitProposal{
  2104  		GroupPolicyAddress: accountAddr,
  2105  		Proposers:          []string{addr4.String()},
  2106  		Messages:           nil,
  2107  	}
  2108  	msg := &banktypes.MsgSend{
  2109  		FromAddress: accountAddr,
  2110  		ToAddress:   addr5.String(),
  2111  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  2112  	}
  2113  	err = req.SetMsgs([]sdk.Msg{msg})
  2114  	s.Require().NoError(err)
  2115  
  2116  	proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, req)
  2117  	s.Require().NoError(err)
  2118  	myProposalID := proposalRes.ProposalId
  2119  
  2120  	// no group policy
  2121  	proposalsRes, err := s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{
  2122  		Address: addrs[2].String(),
  2123  	})
  2124  	s.Require().NoError(err)
  2125  	proposals := proposalsRes.Proposals
  2126  	s.Require().Equal(len(proposals), 0)
  2127  
  2128  	// proposals by group policy (request with pagination)
  2129  	proposalsRes, err = s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{
  2130  		Address: accountAddr,
  2131  		Pagination: &query.PageRequest{
  2132  			Limit: 2,
  2133  		},
  2134  	})
  2135  	s.Require().NoError(err)
  2136  	proposals = proposalsRes.Proposals
  2137  	s.Require().Equal(len(proposals), 1)
  2138  
  2139  	// proposals by group policy
  2140  	proposalsRes, err = s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{
  2141  		Address: accountAddr,
  2142  	})
  2143  	s.Require().NoError(err)
  2144  	proposals = proposalsRes.Proposals
  2145  	s.Require().Equal(len(proposals), 1)
  2146  	s.Assert().Equal(req.GroupPolicyAddress, proposals[0].GroupPolicyAddress)
  2147  	s.Assert().Equal(req.Metadata, proposals[0].Metadata)
  2148  	s.Assert().Equal(req.Proposers, proposals[0].Proposers)
  2149  	s.Assert().Equal(s.blockTime, proposals[0].SubmitTime)
  2150  	s.Assert().Equal(uint64(1), proposals[0].GroupVersion)
  2151  	s.Assert().Equal(uint64(1), proposals[0].GroupPolicyVersion)
  2152  	s.Assert().Equal(group.PROPOSAL_STATUS_SUBMITTED, proposals[0].Status)
  2153  	s.Assert().Equal(group.DefaultTallyResult(), proposals[0].FinalTallyResult)
  2154  
  2155  	specs := map[string]struct {
  2156  		srcCtx            sdk.Context
  2157  		expTallyResult    group.TallyResult // expected after tallying
  2158  		isFinal           bool              // is the tally result final?
  2159  		req               *group.MsgVote
  2160  		doBefore          func(ctx context.Context)
  2161  		postRun           func(sdkCtx sdk.Context)
  2162  		expProposalStatus group.ProposalStatus         // expected after tallying
  2163  		expExecutorResult group.ProposalExecutorResult // expected after tallying
  2164  		expErr            bool
  2165  		expErrMsg         string
  2166  	}{
  2167  		"vote yes": {
  2168  			req: &group.MsgVote{
  2169  				ProposalId: myProposalID,
  2170  				Voter:      addr4.String(),
  2171  				Option:     group.VOTE_OPTION_YES,
  2172  			},
  2173  			expTallyResult: group.TallyResult{
  2174  				YesCount:        "1",
  2175  				NoCount:         "0",
  2176  				AbstainCount:    "0",
  2177  				NoWithVetoCount: "0",
  2178  			},
  2179  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2180  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2181  			postRun:           func(sdkCtx sdk.Context) {},
  2182  		},
  2183  		"with try exec": {
  2184  			req: &group.MsgVote{
  2185  				ProposalId: myProposalID,
  2186  				Voter:      addr3.String(),
  2187  				Option:     group.VOTE_OPTION_YES,
  2188  				Exec:       group.Exec_EXEC_TRY,
  2189  			},
  2190  			expTallyResult: group.TallyResult{
  2191  				YesCount:        "2",
  2192  				NoCount:         "0",
  2193  				AbstainCount:    "0",
  2194  				NoWithVetoCount: "0",
  2195  			},
  2196  			isFinal:           true,
  2197  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2198  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2199  			doBefore: func(ctx context.Context) {
  2200  				s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil)
  2201  			},
  2202  			postRun: func(sdkCtx sdk.Context) {
  2203  				s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), groupPolicy).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900)))
  2204  				s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), addr5).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100)))
  2205  
  2206  				fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, groupPolicy)
  2207  				s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900))
  2208  				toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr5)
  2209  				s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100))
  2210  			},
  2211  		},
  2212  		"with try exec, not enough yes votes for proposal to pass": {
  2213  			req: &group.MsgVote{
  2214  				ProposalId: myProposalID,
  2215  				Voter:      addr4.String(),
  2216  				Option:     group.VOTE_OPTION_YES,
  2217  				Exec:       group.Exec_EXEC_TRY,
  2218  			},
  2219  			expTallyResult: group.TallyResult{
  2220  				YesCount:        "1",
  2221  				NoCount:         "0",
  2222  				AbstainCount:    "0",
  2223  				NoWithVetoCount: "0",
  2224  			},
  2225  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2226  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2227  			postRun:           func(sdkCtx sdk.Context) {},
  2228  		},
  2229  		"vote no": {
  2230  			req: &group.MsgVote{
  2231  				ProposalId: myProposalID,
  2232  				Voter:      addr4.String(),
  2233  				Option:     group.VOTE_OPTION_NO,
  2234  			},
  2235  			expTallyResult: group.TallyResult{
  2236  				YesCount:        "0",
  2237  				NoCount:         "1",
  2238  				AbstainCount:    "0",
  2239  				NoWithVetoCount: "0",
  2240  			},
  2241  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2242  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2243  			postRun:           func(sdkCtx sdk.Context) {},
  2244  		},
  2245  		"vote abstain": {
  2246  			req: &group.MsgVote{
  2247  				ProposalId: myProposalID,
  2248  				Voter:      addr4.String(),
  2249  				Option:     group.VOTE_OPTION_ABSTAIN,
  2250  			},
  2251  			expTallyResult: group.TallyResult{
  2252  				YesCount:        "0",
  2253  				NoCount:         "0",
  2254  				AbstainCount:    "1",
  2255  				NoWithVetoCount: "0",
  2256  			},
  2257  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2258  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2259  			postRun:           func(sdkCtx sdk.Context) {},
  2260  		},
  2261  		"vote veto": {
  2262  			req: &group.MsgVote{
  2263  				ProposalId: myProposalID,
  2264  				Voter:      addr4.String(),
  2265  				Option:     group.VOTE_OPTION_NO_WITH_VETO,
  2266  			},
  2267  			expTallyResult: group.TallyResult{
  2268  				YesCount:        "0",
  2269  				NoCount:         "0",
  2270  				AbstainCount:    "0",
  2271  				NoWithVetoCount: "1",
  2272  			},
  2273  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2274  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2275  			postRun:           func(sdkCtx sdk.Context) {},
  2276  		},
  2277  		"apply decision policy early": {
  2278  			req: &group.MsgVote{
  2279  				ProposalId: myProposalID,
  2280  				Voter:      addr3.String(),
  2281  				Option:     group.VOTE_OPTION_YES,
  2282  			},
  2283  			expTallyResult: group.TallyResult{
  2284  				YesCount:        "2",
  2285  				NoCount:         "0",
  2286  				AbstainCount:    "0",
  2287  				NoWithVetoCount: "0",
  2288  			},
  2289  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2290  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2291  			postRun:           func(sdkCtx sdk.Context) {},
  2292  		},
  2293  		"reject new votes when final decision is made already": {
  2294  			req: &group.MsgVote{
  2295  				ProposalId: myProposalID,
  2296  				Voter:      addr4.String(),
  2297  				Option:     group.VOTE_OPTION_YES,
  2298  			},
  2299  			doBefore: func(ctx context.Context) {
  2300  				_, err := s.groupKeeper.Vote(ctx, &group.MsgVote{
  2301  					ProposalId: myProposalID,
  2302  					Voter:      addr3.String(),
  2303  					Option:     group.VOTE_OPTION_NO_WITH_VETO,
  2304  					Exec:       1, // Execute the proposal so that its status is final
  2305  				})
  2306  				s.Require().NoError(err)
  2307  			},
  2308  			expErr:    true,
  2309  			expErrMsg: "proposal not open for voting",
  2310  			postRun:   func(sdkCtx sdk.Context) {},
  2311  		},
  2312  		"metadata too long": {
  2313  			req: &group.MsgVote{
  2314  				ProposalId: myProposalID,
  2315  				Voter:      addr4.String(),
  2316  				Option:     group.VOTE_OPTION_NO,
  2317  				Metadata:   strings.Repeat("a", 256),
  2318  			},
  2319  			expErr:    true,
  2320  			expErrMsg: "metadata: limit exceeded",
  2321  			postRun:   func(sdkCtx sdk.Context) {},
  2322  		},
  2323  		"existing proposal required": {
  2324  			req: &group.MsgVote{
  2325  				ProposalId: 999,
  2326  				Voter:      addr4.String(),
  2327  				Option:     group.VOTE_OPTION_NO,
  2328  			},
  2329  			expErr:    true,
  2330  			expErrMsg: "load proposal: not found",
  2331  			postRun:   func(sdkCtx sdk.Context) {},
  2332  		},
  2333  		"empty vote option": {
  2334  			req: &group.MsgVote{
  2335  				ProposalId: myProposalID,
  2336  				Voter:      addr4.String(),
  2337  			},
  2338  			expErr:    true,
  2339  			expErrMsg: "vote option: value is empty",
  2340  			postRun:   func(sdkCtx sdk.Context) {},
  2341  		},
  2342  		"invalid vote option": {
  2343  			req: &group.MsgVote{
  2344  				ProposalId: myProposalID,
  2345  				Voter:      addr4.String(),
  2346  				Option:     5,
  2347  			},
  2348  			expErr:    true,
  2349  			expErrMsg: "ote option: invalid value",
  2350  			postRun:   func(sdkCtx sdk.Context) {},
  2351  		},
  2352  		"voter must be in group": {
  2353  			req: &group.MsgVote{
  2354  				ProposalId: myProposalID,
  2355  				Voter:      addr2.String(),
  2356  				Option:     group.VOTE_OPTION_NO,
  2357  			},
  2358  			expErr:    true,
  2359  			expErrMsg: "not found",
  2360  			postRun:   func(sdkCtx sdk.Context) {},
  2361  		},
  2362  		"admin that is not a group member can not vote": {
  2363  			req: &group.MsgVote{
  2364  				ProposalId: myProposalID,
  2365  				Voter:      addr1.String(),
  2366  				Option:     group.VOTE_OPTION_NO,
  2367  			},
  2368  			expErr:    true,
  2369  			expErrMsg: "not found",
  2370  			postRun:   func(sdkCtx sdk.Context) {},
  2371  		},
  2372  		"on voting period end": {
  2373  			req: &group.MsgVote{
  2374  				ProposalId: myProposalID,
  2375  				Voter:      addr4.String(),
  2376  				Option:     group.VOTE_OPTION_NO,
  2377  			},
  2378  			srcCtx:    s.sdkCtx.WithBlockTime(s.blockTime.Add(time.Second)),
  2379  			expErr:    true,
  2380  			expErrMsg: "voting period has ended already: expired",
  2381  			postRun:   func(sdkCtx sdk.Context) {},
  2382  		},
  2383  		"vote closed already": {
  2384  			req: &group.MsgVote{
  2385  				ProposalId: myProposalID,
  2386  				Voter:      addr4.String(),
  2387  				Option:     group.VOTE_OPTION_NO,
  2388  			},
  2389  			doBefore: func(ctx context.Context) {
  2390  				s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil)
  2391  
  2392  				_, err := s.groupKeeper.Vote(ctx, &group.MsgVote{
  2393  					ProposalId: myProposalID,
  2394  					Voter:      addr3.String(),
  2395  					Option:     group.VOTE_OPTION_YES,
  2396  					Exec:       1, // Execute to close the proposal.
  2397  				})
  2398  				s.Require().NoError(err)
  2399  			},
  2400  			expErr:    true,
  2401  			expErrMsg: "load proposal: not found",
  2402  			postRun:   func(sdkCtx sdk.Context) {},
  2403  		},
  2404  		"voted already": {
  2405  			req: &group.MsgVote{
  2406  				ProposalId: myProposalID,
  2407  				Voter:      addr4.String(),
  2408  				Option:     group.VOTE_OPTION_NO,
  2409  			},
  2410  			doBefore: func(ctx context.Context) {
  2411  				_, err := s.groupKeeper.Vote(ctx, &group.MsgVote{
  2412  					ProposalId: myProposalID,
  2413  					Voter:      addr4.String(),
  2414  					Option:     group.VOTE_OPTION_YES,
  2415  				})
  2416  				s.Require().NoError(err)
  2417  			},
  2418  			expErr:    true,
  2419  			expErrMsg: "store vote: unique constraint violation",
  2420  			postRun:   func(sdkCtx sdk.Context) {},
  2421  		},
  2422  	}
  2423  	for msg, spec := range specs {
  2424  		spec := spec
  2425  		s.Run(msg, func() {
  2426  			sdkCtx := s.sdkCtx
  2427  			if !spec.srcCtx.IsZero() {
  2428  				sdkCtx = spec.srcCtx
  2429  			}
  2430  			sdkCtx, _ = sdkCtx.CacheContext()
  2431  			if spec.doBefore != nil {
  2432  				spec.doBefore(sdkCtx)
  2433  			}
  2434  			_, err := s.groupKeeper.Vote(sdkCtx, spec.req)
  2435  			if spec.expErr {
  2436  				s.Require().Error(err)
  2437  				s.Require().Contains(err.Error(), spec.expErrMsg)
  2438  				return
  2439  			}
  2440  			s.Require().NoError(err)
  2441  
  2442  			if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) {
  2443  				// vote is stored and all data persisted
  2444  				res, err := s.groupKeeper.VoteByProposalVoter(sdkCtx, &group.QueryVoteByProposalVoterRequest{
  2445  					ProposalId: spec.req.ProposalId,
  2446  					Voter:      spec.req.Voter,
  2447  				})
  2448  				s.Require().NoError(err)
  2449  				loaded := res.Vote
  2450  				s.Assert().Equal(spec.req.ProposalId, loaded.ProposalId)
  2451  				s.Assert().Equal(spec.req.Voter, loaded.Voter)
  2452  				s.Assert().Equal(spec.req.Option, loaded.Option)
  2453  				s.Assert().Equal(spec.req.Metadata, loaded.Metadata)
  2454  				s.Assert().Equal(s.blockTime, loaded.SubmitTime)
  2455  
  2456  				// query votes by proposal
  2457  				votesByProposalRes, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{
  2458  					ProposalId: spec.req.ProposalId,
  2459  				})
  2460  				s.Require().NoError(err)
  2461  				votesByProposal := votesByProposalRes.Votes
  2462  				s.Require().Equal(1, len(votesByProposal))
  2463  				vote := votesByProposal[0]
  2464  				s.Assert().Equal(spec.req.ProposalId, vote.ProposalId)
  2465  				s.Assert().Equal(spec.req.Voter, vote.Voter)
  2466  				s.Assert().Equal(spec.req.Option, vote.Option)
  2467  				s.Assert().Equal(spec.req.Metadata, vote.Metadata)
  2468  				s.Assert().Equal(s.blockTime, vote.SubmitTime)
  2469  
  2470  				// query votes by voter
  2471  				voter := spec.req.Voter
  2472  				votesByVoterRes, err := s.groupKeeper.VotesByVoter(sdkCtx, &group.QueryVotesByVoterRequest{
  2473  					Voter: voter,
  2474  				})
  2475  				s.Require().NoError(err)
  2476  				votesByVoter := votesByVoterRes.Votes
  2477  				s.Require().Equal(1, len(votesByVoter))
  2478  				s.Assert().Equal(spec.req.ProposalId, votesByVoter[0].ProposalId)
  2479  				s.Assert().Equal(voter, votesByVoter[0].Voter)
  2480  				s.Assert().Equal(spec.req.Option, votesByVoter[0].Option)
  2481  				s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata)
  2482  				s.Assert().Equal(s.blockTime, votesByVoter[0].SubmitTime)
  2483  
  2484  				proposalRes, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{
  2485  					ProposalId: spec.req.ProposalId,
  2486  				})
  2487  				s.Require().NoError(err)
  2488  
  2489  				proposal := proposalRes.Proposal
  2490  				if spec.isFinal {
  2491  					s.Assert().Equal(spec.expTallyResult, proposal.FinalTallyResult)
  2492  					s.Assert().Equal(spec.expProposalStatus, proposal.Status)
  2493  					s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult)
  2494  				} else {
  2495  					s.Assert().Equal(group.DefaultTallyResult(), proposal.FinalTallyResult) // Make sure proposal isn't mutated.
  2496  
  2497  					// do a round of tallying
  2498  					tallyResult, err := s.groupKeeper.Tally(sdkCtx, *proposal, myGroupID)
  2499  					s.Require().NoError(err)
  2500  
  2501  					s.Assert().Equal(spec.expTallyResult, tallyResult)
  2502  				}
  2503  			}
  2504  
  2505  			spec.postRun(sdkCtx)
  2506  		})
  2507  	}
  2508  
  2509  	s.T().Log("test tally result should not take into account the member who left the group")
  2510  	members = []group.MemberRequest{
  2511  		{Address: addr2.String(), Weight: "3"},
  2512  		{Address: addr3.String(), Weight: "2"},
  2513  		{Address: addr4.String(), Weight: "1"},
  2514  	}
  2515  	reqCreate := &group.MsgCreateGroupWithPolicy{
  2516  		Admin:         addr1.String(),
  2517  		Members:       members,
  2518  		GroupMetadata: "metadata",
  2519  	}
  2520  
  2521  	policy = group.NewThresholdDecisionPolicy(
  2522  		"4",
  2523  		time.Duration(10),
  2524  		0,
  2525  	)
  2526  	s.Require().NoError(reqCreate.SetDecisionPolicy(policy))
  2527  	s.setNextAccount()
  2528  
  2529  	result, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, reqCreate)
  2530  	s.Require().NoError(err)
  2531  	s.Require().NotNil(result)
  2532  
  2533  	policyAddr := result.GroupPolicyAddress
  2534  	groupID := result.GroupId
  2535  	reqProposal := &group.MsgSubmitProposal{
  2536  		GroupPolicyAddress: policyAddr,
  2537  		Proposers:          []string{addr4.String()},
  2538  	}
  2539  	s.Require().NoError(reqProposal.SetMsgs([]sdk.Msg{&banktypes.MsgSend{
  2540  		FromAddress: policyAddr,
  2541  		ToAddress:   addr5.String(),
  2542  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  2543  	}}))
  2544  
  2545  	resSubmitProposal, err := s.groupKeeper.SubmitProposal(s.ctx, reqProposal)
  2546  	s.Require().NoError(err)
  2547  	s.Require().NotNil(resSubmitProposal)
  2548  	proposalID := resSubmitProposal.ProposalId
  2549  
  2550  	for _, voter := range []string{addr4.String(), addr3.String(), addr2.String()} {
  2551  		_, err := s.groupKeeper.Vote(s.ctx,
  2552  			&group.MsgVote{ProposalId: proposalID, Voter: voter, Option: group.VOTE_OPTION_YES},
  2553  		)
  2554  		s.Require().NoError(err)
  2555  	}
  2556  
  2557  	qProposals, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{
  2558  		ProposalId: proposalID,
  2559  	})
  2560  	s.Require().NoError(err)
  2561  
  2562  	tallyResult, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID)
  2563  	s.Require().NoError(err)
  2564  
  2565  	_, err = s.groupKeeper.LeaveGroup(s.ctx, &group.MsgLeaveGroup{Address: addr4.String(), GroupId: groupID})
  2566  	s.Require().NoError(err)
  2567  
  2568  	tallyResult1, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID)
  2569  	s.Require().NoError(err)
  2570  	s.Require().NotEqual(tallyResult.String(), tallyResult1.String())
  2571  }
  2572  
  2573  func (s *TestSuite) TestExecProposal() {
  2574  	addrs := s.addrs
  2575  	addr1 := addrs[0]
  2576  	addr2 := addrs[1]
  2577  
  2578  	msgSend1 := &banktypes.MsgSend{
  2579  		FromAddress: s.groupPolicyAddr.String(),
  2580  		ToAddress:   addr2.String(),
  2581  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  2582  	}
  2583  	msgSend2 := &banktypes.MsgSend{
  2584  		FromAddress: s.groupPolicyAddr.String(),
  2585  		ToAddress:   addr2.String(),
  2586  		Amount:      sdk.Coins{sdk.NewInt64Coin("test", 10001)},
  2587  	}
  2588  	proposers := []string{addr2.String()}
  2589  
  2590  	specs := map[string]struct {
  2591  		srcBlockTime      time.Time
  2592  		setupProposal     func(ctx context.Context) uint64
  2593  		expErr            bool
  2594  		expErrMsg         string
  2595  		expProposalStatus group.ProposalStatus
  2596  		expExecutorResult group.ProposalExecutorResult
  2597  		expBalance        bool
  2598  		expFromBalances   sdk.Coin
  2599  		expToBalances     sdk.Coin
  2600  		postRun           func(sdkCtx sdk.Context)
  2601  	}{
  2602  		"proposal executed when accepted": {
  2603  			setupProposal: func(ctx context.Context) uint64 {
  2604  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil)
  2605  				msgs := []sdk.Msg{msgSend1}
  2606  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2607  			},
  2608  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2609  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2610  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2611  			expBalance:        true,
  2612  			expFromBalances:   sdk.NewInt64Coin("test", 9900),
  2613  			expToBalances:     sdk.NewInt64Coin("test", 100),
  2614  			postRun: func(sdkCtx sdk.Context) {
  2615  				events := sdkCtx.EventManager().ABCIEvents()
  2616  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  2617  			},
  2618  		},
  2619  		"proposal with multiple messages executed when accepted": {
  2620  			setupProposal: func(ctx context.Context) uint64 {
  2621  				msgs := []sdk.Msg{msgSend1, msgSend1}
  2622  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2)
  2623  
  2624  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2625  			},
  2626  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2627  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2628  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2629  			expBalance:        true,
  2630  			expFromBalances:   sdk.NewInt64Coin("test", 9800),
  2631  			expToBalances:     sdk.NewInt64Coin("test", 200),
  2632  			postRun: func(sdkCtx sdk.Context) {
  2633  				events := sdkCtx.EventManager().ABCIEvents()
  2634  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  2635  			},
  2636  		},
  2637  		"proposal not executed when rejected": {
  2638  			setupProposal: func(ctx context.Context) uint64 {
  2639  				msgs := []sdk.Msg{msgSend1}
  2640  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO)
  2641  			},
  2642  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2643  			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
  2644  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2645  			postRun: func(sdkCtx sdk.Context) {
  2646  				events := sdkCtx.EventManager().ABCIEvents()
  2647  				s.Require().False(eventTypeFound(events, EventProposalPruned))
  2648  			},
  2649  		},
  2650  		"open proposal must not fail": {
  2651  			setupProposal: func(ctx context.Context) uint64 {
  2652  				return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
  2653  			},
  2654  			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
  2655  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2656  			postRun: func(sdkCtx sdk.Context) {
  2657  				events := sdkCtx.EventManager().ABCIEvents()
  2658  				s.Require().False(eventTypeFound(events, EventProposalPruned))
  2659  			},
  2660  		},
  2661  		"invalid proposal id": {
  2662  			setupProposal: func(ctx context.Context) uint64 {
  2663  				return 0
  2664  			},
  2665  			expErr:    true,
  2666  			expErrMsg: "proposal id: value is empty",
  2667  		},
  2668  		"existing proposal required": {
  2669  			setupProposal: func(ctx context.Context) uint64 {
  2670  				return 9999
  2671  			},
  2672  			expErr:    true,
  2673  			expErrMsg: "load proposal: not found",
  2674  		},
  2675  		"Decision policy also applied on exactly voting period end": {
  2676  			setupProposal: func(ctx context.Context) uint64 {
  2677  				msgs := []sdk.Msg{msgSend1}
  2678  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO)
  2679  			},
  2680  			srcBlockTime:      s.blockTime.Add(time.Second), // Voting period is 1s
  2681  			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
  2682  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2683  			postRun:           func(sdkCtx sdk.Context) {},
  2684  		},
  2685  		"Decision policy also applied after voting period end": {
  2686  			setupProposal: func(ctx context.Context) uint64 {
  2687  				msgs := []sdk.Msg{msgSend1}
  2688  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO)
  2689  			},
  2690  			srcBlockTime:      s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s
  2691  			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
  2692  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2693  			postRun:           func(sdkCtx sdk.Context) {},
  2694  		},
  2695  		"exec proposal before MinExecutionPeriod should fail": {
  2696  			setupProposal: func(ctx context.Context) uint64 {
  2697  				msgs := []sdk.Msg{msgSend1}
  2698  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2699  			},
  2700  			srcBlockTime:      s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime
  2701  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2702  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed
  2703  			postRun:           func(sdkCtx sdk.Context) {},
  2704  		},
  2705  		"exec proposal at exactly MinExecutionPeriod should pass": {
  2706  			setupProposal: func(ctx context.Context) uint64 {
  2707  				msgs := []sdk.Msg{msgSend1}
  2708  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil)
  2709  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2710  			},
  2711  			srcBlockTime:      s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime
  2712  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2713  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2714  			postRun: func(sdkCtx sdk.Context) {
  2715  				events := sdkCtx.EventManager().ABCIEvents()
  2716  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  2717  			},
  2718  		},
  2719  		"prevent double execution when successful": {
  2720  			setupProposal: func(ctx context.Context) uint64 {
  2721  				myProposalID := submitProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.VOTE_OPTION_YES)
  2722  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil)
  2723  
  2724  				// Wait after min execution period end before Exec
  2725  				sdkCtx := sdk.UnwrapSDKContext(ctx)
  2726  				sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s
  2727  				_, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID})
  2728  				s.Require().NoError(err)
  2729  				return myProposalID
  2730  			},
  2731  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2732  			expErr:            true,                                // since proposal is pruned after a successful MsgExec
  2733  			expErrMsg:         "load proposal: not found",
  2734  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2735  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2736  			expBalance:        true,
  2737  			expFromBalances:   sdk.NewInt64Coin("test", 9900),
  2738  			expToBalances:     sdk.NewInt64Coin("test", 100),
  2739  			postRun:           func(sdkCtx sdk.Context) {},
  2740  		},
  2741  		"rollback all msg updates on failure": {
  2742  			setupProposal: func(ctx context.Context) uint64 {
  2743  				msgs := []sdk.Msg{msgSend1, msgSend2}
  2744  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil)
  2745  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error"))
  2746  
  2747  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2748  			},
  2749  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2750  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2751  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE,
  2752  			postRun:           func(sdkCtx sdk.Context) {},
  2753  		},
  2754  		"executable when failed before": {
  2755  			setupProposal: func(ctx context.Context) uint64 {
  2756  				msgs := []sdk.Msg{msgSend2}
  2757  				myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2758  
  2759  				// Wait after min execution period end before Exec
  2760  				sdkCtx := sdk.UnwrapSDKContext(ctx)
  2761  				sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s
  2762  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error"))
  2763  				_, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID})
  2764  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil)
  2765  
  2766  				s.Require().NoError(err)
  2767  				s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)}))
  2768  
  2769  				return myProposalID
  2770  			},
  2771  			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
  2772  			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
  2773  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2774  			postRun:           func(sdkCtx sdk.Context) {},
  2775  		},
  2776  	}
  2777  	for msg, spec := range specs {
  2778  		spec := spec
  2779  		s.Run(msg, func() {
  2780  			sdkCtx, _ := s.sdkCtx.CacheContext()
  2781  			proposalID := spec.setupProposal(sdkCtx)
  2782  
  2783  			if !spec.srcBlockTime.IsZero() {
  2784  				sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime)
  2785  			}
  2786  
  2787  			_, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID})
  2788  			if spec.expErr {
  2789  				s.Require().Error(err)
  2790  				s.Require().Contains(err.Error(), spec.expErrMsg)
  2791  				return
  2792  			}
  2793  			s.Require().NoError(err)
  2794  
  2795  			if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) {
  2796  
  2797  				// and proposal is updated
  2798  				res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID})
  2799  				s.Require().NoError(err)
  2800  				proposal := res.Proposal
  2801  
  2802  				exp := group.ProposalStatus_name[int32(spec.expProposalStatus)]
  2803  				got := group.ProposalStatus_name[int32(proposal.Status)]
  2804  				s.Assert().Equal(exp, got)
  2805  
  2806  				exp = group.ProposalExecutorResult_name[int32(spec.expExecutorResult)]
  2807  				got = group.ProposalExecutorResult_name[int32(proposal.ExecutorResult)]
  2808  				s.Assert().Equal(exp, got)
  2809  			}
  2810  
  2811  			if spec.expBalance {
  2812  				s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, s.groupPolicyAddr).Return(sdk.Coins{spec.expFromBalances})
  2813  				s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.Coins{spec.expToBalances})
  2814  
  2815  				fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, s.groupPolicyAddr)
  2816  				s.Require().Contains(fromBalances, spec.expFromBalances)
  2817  				toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2)
  2818  				s.Require().Contains(toBalances, spec.expToBalances)
  2819  			}
  2820  			spec.postRun(sdkCtx)
  2821  		})
  2822  
  2823  	}
  2824  }
  2825  
  2826  func (s *TestSuite) TestExecPrunedProposalsAndVotes() {
  2827  	addrs := s.addrs
  2828  	addr1 := addrs[0]
  2829  	addr2 := addrs[1]
  2830  
  2831  	proposers := []string{addr2.String()}
  2832  	specs := map[string]struct {
  2833  		srcBlockTime      time.Time
  2834  		setupProposal     func(ctx context.Context) uint64
  2835  		expErr            bool
  2836  		expErrMsg         string
  2837  		expExecutorResult group.ProposalExecutorResult
  2838  	}{
  2839  		"proposal pruned after executor result success": {
  2840  			setupProposal: func(ctx context.Context) uint64 {
  2841  				msgSend1 := &banktypes.MsgSend{
  2842  					FromAddress: s.groupPolicyAddr.String(),
  2843  					ToAddress:   addr2.String(),
  2844  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 101)},
  2845  				}
  2846  				msgs := []sdk.Msg{msgSend1}
  2847  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil)
  2848  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2849  			},
  2850  			expErrMsg:         "load proposal: not found",
  2851  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2852  		},
  2853  		"proposal with multiple messages pruned when executed with result success": {
  2854  			setupProposal: func(ctx context.Context) uint64 {
  2855  				msgSend1 := &banktypes.MsgSend{
  2856  					FromAddress: s.groupPolicyAddr.String(),
  2857  					ToAddress:   addr2.String(),
  2858  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 102)},
  2859  				}
  2860  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2)
  2861  
  2862  				msgs := []sdk.Msg{msgSend1, msgSend1}
  2863  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2864  			},
  2865  			expErrMsg:         "load proposal: not found",
  2866  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2867  		},
  2868  		"proposal not pruned when not executed and rejected": {
  2869  			setupProposal: func(ctx context.Context) uint64 {
  2870  				msgSend1 := &banktypes.MsgSend{
  2871  					FromAddress: s.groupPolicyAddr.String(),
  2872  					ToAddress:   addr2.String(),
  2873  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 103)},
  2874  				}
  2875  				msgs := []sdk.Msg{msgSend1}
  2876  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO)
  2877  			},
  2878  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2879  		},
  2880  		"open proposal is not pruned which must not fail ": {
  2881  			setupProposal: func(ctx context.Context) uint64 {
  2882  				msgSend1 := &banktypes.MsgSend{
  2883  					FromAddress: s.groupPolicyAddr.String(),
  2884  					ToAddress:   addr2.String(),
  2885  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 104)},
  2886  				}
  2887  				return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
  2888  			},
  2889  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2890  		},
  2891  		"proposal not pruned with group modified before tally": {
  2892  			setupProposal: func(ctx context.Context) uint64 {
  2893  				msgSend1 := &banktypes.MsgSend{
  2894  					FromAddress: s.groupPolicyAddr.String(),
  2895  					ToAddress:   addr2.String(),
  2896  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 105)},
  2897  				}
  2898  				myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
  2899  
  2900  				// then modify group
  2901  				_, err := s.groupKeeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{
  2902  					Admin:   addr1.String(),
  2903  					GroupId: s.groupID,
  2904  				})
  2905  				s.Require().NoError(err)
  2906  				return myProposalID
  2907  			},
  2908  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2909  		},
  2910  		"proposal not pruned with group policy modified before tally": {
  2911  			setupProposal: func(ctx context.Context) uint64 {
  2912  				msgSend1 := &banktypes.MsgSend{
  2913  					FromAddress: s.groupPolicyAddr.String(),
  2914  					ToAddress:   addr2.String(),
  2915  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 106)},
  2916  				}
  2917  
  2918  				myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers)
  2919  				_, err := s.groupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{
  2920  					Admin:              addr1.String(),
  2921  					GroupPolicyAddress: s.groupPolicyAddr.String(),
  2922  				})
  2923  				s.Require().NoError(err)
  2924  				return myProposalID
  2925  			},
  2926  			expErr:            true, // since proposal status will be `aborted` when group policy is modified
  2927  			expErrMsg:         "not possible to exec with proposal status",
  2928  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
  2929  		},
  2930  		"proposal exists when rollback all msg updates on failure": {
  2931  			setupProposal: func(ctx context.Context) uint64 {
  2932  				msgSend1 := &banktypes.MsgSend{
  2933  					FromAddress: s.groupPolicyAddr.String(),
  2934  					ToAddress:   addr2.String(),
  2935  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 107)},
  2936  				}
  2937  
  2938  				msgSend2 := &banktypes.MsgSend{
  2939  					FromAddress: s.groupPolicyAddr.String(),
  2940  					ToAddress:   addr2.String(),
  2941  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 10002)},
  2942  				}
  2943  
  2944  				msgs := []sdk.Msg{msgSend1, msgSend2}
  2945  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, fmt.Errorf("error"))
  2946  
  2947  				return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2948  			},
  2949  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE,
  2950  		},
  2951  		"pruned when proposal is executable when failed before": {
  2952  			setupProposal: func(ctx context.Context) uint64 {
  2953  				msgSend2 := &banktypes.MsgSend{
  2954  					FromAddress: s.groupPolicyAddr.String(),
  2955  					ToAddress:   addr2.String(),
  2956  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 10003)},
  2957  				}
  2958  
  2959  				msgs := []sdk.Msg{msgSend2}
  2960  
  2961  				myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES)
  2962  
  2963  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error"))
  2964  
  2965  				// Wait for min execution period end
  2966  				sdkCtx := sdk.UnwrapSDKContext(ctx)
  2967  				sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod))
  2968  				_, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID})
  2969  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil)
  2970  
  2971  				s.Require().NoError(err)
  2972  				return myProposalID
  2973  			},
  2974  			expErrMsg:         "load proposal: not found",
  2975  			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
  2976  		},
  2977  	}
  2978  	for msg, spec := range specs {
  2979  		spec := spec
  2980  		s.Run(msg, func() {
  2981  			sdkCtx, _ := s.sdkCtx.CacheContext()
  2982  			proposalID := spec.setupProposal(sdkCtx)
  2983  
  2984  			if !spec.srcBlockTime.IsZero() {
  2985  				sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime)
  2986  			}
  2987  
  2988  			// Wait for min execution period end
  2989  			sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod))
  2990  			_, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID})
  2991  			if spec.expErr {
  2992  				s.Require().Error(err)
  2993  				s.Require().Contains(err.Error(), spec.expErrMsg)
  2994  				return
  2995  			}
  2996  			s.Require().NoError(err)
  2997  
  2998  			if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS {
  2999  				// Make sure proposal is deleted from state
  3000  				_, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID})
  3001  				s.Require().Contains(err.Error(), spec.expErrMsg)
  3002  				res, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: proposalID})
  3003  				s.Require().NoError(err)
  3004  				s.Require().Empty(res.GetVotes())
  3005  				events := sdkCtx.EventManager().ABCIEvents()
  3006  				s.Require().True(eventTypeFound(events, EventProposalPruned))
  3007  
  3008  			} else {
  3009  				// Check that proposal and votes exists
  3010  				res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID})
  3011  				s.Require().NoError(err)
  3012  				_, err = s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id})
  3013  				s.Require().NoError(err)
  3014  				s.Require().Equal("", spec.expErrMsg)
  3015  
  3016  				exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)]
  3017  				got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)]
  3018  				s.Assert().Equal(exp, got)
  3019  			}
  3020  		})
  3021  	}
  3022  }
  3023  
  3024  func (s *TestSuite) TestLeaveGroup() {
  3025  	addrs := simtestutil.CreateIncrementalAccounts(7)
  3026  
  3027  	admin1 := addrs[0]
  3028  	member1 := addrs[1]
  3029  	member2 := addrs[2]
  3030  	member3 := addrs[3]
  3031  	member4 := addrs[4]
  3032  	admin2 := addrs[5]
  3033  	admin3 := addrs[6]
  3034  
  3035  	members := []group.MemberRequest{
  3036  		{
  3037  			Address:  member1.String(),
  3038  			Weight:   "1",
  3039  			Metadata: "metadata",
  3040  		},
  3041  		{
  3042  			Address:  member2.String(),
  3043  			Weight:   "2",
  3044  			Metadata: "metadata",
  3045  		},
  3046  		{
  3047  			Address:  member3.String(),
  3048  			Weight:   "3",
  3049  			Metadata: "metadata",
  3050  		},
  3051  	}
  3052  	policy := group.NewThresholdDecisionPolicy(
  3053  		"3",
  3054  		time.Hour,
  3055  		time.Hour,
  3056  	)
  3057  	s.setNextAccount()
  3058  	_, groupID1 := s.createGroupAndGroupPolicy(admin1, members, policy)
  3059  
  3060  	members = []group.MemberRequest{
  3061  		{
  3062  			Address:  member1.String(),
  3063  			Weight:   "1",
  3064  			Metadata: "metadata",
  3065  		},
  3066  	}
  3067  
  3068  	s.setNextAccount()
  3069  	_, groupID2 := s.createGroupAndGroupPolicy(admin2, members, nil)
  3070  
  3071  	members = []group.MemberRequest{
  3072  		{
  3073  			Address:  member1.String(),
  3074  			Weight:   "1",
  3075  			Metadata: "metadata",
  3076  		},
  3077  		{
  3078  			Address:  member2.String(),
  3079  			Weight:   "2",
  3080  			Metadata: "metadata",
  3081  		},
  3082  	}
  3083  	policy = &group.PercentageDecisionPolicy{
  3084  		Percentage: "0.5",
  3085  		Windows:    &group.DecisionPolicyWindows{VotingPeriod: time.Hour},
  3086  	}
  3087  
  3088  	s.setNextAccount()
  3089  
  3090  	_, groupID3 := s.createGroupAndGroupPolicy(admin3, members, policy)
  3091  	testCases := []struct {
  3092  		name           string
  3093  		req            *group.MsgLeaveGroup
  3094  		expErr         bool
  3095  		expErrMsg      string
  3096  		expMembersSize int
  3097  		memberWeight   math.Dec
  3098  	}{
  3099  		{
  3100  			"group not found",
  3101  			&group.MsgLeaveGroup{
  3102  				GroupId: 100000,
  3103  				Address: member1.String(),
  3104  			},
  3105  			true,
  3106  			"group: not found",
  3107  			0,
  3108  			math.NewDecFromInt64(0),
  3109  		},
  3110  		{
  3111  			"member address invalid",
  3112  			&group.MsgLeaveGroup{
  3113  				GroupId: groupID1,
  3114  				Address: "invalid",
  3115  			},
  3116  			true,
  3117  			"decoding bech32 failed",
  3118  			0,
  3119  			math.NewDecFromInt64(0),
  3120  		},
  3121  		{
  3122  			"member not part of group",
  3123  			&group.MsgLeaveGroup{
  3124  				GroupId: groupID1,
  3125  				Address: member4.String(),
  3126  			},
  3127  			true,
  3128  			"not part of group",
  3129  			0,
  3130  			math.NewDecFromInt64(0),
  3131  		},
  3132  		{
  3133  			"valid testcase: decision policy is not present (and group total weight can be 0)",
  3134  			&group.MsgLeaveGroup{
  3135  				GroupId: groupID2,
  3136  				Address: member1.String(),
  3137  			},
  3138  			false,
  3139  			"",
  3140  			0,
  3141  			math.NewDecFromInt64(1),
  3142  		},
  3143  		{
  3144  			"valid testcase: threshold decision policy",
  3145  			&group.MsgLeaveGroup{
  3146  				GroupId: groupID1,
  3147  				Address: member3.String(),
  3148  			},
  3149  			false,
  3150  			"",
  3151  			2,
  3152  			math.NewDecFromInt64(3),
  3153  		},
  3154  		{
  3155  			"valid request: can leave group policy threshold more than group weight",
  3156  			&group.MsgLeaveGroup{
  3157  				GroupId: groupID1,
  3158  				Address: member2.String(),
  3159  			},
  3160  			false,
  3161  			"",
  3162  			1,
  3163  			math.NewDecFromInt64(2),
  3164  		},
  3165  		{
  3166  			"valid request: can leave group (percentage decision policy)",
  3167  			&group.MsgLeaveGroup{
  3168  				GroupId: groupID3,
  3169  				Address: member2.String(),
  3170  			},
  3171  			false,
  3172  			"",
  3173  			1,
  3174  			math.NewDecFromInt64(2),
  3175  		},
  3176  	}
  3177  
  3178  	for _, tc := range testCases {
  3179  		s.Run(tc.name, func() {
  3180  			var groupWeight1 math.Dec
  3181  			if !tc.expErr {
  3182  				groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId})
  3183  				s.Require().NoError(err)
  3184  				groupWeight1, err = math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight)
  3185  				s.Require().NoError(err)
  3186  			}
  3187  
  3188  			res, err := s.groupKeeper.LeaveGroup(s.ctx, tc.req)
  3189  			if tc.expErr {
  3190  				s.Require().Error(err)
  3191  				s.Require().Contains(err.Error(), tc.expErrMsg)
  3192  			} else {
  3193  				s.Require().NoError(err)
  3194  				s.Require().NotNil(res)
  3195  				res, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{
  3196  					GroupId: tc.req.GroupId,
  3197  				})
  3198  				s.Require().NoError(err)
  3199  				s.Require().Len(res.Members, tc.expMembersSize)
  3200  
  3201  				groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId})
  3202  				s.Require().NoError(err)
  3203  				groupWeight2, err := math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight)
  3204  				s.Require().NoError(err)
  3205  
  3206  				rWeight, err := groupWeight1.Sub(tc.memberWeight)
  3207  				s.Require().NoError(err)
  3208  				s.Require().Equal(rWeight.Cmp(groupWeight2), 0)
  3209  			}
  3210  		})
  3211  	}
  3212  }
  3213  
  3214  func (s *TestSuite) TestExecProposalsWhenMemberLeavesOrIsUpdated() {
  3215  	proposers := []string{s.addrs[1].String()}
  3216  
  3217  	specs := map[string]struct {
  3218  		votes         []group.VoteOption
  3219  		members       []group.MemberRequest
  3220  		setupProposal func(ctx context.Context, groupPolicyAddr string) uint64
  3221  		malleate      func(ctx context.Context, k keeper.Keeper, groupPolicyAddr string, groupID uint64) error
  3222  		expErrMsg     string
  3223  	}{
  3224  		"member leaves while all others vote yes: proposal accepted": {
  3225  			members: []group.MemberRequest{
  3226  				{Address: s.addrs[4].String(), Weight: "1"},
  3227  				{Address: s.addrs[1].String(), Weight: "2"},
  3228  				{Address: s.addrs[3].String(), Weight: "1"},
  3229  				{Address: s.addrs[5].String(), Weight: "2"},
  3230  				{Address: s.addrs[2].String(), Weight: "2"},
  3231  			},
  3232  			votes: []group.VoteOption{
  3233  				group.VOTE_OPTION_YES, group.VOTE_OPTION_YES,
  3234  				group.VOTE_OPTION_YES, group.VOTE_OPTION_YES,
  3235  				group.VOTE_OPTION_YES,
  3236  			},
  3237  			setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 {
  3238  				msgSend1 := &banktypes.MsgSend{
  3239  					FromAddress: groupPolicyAddr,
  3240  					ToAddress:   s.addrs[1].String(),
  3241  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  3242  				}
  3243  
  3244  				// the proposal will pass and be executed
  3245  				s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(1)
  3246  
  3247  				msgs := []sdk.Msg{msgSend1}
  3248  				proposalReq := &group.MsgSubmitProposal{
  3249  					GroupPolicyAddress: groupPolicyAddr,
  3250  					Proposers:          proposers,
  3251  				}
  3252  				err := proposalReq.SetMsgs(msgs)
  3253  				s.Require().NoError(err)
  3254  
  3255  				proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
  3256  				s.Require().NoError(err)
  3257  
  3258  				return proposalRes.ProposalId
  3259  			},
  3260  			malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error {
  3261  				_, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[5].String()})
  3262  				return err
  3263  			},
  3264  		},
  3265  		"member leaves while all others vote yes and no: proposal rejected": {
  3266  			members: []group.MemberRequest{
  3267  				{Address: s.addrs[4].String(), Weight: "2"},
  3268  				{Address: s.addrs[1].String(), Weight: "2"},
  3269  				{Address: s.addrs[3].String(), Weight: "2"},
  3270  				{Address: s.addrs[2].String(), Weight: "2"},
  3271  			},
  3272  			votes: []group.VoteOption{
  3273  				group.VOTE_OPTION_NO, group.VOTE_OPTION_NO,
  3274  				group.VOTE_OPTION_YES, group.VOTE_OPTION_YES,
  3275  			},
  3276  			setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 {
  3277  				msgSend1 := &banktypes.MsgSend{
  3278  					FromAddress: groupPolicyAddr,
  3279  					ToAddress:   s.addrs[1].String(),
  3280  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  3281  				}
  3282  				msgs := []sdk.Msg{msgSend1, msgSend1}
  3283  
  3284  				proposalReq := &group.MsgSubmitProposal{
  3285  					GroupPolicyAddress: groupPolicyAddr,
  3286  					Proposers:          proposers,
  3287  				}
  3288  				err := proposalReq.SetMsgs(msgs)
  3289  				s.Require().NoError(err)
  3290  
  3291  				proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
  3292  				s.Require().NoError(err)
  3293  
  3294  				return proposalRes.ProposalId
  3295  			},
  3296  			malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error {
  3297  				_, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[3].String()})
  3298  				return err
  3299  			},
  3300  		},
  3301  		"member that leaves does affect the threshold policy outcome": {
  3302  			members: []group.MemberRequest{
  3303  				{Address: s.addrs[3].String(), Weight: "6"},
  3304  				{Address: s.addrs[1].String(), Weight: "1"},
  3305  				{Address: s.addrs[5].String(), Weight: "1"},
  3306  				{Address: s.addrs[2].String(), Weight: "1"},
  3307  			},
  3308  			votes: []group.VoteOption{
  3309  				group.VOTE_OPTION_YES, group.VOTE_OPTION_NO,
  3310  				group.VOTE_OPTION_YES, group.VOTE_OPTION_YES,
  3311  			},
  3312  			setupProposal: func(ctx context.Context, addr string) uint64 {
  3313  				msgSend1 := &banktypes.MsgSend{
  3314  					FromAddress: addr,
  3315  					ToAddress:   s.addrs[1].String(),
  3316  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  3317  				}
  3318  				msgs := []sdk.Msg{msgSend1, msgSend1}
  3319  
  3320  				proposalReq := &group.MsgSubmitProposal{
  3321  					GroupPolicyAddress: addr,
  3322  					Proposers:          proposers,
  3323  				}
  3324  				err := proposalReq.SetMsgs(msgs)
  3325  				s.Require().NoError(err)
  3326  
  3327  				proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
  3328  				s.Require().NoError(err)
  3329  
  3330  				return proposalRes.ProposalId
  3331  			},
  3332  			malleate: func(ctx context.Context, k keeper.Keeper, _ string, groupID uint64) error {
  3333  				_, err := k.LeaveGroup(ctx, &group.MsgLeaveGroup{GroupId: groupID, Address: s.addrs[3].String()})
  3334  				return err
  3335  			},
  3336  		},
  3337  		"update group policy voids the proposal": {
  3338  			members: []group.MemberRequest{
  3339  				{Address: s.addrs[3].String(), Weight: "2"},
  3340  				{Address: s.addrs[2].String(), Weight: "2"},
  3341  				{Address: s.addrs[1].String(), Weight: "2"},
  3342  				{Address: s.addrs[4].String(), Weight: "2"},
  3343  			},
  3344  			votes: []group.VoteOption{
  3345  				group.VOTE_OPTION_YES, group.VOTE_OPTION_NO,
  3346  				group.VOTE_OPTION_YES, group.VOTE_OPTION_NO,
  3347  			},
  3348  			setupProposal: func(ctx context.Context, groupPolicyAddr string) uint64 {
  3349  				msgSend1 := &banktypes.MsgSend{
  3350  					FromAddress: groupPolicyAddr,
  3351  					ToAddress:   s.addrs[1].String(),
  3352  					Amount:      sdk.Coins{sdk.NewInt64Coin("test", 100)},
  3353  				}
  3354  				msgs := []sdk.Msg{msgSend1, msgSend1}
  3355  				proposalReq := &group.MsgSubmitProposal{
  3356  					GroupPolicyAddress: groupPolicyAddr,
  3357  					Proposers:          proposers,
  3358  				}
  3359  				err := proposalReq.SetMsgs(msgs)
  3360  				s.Require().NoError(err)
  3361  
  3362  				proposalRes, err := s.groupKeeper.SubmitProposal(ctx, proposalReq)
  3363  				s.Require().NoError(err)
  3364  
  3365  				return proposalRes.ProposalId
  3366  			},
  3367  			malleate: func(ctx context.Context, k keeper.Keeper, groupPolicyAddr string, groupID uint64) error {
  3368  				newGroupPolicy := &group.MsgUpdateGroupPolicyDecisionPolicy{
  3369  					Admin:              s.addrs[0].String(),
  3370  					GroupPolicyAddress: groupPolicyAddr,
  3371  				}
  3372  				newGroupPolicy.SetDecisionPolicy(group.NewThresholdDecisionPolicy("10", time.Second, minExecutionPeriod))
  3373  
  3374  				_, err := k.UpdateGroupPolicyDecisionPolicy(ctx, newGroupPolicy)
  3375  				return err
  3376  			},
  3377  			expErrMsg: "PROPOSAL_STATUS_ABORTED",
  3378  		},
  3379  	}
  3380  	for msg, spec := range specs {
  3381  		spec := spec
  3382  		s.Run(msg, func() {
  3383  			sdkCtx, _ := s.sdkCtx.CacheContext()
  3384  
  3385  			s.setNextAccount()
  3386  			groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{
  3387  				Admin:   s.addrs[0].String(),
  3388  				Members: spec.members,
  3389  			})
  3390  			s.Require().NoError(err)
  3391  			groupID := groupRes.GroupId
  3392  
  3393  			policy := group.NewThresholdDecisionPolicy("4", time.Second, minExecutionPeriod)
  3394  			policyReq := &group.MsgCreateGroupPolicy{
  3395  				Admin:   s.addrs[0].String(),
  3396  				GroupId: groupID,
  3397  			}
  3398  			err = policyReq.SetDecisionPolicy(policy)
  3399  			s.Require().NoError(err)
  3400  
  3401  			s.setNextAccount()
  3402  
  3403  			s.groupKeeper.GetGroupSequence(s.sdkCtx)
  3404  			policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq)
  3405  			s.Require().NoError(err)
  3406  
  3407  			// Setup and submit proposal
  3408  			proposalID := spec.setupProposal(sdkCtx, policyRes.Address)
  3409  
  3410  			// vote on the proposals
  3411  			for i, vote := range spec.votes {
  3412  				_, err := s.groupKeeper.Vote(sdkCtx, &group.MsgVote{
  3413  					ProposalId: proposalID,
  3414  					Voter:      spec.members[i].Address,
  3415  					Option:     vote,
  3416  				})
  3417  				s.Require().NoError(err)
  3418  			}
  3419  
  3420  			err = spec.malleate(sdkCtx, s.groupKeeper, policyRes.Address, groupID)
  3421  			s.Require().NoError(err)
  3422  
  3423  			// travel in time
  3424  			sdkCtx = sdkCtx.WithBlockTime(s.blockTime.Add(minExecutionPeriod + 1))
  3425  			_, err = s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: s.addrs[1].String(), ProposalId: proposalID})
  3426  			if spec.expErrMsg != "" {
  3427  				s.Require().Contains(err.Error(), spec.expErrMsg)
  3428  				return
  3429  			}
  3430  			s.Require().NoError(err)
  3431  		})
  3432  	}
  3433  }
  3434  
  3435  func eventTypeFound(events []abci.Event, eventType string) bool {
  3436  	eventTypeFound := false
  3437  	for _, e := range events {
  3438  		if e.Type == eventType {
  3439  			eventTypeFound = true
  3440  			break
  3441  		}
  3442  	}
  3443  	return eventTypeFound
  3444  }