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