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 }