From 10d7d15f04eb7e8999e997e3bb788cef02250daf Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Tue, 18 Apr 2023 16:26:33 +0200 Subject: [PATCH] refactor(group): move `ValidateBasic` logic to `msgServer` (#15785) Co-authored-by: Marko --- tests/e2e/group/tx.go | 2119 +------------------- x/authz/keeper/msg_server.go | 5 +- x/group/client/cli/tx.go | 133 +- x/group/client/cli/tx_test.go | 520 +++-- x/group/client/cli/util.go | 7 +- x/group/keeper/keeper_test.go | 2859 +-------------------------- x/group/keeper/msg_server.go | 585 +++--- x/group/keeper/msg_server_test.go | 3078 +++++++++++++++++++++++++++++ x/group/msgs.go | 308 --- x/group/msgs_test.go | 1232 ------------ x/group/types.go | 10 +- x/group/typesupport.go | 49 - 12 files changed, 3782 insertions(+), 7123 deletions(-) create mode 100644 x/group/keeper/msg_server_test.go delete mode 100644 x/group/msgs_test.go delete mode 100644 x/group/typesupport.go diff --git a/tests/e2e/group/tx.go b/tests/e2e/group/tx.go index be371cb9ab..c3e184db89 100644 --- a/tests/e2e/group/tx.go +++ b/tests/e2e/group/tx.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" "github.com/cosmos/gogoproto/proto" @@ -12,18 +11,15 @@ import ( "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/codec/address" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/testutil" clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/cosmos/cosmos-sdk/testutil/network" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/group" client "github.com/cosmos/cosmos-sdk/x/group/client/cli" - "github.com/cosmos/cosmos-sdk/x/group/errors" ) type E2ETestSuite struct { @@ -42,8 +38,6 @@ type E2ETestSuite struct { const validMetadata = "metadata" -var tooLongMetadata = strings.Repeat("A", 256) - func NewE2ETestSuite(cfg network.Config) *E2ETestSuite { return &E2ETestSuite{cfg: cfg} } @@ -208,2091 +202,6 @@ func (s *E2ETestSuite) TearDownSuite() { s.network.Cleanup() } -func (s *E2ETestSuite) TestTxCreateGroup() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - validMembers := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, val.Address.String(), validMetadata) - validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - - invalidMembersAddress := `{"members": [{ - "address": "", - "weight": "1" -}]}` - invalidMembersAddressFile := testutil.WriteToNewTempFile(s.T(), invalidMembersAddress) - - invalidMembersWeight := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "0" - }]}`, val.Address.String()) - invalidMembersWeightFile := testutil.WriteToNewTempFile(s.T(), invalidMembersWeight) - - invalidMembersMetadata := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, val.Address.String(), tooLongMetadata) - invalidMembersMetadataFile := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - val.Address.String(), - "", - validMembersFile.Name(), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - val.Address.String(), - "", - validMembersFile.Name(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group metadata too long", - append( - []string{ - val.Address.String(), - strings.Repeat("a", 256), - "", - }, - s.commonFlags..., - ), - false, - "group metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "invalid members address", - append( - []string{ - val.Address.String(), - "null", - invalidMembersAddressFile.Name(), - }, - s.commonFlags..., - ), - true, - "message validation failed: address: empty address string is not allowed", - nil, - 0, - }, - { - "invalid members weight", - append( - []string{ - val.Address.String(), - "null", - invalidMembersWeightFile.Name(), - }, - s.commonFlags..., - ), - true, - "expected a positive decimal, got 0: invalid decimal string", - nil, - 0, - }, - { - "members metadata too long", - append( - []string{ - val.Address.String(), - "null", - invalidMembersMetadataFile.Name(), - }, - s.commonFlags..., - ), - false, - "member metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgCreateGroupCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupAdmin() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - groupIDs := make([]string, 2) - for i := 0; i < 2; i++ { - validMembers := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, val.Address.String(), validMetadata) - validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, client.MsgCreateGroupCmd(), - append( - []string{ - val.Address.String(), - validMetadata, - validMembersFile.Name(), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - var txResp sdk.TxResponse - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String()) - txResp, err = clitestutil.GetTxResponse(s.network, val.ClientCtx, txResp.TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, uint32(0), out.String()) - groupIDs[i] = s.getGroupIDFromTxResponse(txResp) - } - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - val.Address.String(), - groupIDs[0], - s.network.Validators[1].Address.String(), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - val.Address.String(), - groupIDs[1], - s.network.Validators[1].Address.String(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group id invalid", - append( - []string{ - val.Address.String(), - "", - s.network.Validators[1].Address.String(), - }, - s.commonFlags..., - ), - true, - "strconv.ParseUint: parsing \"\": invalid syntax", - nil, - 0, - }, - { - "group doesn't exist", - append( - []string{ - val.Address.String(), - "12345", - s.network.Validators[1].Address.String(), - }, - s.commonFlags..., - ), - false, - "not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupAdminCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupMetadata() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - val.Address.String(), - "1", - validMetadata, - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - val.Address.String(), - "1", - validMetadata, - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group metadata too long", - append( - []string{ - val.Address.String(), - strconv.FormatUint(s.group.Id, 10), - strings.Repeat("a", 256), - }, - s.commonFlags..., - ), - false, - "group metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupMetadataCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupMembers() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - weights := []string{"1", "1", "1"} - accounts := s.createAccounts(3) - - groupID := s.createGroupWithMembers(weights, accounts) - groupPolicyAddress := s.createGroupThresholdPolicyWithBalance(accounts[0], groupID, 3, 100) - - validUpdatedMembersFileName := testutil.WriteToNewTempFile(s.T(), fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "0", - "metadata": "%s" - }, { - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, accounts[0], validMetadata, groupPolicyAddress, validMetadata)).Name() - - invalidMembersMetadata := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, accounts[0], tooLongMetadata) - invalidMembersMetadataFileName := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata).Name() - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - accounts[0], - groupID, - validUpdatedMembersFileName, - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - accounts[0], - groupID, - testutil.WriteToNewTempFile(s.T(), fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "2", - "metadata": "%s" - }]}`, s.groupPolicies[0].Address, validMetadata)).Name(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group member metadata too long", - append( - []string{ - accounts[0], - groupID, - invalidMembersMetadataFileName, - }, - s.commonFlags..., - ), - false, - "group member metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "group doesn't exist", - append( - []string{ - accounts[0], - "12345", - validUpdatedMembersFileName, - }, - s.commonFlags..., - ), - false, - "not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupMembersCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxCreateGroupWithPolicy() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - validMembers := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, val.Address.String(), validMetadata) - validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - - invalidMembersAddress := `{"members": [{ - "address": "", - "weight": "1" - }]}` - invalidMembersAddressFile := testutil.WriteToNewTempFile(s.T(), invalidMembersAddress) - - invalidMembersWeight := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "0" - }]}`, val.Address.String()) - invalidMembersWeightFile := testutil.WriteToNewTempFile(s.T(), invalidMembersWeight) - - invalidMembersMetadata := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, val.Address.String(), tooLongMetadata) - invalidMembersMetadataFile := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata) - - thresholdDecisionPolicyFile := testutil.WriteToNewTempFile(s.T(), `{"@type": "/cosmos.group.v1.ThresholdDecisionPolicy","threshold": "1","windows": {"voting_period":"1s"}}`) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - validMembersFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group-policy-as-admin is true", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - validMembersFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, true), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - validMembersFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "group metadata too long", - append( - []string{ - val.Address.String(), - strings.Repeat("a", 256), - validMetadata, - validMembersFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - false, - "group metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "group policy metadata too long", - append( - []string{ - val.Address.String(), - validMetadata, - strings.Repeat("a", 256), - validMembersFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - false, - "group policy metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "invalid members address", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - invalidMembersAddressFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - true, - "message validation failed: address: empty address string is not allowed", - nil, - 0, - }, - { - "invalid members weight", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - invalidMembersWeightFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - true, - "expected a positive decimal, got 0: invalid decimal string", - nil, - 0, - }, - { - "members metadata too long", - append( - []string{ - val.Address.String(), - validMetadata, - validMetadata, - invalidMembersMetadataFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", client.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - false, - "member metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - } - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgCreateGroupWithPolicyCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxCreateGroupPolicy() { - val := s.network.Validators[0] - wrongAdmin := s.network.Validators[1].Address - clientCtx := val.ClientCtx - - groupID := s.group.Id - - thresholdDecisionPolicyFile := testutil.WriteToNewTempFile(s.T(), `{"@type": "/cosmos.group.v1.ThresholdDecisionPolicy","threshold": "1","windows": {"voting_period":"1s"}}`) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - thresholdDecisionPolicyFile.Name(), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "correct data with percentage decision policy", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"0.5", "windows":{"voting_period":"1s"}}`).Name(), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "wrong admin", - append( - []string{ - wrongAdmin.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - thresholdDecisionPolicyFile.Name(), - }, - s.commonFlags..., - ), - true, - "key not found", - &sdk.TxResponse{}, - 0, - }, - { - "metadata too long", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - strings.Repeat("a", 500), - thresholdDecisionPolicyFile.Name(), - }, - s.commonFlags..., - ), - false, - "group policy metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "wrong group id", - append( - []string{ - val.Address.String(), - "10", - validMetadata, - thresholdDecisionPolicyFile.Name(), - }, - s.commonFlags..., - ), - false, - "not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - { - "invalid percentage decision policy with negative value", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"-0.5", "windows":{"voting_period":"1s"}}`).Name(), - }, - s.commonFlags..., - ), - true, - "expected a positive decimal", - &sdk.TxResponse{}, - 0, - }, - { - "invalid percentage decision policy with value greater than 1", - append( - []string{ - val.Address.String(), - fmt.Sprintf("%v", groupID), - validMetadata, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"2", "windows":{"voting_period":"1s"}}`).Name(), - }, - s.commonFlags..., - ), - true, - "percentage must be > 0 and <= 1", - &sdk.TxResponse{}, - 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgCreateGroupPolicyCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupPolicyAdmin() { - val := s.network.Validators[0] - newAdmin := s.network.Validators[1].Address - clientCtx := val.ClientCtx - groupPolicy := s.groupPolicies[3] - - commonFlags := s.commonFlags - commonFlags = append(commonFlags, fmt.Sprintf("--%s=%d", flags.FlagGas, 300000)) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - newAdmin.String(), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - groupPolicy.Admin, - s.groupPolicies[4].Address, - newAdmin.String(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "wrong admin", - append( - []string{ - newAdmin.String(), - groupPolicy.Address, - newAdmin.String(), - }, - commonFlags..., - ), - true, - "key not found", - &sdk.TxResponse{}, - 0, - }, - { - "wrong group policy", - append( - []string{ - groupPolicy.Admin, - newAdmin.String(), - newAdmin.String(), - }, - commonFlags..., - ), - false, - "load group policy: not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupPolicyAdminCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().NoError(clitestutil.CheckTxCode(s.network, clientCtx, txResp.TxHash, tc.expectedCode)) - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { - val := s.network.Validators[0] - newAdmin := s.network.Validators[1].Address - clientCtx := val.ClientCtx - groupPolicy := s.groupPolicies[2] - - commonFlags := s.commonFlags - commonFlags = append(commonFlags, fmt.Sprintf("--%s=%d", flags.FlagGas, 300000)) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"1", "windows":{"voting_period":"40000s"}}`).Name(), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "correct data with percentage decision policy", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"0.5", "windows":{"voting_period":"40000s"}}`).Name(), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"1", "windows":{"voting_period":"50000s"}}`).Name(), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "wrong admin", - append( - []string{ - newAdmin.String(), - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"1", "windows":{"voting_period":"1s"}}`).Name(), - }, - commonFlags..., - ), - true, - "key not found", - &sdk.TxResponse{}, - 0, - }, - { - "wrong group policy", - append( - []string{ - groupPolicy.Admin, - newAdmin.String(), - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"1", "windows":{"voting_period":"1s"}}`).Name(), - }, - commonFlags..., - ), - false, - "load group policy: not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - { - "invalid percentage decision policy with negative value", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"-0.5", "windows":{"voting_period":"1s"}}`).Name(), - }, - commonFlags..., - ), - true, - "expected a positive decimal", - &sdk.TxResponse{}, - 0, - }, - { - "invalid percentage decision policy with value greater than 1", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"2", "windows":{"voting_period":"40000s"}}`).Name(), - }, - commonFlags..., - ), - true, - "percentage must be > 0 and <= 1", - &sdk.TxResponse{}, - 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupPolicyDecisionPolicyCmd(address.NewBech32Codec("cosmos")) - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp := tc.respType.(*sdk.TxResponse) - s.Require().NoError(clitestutil.CheckTxCode(s.network, clientCtx, txResp.TxHash, tc.expectedCode)) - } - }) - } -} - -func (s *E2ETestSuite) TestTxUpdateGroupPolicyMetadata() { - val := s.network.Validators[0] - newAdmin := s.network.Validators[1].Address - clientCtx := val.ClientCtx - groupPolicy := s.groupPolicies[2] - - commonFlags := s.commonFlags - commonFlags = append(commonFlags, fmt.Sprintf("--%s=%d", flags.FlagGas, 300000)) - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - validMetadata, - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - validMetadata, - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "long metadata", - append( - []string{ - groupPolicy.Admin, - groupPolicy.Address, - strings.Repeat("a", 500), - }, - commonFlags..., - ), - false, - "group policy metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "wrong admin", - append( - []string{ - newAdmin.String(), - groupPolicy.Address, - validMetadata, - }, - commonFlags..., - ), - true, - "key not found", - &sdk.TxResponse{}, - 0, - }, - { - "wrong group policy", - append( - []string{ - groupPolicy.Admin, - newAdmin.String(), - validMetadata, - }, - commonFlags..., - ), - false, - "load group policy: not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgUpdateGroupPolicyMetadataCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxSubmitProposal() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", - "title", "summary", - ), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with try exec", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", - "title", "summary", - ), - fmt.Sprintf("--%s=try", client.FlagExec), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with try exec, not enough yes votes for proposal to pass", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[3].Address, val.Address.String(), - s.groupPolicies[3].Address, val.Address.String(), - "", "title", "summary"), - fmt.Sprintf("--%s=try", client.FlagExec), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", "title", "summary", - ), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "metadata too long", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - tooLongMetadata, "title", "summary", - ), - }, - s.commonFlags..., - ), - false, - "metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "unauthorized msg", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - val.Address.String(), s.groupPolicies[0].Address, - "", "title", "summary"), - }, - s.commonFlags..., - ), - false, - "msg does not have group policy authorization", - &sdk.TxResponse{}, - sdkerrors.ErrUnauthorized.ABCICode(), - }, - { - "invalid proposers", - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, "invalid", - s.groupPolicies[0].Address, val.Address.String(), - "", "title", "summary", - ), - }, - s.commonFlags..., - ), - true, - "invalid.info: key not found", - nil, - 0, - }, - { - "invalid group policy", - append( - []string{ - s.createCLIProposal( - "invalid", val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", "title", "summary", - ), - }, - s.commonFlags..., - ), - true, - "group policy: decoding bech32 failed", - nil, - 0, - }, - { - "no group policy", - append( - []string{ - s.createCLIProposal( - val.Address.String(), val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", "title", "summary", - ), - }, - s.commonFlags..., - ), - false, - "not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgSubmitProposalCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxVote() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - ids := make([]string, 4) - weights := []string{"1", "1", "1"} - accounts := s.createAccounts(3) - - groupID := s.createGroupWithMembers(weights, accounts) - groupPolicyAddress := s.createGroupThresholdPolicyWithBalance(accounts[0], groupID, 3, 100) - - for i := 0; i < 4; i++ { - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, client.MsgSubmitProposalCmd(), - append( - []string{ - s.createCLIProposal( - groupPolicyAddress, accounts[0], - groupPolicyAddress, accounts[0], - "", "title", "summary", - ), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - - var txResp sdk.TxResponse - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String()) - txResp, err = clitestutil.GetTxResponse(s.network, val.ClientCtx, txResp.TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, uint32(0), out.String()) - ids[i] = s.getProposalIDFromTxResponse(txResp) - } - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - ids[0], - accounts[0], - "VOTE_OPTION_YES", - "", - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with try exec", - append( - []string{ - ids[1], - accounts[0], - "VOTE_OPTION_YES", - "", - fmt.Sprintf("--%s=try", client.FlagExec), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with try exec, not enough yes votes for proposal to pass", - append( - []string{ - ids[2], - accounts[0], - "VOTE_OPTION_NO", - "", - fmt.Sprintf("--%s=try", client.FlagExec), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - ids[3], - accounts[0], - "VOTE_OPTION_YES", - "", - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "invalid proposal id", - append( - []string{ - "abcd", - accounts[0], - "VOTE_OPTION_YES", - "", - }, - s.commonFlags..., - ), - true, - "invalid syntax", - nil, - 0, - }, - { - "proposal not found", - append( - []string{ - "1234", - accounts[0], - "VOTE_OPTION_YES", - "", - }, - s.commonFlags..., - ), - false, - "proposal: not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - { - "metadata too long", - append( - []string{ - "2", - accounts[0], - "VOTE_OPTION_YES", - tooLongMetadata, - }, - s.commonFlags..., - ), - false, - "metadata: limit exceeded", - &sdk.TxResponse{}, - errors.ErrMaxLimit.ABCICode(), - }, - { - "invalid vote option", - append( - []string{ - "2", - accounts[0], - "INVALID_VOTE_OPTION", - "", - }, - s.commonFlags..., - ), - true, - "not a valid vote option", - nil, - 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgVoteCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxWithdrawProposal() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - ids := make([]string, 2) - - for i := 0; i < 2; i++ { - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, client.MsgSubmitProposalCmd(), - append( - []string{ - s.createCLIProposal( - s.groupPolicies[1].Address, val.Address.String(), - s.groupPolicies[1].Address, val.Address.String(), - "", "title", "summary"), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - - var txResp sdk.TxResponse - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String()) - txResp, err = clitestutil.GetTxResponse(s.network, val.ClientCtx, txResp.TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, uint32(0), out.String()) - ids[i] = s.getProposalIDFromTxResponse(txResp) - } - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - ids[0], - val.Address.String(), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "already withdrawn proposal", - append( - []string{ - ids[0], - val.Address.String(), - }, - s.commonFlags..., - ), - false, - "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN", - &sdk.TxResponse{}, - errors.ErrInvalid.ABCICode(), - }, - { - "proposal not found", - append( - []string{ - "222", - "wrongAdmin", - }, - s.commonFlags..., - ), - true, - "not found", - &sdk.TxResponse{}, - 0, - }, - { - "invalid proposal", - append( - []string{ - "abc", - val.Address.String(), - }, - s.commonFlags..., - ), - true, - "invalid syntax", - &sdk.TxResponse{}, - 0, - }, - { - "wrong admin", - append( - []string{ - ids[1], - "wrongAdmin", - }, - s.commonFlags..., - ), - true, - "key not found", - &sdk.TxResponse{}, - 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgWithdrawProposalCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) getProposalIDFromTxResponse(txResp sdk.TxResponse) string { - s.Require().Greater(len(txResp.Logs), 0) - s.Require().NotNil(txResp.Logs[0].Events) - events := txResp.Logs[0].Events - createProposalEvent, _ := sdk.TypedEventToEvent(&group.EventSubmitProposal{}) - - for _, e := range events { - if e.Type == createProposalEvent.Type { - return strings.ReplaceAll(e.Attributes[0].Value, "\"", "") - } - } - - return "" -} - -func (s *E2ETestSuite) TestTxExec() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - var proposalIDs []string - // create proposals and vote - for i := 0; i < 2; i++ { - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, client.MsgSubmitProposalCmd(), - append( - []string{ - s.createCLIProposal( - s.groupPolicies[0].Address, val.Address.String(), - s.groupPolicies[0].Address, val.Address.String(), - "", "title", "summary", - ), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - - var txResp sdk.TxResponse - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String()) - txResp, err = clitestutil.GetTxResponse(s.network, clientCtx, txResp.TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, uint32(0), out.String()) - proposalID := s.getProposalIDFromTxResponse(txResp) - proposalIDs = append(proposalIDs, proposalID) - - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, client.MsgVoteCmd(), - append( - []string{ - proposalID, - val.Address.String(), - "VOTE_OPTION_YES", - "", - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - s.Require().NoError(s.network.WaitForNextBlock()) - } - - testCases := []struct { - name string - args []string - expectErr bool - expectErrMsg string - respType proto.Message - expectedCode uint32 - }{ - { - "correct data", - append( - []string{ - proposalIDs[0], - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "with amino-json", - append( - []string{ - proposalIDs[1], - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), - fmt.Sprintf("--%s=%s", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - s.commonFlags..., - ), - false, - "", - &sdk.TxResponse{}, - 0, - }, - { - "invalid proposal id", - append( - []string{ - "abcd", - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), - }, - s.commonFlags..., - ), - true, - "invalid syntax", - nil, - 0, - }, - { - "proposal not found", - append( - []string{ - "1234", - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), - }, - s.commonFlags..., - ), - false, - "proposal: not found", - &sdk.TxResponse{}, - sdkerrors.ErrNotFound.ABCICode(), - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgExecCmd() - - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.expectErrMsg) - } else { - s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) - - txResp, err := clitestutil.GetTxResponse(s.network, clientCtx, tc.respType.(*sdk.TxResponse).TxHash) - s.Require().NoError(err) - s.Require().Equal(txResp.Code, tc.expectedCode) - if tc.expectErrMsg != "" { - s.Require().Contains(txResp.RawLog, tc.expectErrMsg) - } - } - }) - } -} - -func (s *E2ETestSuite) TestTxLeaveGroup() { - val := s.network.Validators[0] - clientCtx := val.ClientCtx - - // create 3 accounts with some tokens - members := s.createAccounts(3) - - // create a group with three members - validMembers := fmt.Sprintf(`{"members": [{ - "address": "%s", - "weight": "1", - "metadata": "AQ==" - },{ - "address": "%s", - "weight": "2", - "metadata": "AQ==" - },{ - "address": "%s", - "weight": "2", - "metadata": "AQ==" - }]}`, members[0], members[1], members[2]) - validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - out, err := clitestutil.ExecTestCLICmd(clientCtx, client.MsgCreateGroupCmd(), - append( - []string{ - val.Address.String(), - validMetadata, - validMembersFile.Name(), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - s.Require().NoError(s.network.WaitForNextBlock()) - - var txResp sdk.TxResponse - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txResp), out.String()) - txResp, err = clitestutil.GetTxResponse(s.network, val.ClientCtx, txResp.TxHash) - s.Require().NoError(err) - groupID := s.getGroupIDFromTxResponse(txResp) - - // create group policy - out, err = clitestutil.ExecTestCLICmd(clientCtx, client.MsgCreateGroupPolicyCmd(), - append( - []string{ - val.Address.String(), - groupID, - "AQ==", - testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"3", "windows":{"voting_period":"1s"}}`).Name(), - }, - s.commonFlags..., - ), - ) - s.Require().NoError(err, out.String()) - - err = s.network.RetryForBlocks(func() error { - out, err = clitestutil.ExecTestCLICmd(clientCtx, client.QueryGroupPoliciesByGroupCmd(), []string{groupID, fmt.Sprintf("--%s=json", flags.FlagOutput)}) - if err != nil { - return err - } - - var resp group.QueryGroupPoliciesByGroupResponse - err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp) - if err != nil { - return err - } - - if len(resp.GroupPolicies) != 1 { - return fmt.Errorf("expected 1 group policy, got %d", len(resp.GroupPolicies)) - } - - return nil - }, 3) - - s.Require().NoError(err, out.String()) - - testCases := []struct { - name string - args []string - expectErr bool - errMsg string - expectedCode uint32 - }{ - { - "invalid member address", - append( - []string{ - "address", - groupID, - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), - }, - s.commonFlags..., - ), - true, - "key not found", - 0, - }, - { - "group not found", - append( - []string{ - members[0], - "40", - fmt.Sprintf("--%s=%s", flags.FlagFrom, members[0]), - }, - s.commonFlags..., - ), - false, - "group: not found", - sdkerrors.ErrNotFound.ABCICode(), - }, - { - "valid case", - append( - []string{ - members[2], - groupID, - fmt.Sprintf("--%s=%s", flags.FlagFrom, members[2]), - }, - s.commonFlags..., - ), - false, - "", - 0, - }, - { - "not part of group", - append( - []string{ - members[2], - groupID, - fmt.Sprintf("--%s=%s", flags.FlagFrom, members[2]), - }, - s.commonFlags..., - ), - false, - "is not part of group", - sdkerrors.ErrNotFound.ABCICode(), - }, - { - "can leave group policy threshold is more than group weight", - append( - []string{ - members[1], - groupID, - fmt.Sprintf("--%s=%s", flags.FlagFrom, members[1]), - }, - s.commonFlags..., - ), - false, - "", - 0, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - cmd := client.MsgLeaveGroupCmd() - out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { - s.Require().Contains(out.String(), tc.errMsg) - } else { - s.Require().NoError(err, out.String()) - var resp sdk.TxResponse - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), &resp), out.String()) - s.Require().NoError(clitestutil.CheckTxCode(s.network, clientCtx, resp.TxHash, tc.expectedCode)) - } - }) - } -} - func (s *E2ETestSuite) TestExecProposalsWhenMemberLeavesOrIsUpdated() { val := s.network.Validators[0] clientCtx := val.ClientCtx @@ -2499,6 +408,21 @@ func (s *E2ETestSuite) TestExecProposalsWhenMemberLeavesOrIsUpdated() { } } +func (s *E2ETestSuite) getProposalIDFromTxResponse(txResp sdk.TxResponse) string { + s.Require().Greater(len(txResp.Logs), 0) + s.Require().NotNil(txResp.Logs[0].Events) + events := txResp.Logs[0].Events + createProposalEvent, _ := sdk.TypedEventToEvent(&group.EventSubmitProposal{}) + + for _, e := range events { + if e.Type == createProposalEvent.Type { + return strings.ReplaceAll(e.Attributes[0].Value, "\"", "") + } + } + + return "" +} + func (s *E2ETestSuite) getGroupIDFromTxResponse(txResp sdk.TxResponse) string { s.Require().Greater(len(txResp.Logs), 0) s.Require().NotNil(txResp.Logs[0].Events) @@ -2645,15 +569,20 @@ func (s *E2ETestSuite) createGroupThresholdPolicyWithBalance(adminAddress, group return groupPolicyAddress } -func (s *E2ETestSuite) newValidMembers(weights, membersAddress []string) group.MemberRequests { +func (s *E2ETestSuite) newValidMembers(weights, membersAddress []string) struct{ Members []group.MemberRequest } { s.Require().Equal(len(weights), len(membersAddress)) - membersValid := group.MemberRequests{} + membersValid := []group.MemberRequest{} for i, address := range membersAddress { - membersValid.Members = append(membersValid.Members, group.MemberRequest{ + membersValid = append(membersValid, group.MemberRequest{ Address: address, Weight: weights[i], Metadata: validMetadata, }) } - return membersValid + + return struct { + Members []group.MemberRequest + }{ + Members: membersValid, + } } diff --git a/x/authz/keeper/msg_server.go b/x/authz/keeper/msg_server.go index 6834d5d8b5..e3800897ca 100644 --- a/x/authz/keeper/msg_server.go +++ b/x/authz/keeper/msg_server.go @@ -5,6 +5,7 @@ import ( "errors" "strings" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/authz" @@ -120,14 +121,14 @@ func (k Keeper) Exec(goCtx context.Context, msg *authz.MsgExec) (*authz.MsgExecR } func validateMsgs(msgs []sdk.Msg) error { - for _, msg := range msgs { + for i, msg := range msgs { m, ok := msg.(sdk.HasValidateBasic) if !ok { continue } if err := m.ValidateBasic(); err != nil { - return err + return errorsmod.Wrapf(err, "msg %d", i) } } diff --git a/x/group/client/cli/tx.go b/x/group/client/cli/tx.go index 349e3c7479..172f64564b 100644 --- a/x/group/client/cli/tx.go +++ b/x/group/client/cli/tx.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "cosmossdk.io/core/address" "github.com/spf13/cobra" @@ -13,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/x/group" + "github.com/cosmos/cosmos-sdk/x/group/internal/math" ) const ( @@ -21,6 +23,8 @@ const ( FlagGroupPolicyAsAdmin = "group-policy-as-admin" ) +var errZeroGroupID = errors.New("group id cannot be 0") + // TxCmd returns a root CLI command handler for all x/group transaction commands. func TxCmd(name string, ac address.Codec) *cobra.Command { txCmd := &cobra.Command{ @@ -95,14 +99,17 @@ Where members.json contains: return err } + for _, member := range members { + if _, err := math.NewPositiveDecFromString(member.Weight); err != nil { + return fmt.Errorf("invalid weight %s for %s: weight must be positive", member.Weight, member.Address) + } + } + msg := &group.MsgCreateGroup{ Admin: clientCtx.GetFromAddress().String(), Members: members, Metadata: args[1], } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -157,19 +164,26 @@ Set a member's weight to "0" to delete it. return err } + for _, member := range members { + if _, err := math.NewNonNegativeDecFromString(member.Weight); err != nil { + return fmt.Errorf("invalid weight %s for %s: weight must not be negative", member.Weight, member.Address) + } + } + groupID, err := strconv.ParseUint(args[1], 10, 64) if err != nil { return err } + if groupID == 0 { + return errZeroGroupID + } + msg := &group.MsgUpdateGroupMembers{ Admin: clientCtx.GetFromAddress().String(), MemberUpdates: members, GroupId: groupID, } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -187,8 +201,7 @@ func MsgUpdateGroupAdminCmd() *cobra.Command { Short: "Update a group's admin", Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { - err := cmd.Flags().Set(flags.FlagFrom, args[0]) - if err != nil { + if err := cmd.Flags().Set(flags.FlagFrom, args[0]); err != nil { return err } @@ -202,14 +215,19 @@ func MsgUpdateGroupAdminCmd() *cobra.Command { return err } + if groupID == 0 { + return errZeroGroupID + } + + if strings.EqualFold(args[0], args[2]) { + return errors.New("new admin cannot be the same as the current admin") + } + msg := &group.MsgUpdateGroupAdmin{ Admin: clientCtx.GetFromAddress().String(), NewAdmin: args[2], GroupId: groupID, } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -242,14 +260,15 @@ func MsgUpdateGroupMetadataCmd() *cobra.Command { return err } + if groupID == 0 { + return errZeroGroupID + } + msg := &group.MsgUpdateGroupMetadata{ Admin: clientCtx.GetFromAddress().String(), Metadata: args[2], GroupId: groupID, } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -322,11 +341,21 @@ and policy.json contains: return err } + for _, member := range members { + if _, err := math.NewPositiveDecFromString(member.Weight); err != nil { + return fmt.Errorf("invalid weight %s for %s: weight must be positive", member.Weight, member.Address) + } + } + policy, err := parseDecisionPolicy(clientCtx.Codec, args[4]) if err != nil { return err } + if err := policy.ValidateBasic(); err != nil { + return err + } + msg, err := group.NewMsgCreateGroupWithPolicy( clientCtx.GetFromAddress().String(), members, @@ -339,10 +368,6 @@ and policy.json contains: return err } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } - return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } @@ -398,11 +423,19 @@ Here, we can use percentage decision policy when needed, where 0 < percentage <= return err } + if groupID == 0 { + return errZeroGroupID + } + policy, err := parseDecisionPolicy(clientCtx.Codec, args[3]) if err != nil { return err } + if err := policy.ValidateBasic(); err != nil { + return err + } + msg, err := group.NewMsgCreateGroupPolicy( clientCtx.GetFromAddress(), groupID, @@ -412,9 +445,6 @@ Here, we can use percentage decision policy when needed, where 0 < percentage <= if err != nil { return err } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -432,11 +462,14 @@ func MsgUpdateGroupPolicyAdminCmd() *cobra.Command { Short: "Update a group policy admin", Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { - err := cmd.Flags().Set(flags.FlagFrom, args[0]) - if err != nil { + if err := cmd.Flags().Set(flags.FlagFrom, args[0]); err != nil { return err } + if strings.EqualFold(args[0], args[2]) { + return errors.New("new admin cannot be the same as the current admin") + } + clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -447,9 +480,6 @@ func MsgUpdateGroupPolicyAdminCmd() *cobra.Command { GroupPolicyAddress: args[1], NewAdmin: args[2], } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -496,10 +526,6 @@ func MsgUpdateGroupPolicyDecisionPolicyCmd(ac address.Codec) *cobra.Command { return err } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } - return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } @@ -516,8 +542,7 @@ func MsgUpdateGroupPolicyMetadataCmd() *cobra.Command { Short: "Update a group policy metadata", Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { - err := cmd.Flags().Set(flags.FlagFrom, args[0]) - if err != nil { + if err := cmd.Flags().Set(flags.FlagFrom, args[0]); err != nil { return err } @@ -531,9 +556,6 @@ func MsgUpdateGroupPolicyMetadataCmd() *cobra.Command { GroupPolicyAddress: args[1], Metadata: args[2], } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -611,7 +633,6 @@ metadata example: } execStr, _ := cmd.Flags().GetString(FlagExec) - msg, err := group.NewMsgSubmitProposal( prop.GroupPolicyAddress, prop.Proposers, @@ -625,10 +646,6 @@ metadata example: return err } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } - return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } @@ -653,8 +670,7 @@ Parameters: `, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - err := cmd.Flags().Set(flags.FlagFrom, args[1]) - if err != nil { + if err := cmd.Flags().Set(flags.FlagFrom, args[1]); err != nil { return err } @@ -668,19 +684,15 @@ Parameters: return err } + if proposalID == 0 { + return fmt.Errorf("invalid proposal id: %d", proposalID) + } + msg := &group.MsgWithdrawProposal{ ProposalId: proposalID, Address: clientCtx.GetFromAddress().String(), } - if err != nil { - return err - } - - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } - return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } @@ -739,13 +751,6 @@ Parameters: Metadata: args[3], Exec: execFromString(execStr), } - if err != nil { - return err - } - - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -778,13 +783,6 @@ func MsgExecCmd() *cobra.Command { ProposalId: proposalID, Executor: clientCtx.GetFromAddress().String(), } - if err != nil { - return err - } - - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, @@ -820,13 +818,14 @@ Parameters: return err } + if groupID == 0 { + return errZeroGroupID + } + msg := &group.MsgLeaveGroup{ Address: clientCtx.GetFromAddress().String(), GroupId: groupID, } - if err = msg.ValidateBasic(); err != nil { - return fmt.Errorf("message validation failed: %w", err) - } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, diff --git a/x/group/client/cli/tx_test.go b/x/group/client/cli/tx_test.go index 7527b9db6f..d901340e4e 100644 --- a/x/group/client/cli/tx_test.go +++ b/x/group/client/cli/tx_test.go @@ -6,14 +6,11 @@ import ( "encoding/json" "fmt" "io" - "strconv" - "strings" "testing" sdkmath "cosmossdk.io/math" abci "github.com/cometbft/cometbft/abci/types" rpcclientmock "github.com/cometbft/cometbft/rpc/client/mock" - "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/client" @@ -31,9 +28,7 @@ import ( groupmodule "github.com/cosmos/cosmos-sdk/x/group/module" ) -const validMetadata = "metadata" - -var tooLongMetadata = strings.Repeat("A", 256) +var validMetadata = "metadata" type CLITestSuite struct { suite.Suite @@ -99,18 +94,17 @@ func (s *CLITestSuite) SetupSuite() { ) s.Require().NoError(err) - memberWeight := "3" // create a group validMembers := fmt.Sprintf(` { "members": [ { "address": "%s", - "weight": "%s", + "weight": "3", "metadata": "%s" } ] - }`, val.Address.String(), memberWeight, validMetadata) + }`, val.Address.String(), validMetadata) validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) out, err := clitestutil.ExecTestCLICmd(s.clientCtx, groupcli.MsgCreateGroupCmd(), append( @@ -144,12 +138,6 @@ func (s *CLITestSuite) TestTxCreateGroup() { }]}`, accounts[0].Address.String(), validMetadata) validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - invalidMembersAddress := `{"members": [{ - "address": "", - "weight": "1" - }]}` - invalidMembersAddressFile := testutil.WriteToNewTempFile(s.T(), invalidMembersAddress) - invalidMembersWeight := fmt.Sprintf(`{"members": [{ "address": "%s", "weight": "0" @@ -160,21 +148,19 @@ func (s *CLITestSuite) TestTxCreateGroup() { name string ctxGen func() client.Context args []string - respType proto.Message expCmdOutput string - expectErr bool expectErrMsg string }{ { - "correct data", - func() client.Context { + name: "correct data", + ctxGen: func() client.Context { bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ Value: bz, }) return s.baseCtx.WithClient(c) }, - append( + args: append( []string{ accounts[0].Address.String(), "", @@ -182,10 +168,8 @@ func (s *CLITestSuite) TestTxCreateGroup() { }, s.commonFlags..., ), - &sdk.TxResponse{}, - fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "", validMembersFile.Name()), - false, - "", + expCmdOutput: fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "", validMembersFile.Name()), + expectErrMsg: "", }, { "with amino-json", @@ -205,33 +189,9 @@ func (s *CLITestSuite) TestTxCreateGroup() { }, s.commonFlags..., ), - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "", validMembersFile.Name()), - false, "", }, - { - "invalid members address", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - accounts[0].Address.String(), - "null", - invalidMembersAddressFile.Name(), - }, - s.commonFlags..., - ), - &sdk.TxResponse{}, - fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "null", invalidMembersAddressFile.Name()), - true, - "message validation failed: address: empty address string is not allowed", - }, { "invalid members weight", func() client.Context { @@ -249,10 +209,28 @@ func (s *CLITestSuite) TestTxCreateGroup() { }, s.commonFlags..., ), - &sdk.TxResponse{}, - fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "null", invalidMembersWeightFile.Name()), - true, - "expected a positive decimal, got 0: invalid decimal string", + "", + "weight must be positive", + }, + { + "no member provided", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + append( + []string{ + accounts[0].Address.String(), + "null", + "doesnotexist.json", + }, + s.commonFlags..., + ), + "", + "no such file or directory", }, } @@ -275,12 +253,13 @@ func (s *CLITestSuite) TestTxCreateGroup() { } out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { + if tc.expectErrMsg != "" { s.Require().Error(err) s.Require().Contains(out.String(), tc.expectErrMsg) } else { s.Require().NoError(err) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) } }) } @@ -334,7 +313,6 @@ func (s *CLITestSuite) TestTxUpdateGroupAdmin() { name string ctxGen func() client.Context args []string - respType proto.Message expCmdOutput string expectErr bool expectErrMsg string @@ -356,7 +334,6 @@ func (s *CLITestSuite) TestTxUpdateGroupAdmin() { }, s.commonFlags..., ), - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s", accounts[0].Address.String(), groupIDs[0], accounts[1].Address.String()), false, "", @@ -379,7 +356,6 @@ func (s *CLITestSuite) TestTxUpdateGroupAdmin() { }, s.commonFlags..., ), - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s --%s=%s", accounts[0].Address.String(), groupIDs[1], accounts[1].Address.String(), flags.FlagSignMode, flags.SignModeLegacyAminoJSON), false, "", @@ -401,7 +377,6 @@ func (s *CLITestSuite) TestTxUpdateGroupAdmin() { }, s.commonFlags..., ), - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "", accounts[1].Address.String()), true, "strconv.ParseUint: parsing \"\": invalid syntax", @@ -432,7 +407,8 @@ func (s *CLITestSuite) TestTxUpdateGroupAdmin() { s.Require().Contains(out.String(), tc.expectErrMsg) } else { s.Require().NoError(err) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) } }) } @@ -449,6 +425,7 @@ func (s *CLITestSuite) TestTxUpdateGroupMetadata() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -468,6 +445,7 @@ func (s *CLITestSuite) TestTxUpdateGroupMetadata() { s.commonFlags..., ), fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "1", validMetadata), + "", }, { "with amino-json", @@ -488,9 +466,10 @@ func (s *CLITestSuite) TestTxUpdateGroupMetadata() { s.commonFlags..., ), fmt.Sprintf("%s %s %s --%s=%s", accounts[0].Address.String(), "1", validMetadata, flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + "", }, { - "group metadata too long", + "invalid group id", func() client.Context { bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ @@ -501,12 +480,33 @@ func (s *CLITestSuite) TestTxUpdateGroupMetadata() { append( []string{ accounts[0].Address.String(), - strconv.FormatUint(s.group.Id, 10), - strings.Repeat("a", 256), + "abc", + validMetadata, }, s.commonFlags..., ), - fmt.Sprintf("%s %s %s", accounts[0].Address.String(), strconv.FormatUint(s.group.Id, 10), strings.Repeat("a", 256)), + fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "abc", validMetadata), + "Error: strconv.ParseUint: parsing \"abc\"", + }, + { + "empty group id", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + append( + []string{ + accounts[0].Address.String(), + "0", + validMetadata, + }, + s.commonFlags..., + ), + fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "0", validMetadata), + "group id cannot be 0", }, } @@ -527,6 +527,16 @@ func (s *CLITestSuite) TestTxUpdateGroupMetadata() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -552,9 +562,9 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { invalidMembersMetadata := fmt.Sprintf(`{"members": [{ "address": "%s", - "weight": "1", - "metadata": "%s" - }]}`, accounts[1], tooLongMetadata) + "weight": "-1", + "metadata": "foo" + }]}`, accounts[1]) invalidMembersMetadataFileName := testutil.WriteToNewTempFile(s.T(), invalidMembersMetadata).Name() testCases := []struct { @@ -562,6 +572,7 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -581,6 +592,7 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { s.commonFlags..., ), fmt.Sprintf("%s %s %s", accounts[0].Address.String(), groupID, validUpdatedMembersFileName), + "", }, { "with amino-json", @@ -601,9 +613,30 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { s.commonFlags..., ), fmt.Sprintf("%s %s %s --%s=%s", accounts[0].Address.String(), groupID, validUpdatedMembersFileName, flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + "", }, { - "group member metadata too long", + "group id invalid", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + append( + []string{ + accounts[0].Address.String(), + "0", + validUpdatedMembersFileName, + }, + s.commonFlags..., + ), + fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "0", validUpdatedMembersFileName), + "group id cannot be 0", + }, + { + "group member weight invalid", func() client.Context { bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ @@ -620,25 +653,7 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { s.commonFlags..., ), fmt.Sprintf("%s %s %s", accounts[0].Address.String(), groupID, invalidMembersMetadataFileName), - }, - { - "group doesn't exist", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - accounts[0].Address.String(), - "12345", - validUpdatedMembersFileName, - }, - s.commonFlags..., - ), - fmt.Sprintf("%s %s %s", accounts[0].Address.String(), "12345", validUpdatedMembersFileName), + "invalid weight -1", }, } @@ -659,6 +674,16 @@ func (s *CLITestSuite) TestTxUpdateGroupMembers() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -676,12 +701,6 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { }]}`, accounts[0].Address.String(), validMetadata) validMembersFile := testutil.WriteToNewTempFile(s.T(), validMembers) - invalidMembersAddress := `{"members": [{ - "address": "", - "weight": "1" - }]}` - invalidMembersAddressFile := testutil.WriteToNewTempFile(s.T(), invalidMembersAddress) - invalidMembersWeight := fmt.Sprintf(`{"members": [{ "address": "%s", "weight": "0" @@ -694,9 +713,7 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { name string ctxGen func() client.Context args []string - expectErr bool expectErrMsg string - respType proto.Message expCmdOutput string }{ { @@ -719,9 +736,7 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s %s --%s=%v", accounts[0].Address.String(), validMetadata, validMetadata, validMembersFile.Name(), thresholdDecisionPolicyFile.Name(), groupcli.FlagGroupPolicyAsAdmin, false), }, { @@ -744,9 +759,7 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s %s --%s=%v", accounts[0].Address.String(), validMetadata, validMetadata, validMembersFile.Name(), thresholdDecisionPolicyFile.Name(), groupcli.FlagGroupPolicyAsAdmin, true), }, { @@ -770,36 +783,9 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s %s --%s=%v --%s=%s", accounts[0].Address.String(), validMetadata, validMetadata, validMembersFile.Name(), thresholdDecisionPolicyFile.Name(), groupcli.FlagGroupPolicyAsAdmin, false, flags.FlagSignMode, flags.SignModeLegacyAminoJSON), }, - { - "invalid members address", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - accounts[0].Address.String(), - validMetadata, - validMetadata, - invalidMembersAddressFile.Name(), - thresholdDecisionPolicyFile.Name(), - fmt.Sprintf("--%s=%v", groupcli.FlagGroupPolicyAsAdmin, false), - }, - s.commonFlags..., - ), - true, - "message validation failed: address: empty address string is not allowed", - nil, - fmt.Sprintf("%s %s %s %s %s --%s=%v", accounts[0].Address.String(), validMetadata, validMetadata, invalidMembersAddressFile.Name(), thresholdDecisionPolicyFile.Name(), groupcli.FlagGroupPolicyAsAdmin, false), - }, { "invalid members weight", func() client.Context { @@ -820,9 +806,7 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { }, s.commonFlags..., ), - true, - "expected a positive decimal, got 0: invalid decimal string", - nil, + "weight must be positive", fmt.Sprintf("%s %s %s %s %s --%s=%v", accounts[0].Address.String(), validMetadata, validMetadata, invalidMembersWeightFile.Name(), thresholdDecisionPolicyFile.Name(), groupcli.FlagGroupPolicyAsAdmin, false), }, } @@ -845,12 +829,13 @@ func (s *CLITestSuite) TestTxCreateGroupWithPolicy() { } out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { + if tc.expectErrMsg != "" { s.Require().Error(err) s.Require().Contains(out.String(), tc.expectErrMsg) } else { s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) } }) } @@ -875,9 +860,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { name string ctxGen func() client.Context args []string - expectErr bool expectErrMsg string - respType proto.Message expCmdOutput string }{ { @@ -898,9 +881,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s", val.Address.String(), fmt.Sprintf("%v", groupID), validMetadata, thresholdDecisionPolicyFile.Name()), }, { @@ -921,9 +902,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s", val.Address.String(), fmt.Sprintf("%v", groupID), validMetadata, percentageDecisionPolicyFile.Name()), }, { @@ -945,9 +924,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - false, "", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s --%s=%s", val.Address.String(), fmt.Sprintf("%v", groupID), validMetadata, thresholdDecisionPolicyFile.Name(), flags.FlagSignMode, flags.SignModeLegacyAminoJSON), }, { @@ -968,9 +945,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - true, "key not found", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s", "wrongAdmin", fmt.Sprintf("%v", groupID), validMetadata, thresholdDecisionPolicyFile.Name()), }, { @@ -991,9 +966,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - true, "expected a positive decimal", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s", val.Address.String(), fmt.Sprintf("%v", groupID), validMetadata, invalidNegativePercentageDecisionPolicyFile.Name()), }, { @@ -1014,9 +987,7 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { }, s.commonFlags..., ), - true, "percentage must be > 0 and <= 1", - &sdk.TxResponse{}, fmt.Sprintf("%s %s %s %s", val.Address.String(), fmt.Sprintf("%v", groupID), validMetadata, invalidPercentageDecisionPolicyFile.Name()), }, } @@ -1040,12 +1011,13 @@ func (s *CLITestSuite) TestTxCreateGroupPolicy() { } out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) - if tc.expectErr { + if tc.expectErrMsg != "" { s.Require().Error(err) s.Require().Contains(out.String(), tc.expectErrMsg) } else { s.Require().NoError(err, out.String()) - s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) } }) } @@ -1068,6 +1040,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -1087,6 +1060,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { commonFlags..., ), fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), newAdmin.Address.String()), + "", }, { "with amino-json", @@ -1107,6 +1081,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { commonFlags..., ), fmt.Sprintf("%s %s %s --%s=%s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), newAdmin.Address.String(), flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + "", }, { "wrong admin", @@ -1126,9 +1101,10 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { commonFlags..., ), fmt.Sprintf("%s %s %s", "wrong admin", groupPolicyAddress.Address.String(), newAdmin.Address.String()), + "key not found", }, { - "wrong group policy", + "identical admin and new admin", func() client.Context { bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ @@ -1139,12 +1115,13 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { append( []string{ groupPolicyAdmin.Address.String(), - "wrong group policy", - newAdmin.Address.String(), + groupPolicyAddress.Address.String(), + groupPolicyAdmin.Address.String(), }, commonFlags..., ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), "wrong group policy", newAdmin.Address.String()), + fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), groupPolicyAdmin.Address.String()), + "new admin cannot be the same as the current admin", }, } @@ -1165,6 +1142,16 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyAdmin() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -1180,8 +1167,6 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { thresholdDecisionPolicy := testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.ThresholdDecisionPolicy", "threshold":"1", "windows":{"voting_period":"40000s"}}`) percentageDecisionPolicy := testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"0.5", "windows":{"voting_period":"40000s"}}`) - invalidNegativePercentageDecisionPolicy := testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"-0.5", "windows":{"voting_period":"1s"}}`) - invalidPercentageDecisionPolicy := testutil.WriteToNewTempFile(s.T(), `{"@type":"/cosmos.group.v1.PercentageDecisionPolicy", "percentage":"2", "windows":{"voting_period":"40000s"}}`) cmd := groupcli.MsgUpdateGroupPolicyDecisionPolicyCmd(address.NewBech32Codec("cosmos")) cmd.SetOutput(io.Discard) @@ -1191,6 +1176,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -1210,6 +1196,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { commonFlags..., ), fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), thresholdDecisionPolicy.Name()), + "", }, { "correct data with percentage decision policy", @@ -1229,6 +1216,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { commonFlags..., ), fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), percentageDecisionPolicy.Name()), + "", }, { "with amino-json", @@ -1249,6 +1237,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { commonFlags..., ), fmt.Sprintf("%s %s %s --%s=%s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), thresholdDecisionPolicy.Name(), flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + "", }, { "wrong admin", @@ -1262,69 +1251,13 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { append( []string{ newAdmin.Address.String(), - groupPolicyAddress.Address.String(), + "invalid", thresholdDecisionPolicy.Name(), }, commonFlags..., ), - fmt.Sprintf("%s %s %s", newAdmin.Address.String(), groupPolicyAddress.Address.String(), thresholdDecisionPolicy.Name()), - }, - { - "wrong group policy", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - groupPolicyAdmin.Address.String(), - "wrong group policy", - thresholdDecisionPolicy.Name(), - }, - commonFlags..., - ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), "wrong group policy", thresholdDecisionPolicy.Name()), - }, - { - "invalid percentage decision policy with negative value", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - groupPolicyAdmin.Address.String(), - groupPolicyAddress.Address.String(), - invalidNegativePercentageDecisionPolicy.Name(), - }, - commonFlags..., - ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), invalidNegativePercentageDecisionPolicy.Name()), - }, - { - "invalid percentage decision policy with value greater than 1", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - groupPolicyAdmin.Address.String(), - groupPolicyAddress.Address.String(), - invalidPercentageDecisionPolicy.Name(), - }, - commonFlags..., - ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.Address.String(), groupPolicyAddress.Address.String(), invalidPercentageDecisionPolicy.Name()), + fmt.Sprintf("%s %s %s", newAdmin.Address.String(), "invalid", thresholdDecisionPolicy.Name()), + "decoding bech32 failed", }, } @@ -1345,6 +1278,16 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyDecisionPolicy() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -1365,6 +1308,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyMetadata() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -1384,6 +1328,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyMetadata() { commonFlags..., ), fmt.Sprintf("%s %s %s", groupPolicyAdmin.String(), groupPolicyAddress.String(), validMetadata), + "", }, { "with amino-json", @@ -1404,25 +1349,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyMetadata() { commonFlags..., ), fmt.Sprintf("%s %s %s --%s=%s", groupPolicyAdmin.String(), groupPolicyAddress.String(), validMetadata, flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - { - "long metadata", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - groupPolicyAdmin.String(), - groupPolicyAddress.String(), - strings.Repeat("a", 500), - }, - commonFlags..., - ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.String(), groupPolicyAddress.String(), strings.Repeat("a", 500)), + "", }, { "wrong admin", @@ -1442,25 +1369,7 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyMetadata() { commonFlags..., ), fmt.Sprintf("%s %s %s", "wrong admin", groupPolicyAddress.String(), validMetadata), - }, - { - "wrong group policy", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - groupPolicyAdmin.String(), - "wrong group policy", - validMetadata, - }, - commonFlags..., - ), - fmt.Sprintf("%s %s %s", groupPolicyAdmin.String(), "wrong group policy", validMetadata), + "key not found", }, } @@ -1481,6 +1390,16 @@ func (s *CLITestSuite) TestTxUpdateGroupPolicyMetadata() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -1507,6 +1426,7 @@ func (s *CLITestSuite) TestTxSubmitProposal() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -1524,6 +1444,7 @@ func (s *CLITestSuite) TestTxSubmitProposal() { s.commonFlags..., ), proposalFile.Name(), + "", }, { "with try exec", @@ -1542,6 +1463,7 @@ func (s *CLITestSuite) TestTxSubmitProposal() { s.commonFlags..., ), fmt.Sprintf("%s --%s=try", proposalFile.Name(), groupcli.FlagExec), + "", }, { "with try exec, not enough yes votes for proposal to pass", @@ -1560,6 +1482,7 @@ func (s *CLITestSuite) TestTxSubmitProposal() { s.commonFlags..., ), fmt.Sprintf("%s --%s=try", proposalFile.Name(), groupcli.FlagExec), + "", }, { "with amino-json", @@ -1578,6 +1501,7 @@ func (s *CLITestSuite) TestTxSubmitProposal() { s.commonFlags..., ), fmt.Sprintf("%s --%s=%s", proposalFile.Name(), flags.FlagSignMode, flags.SignModeLegacyAminoJSON), + "", }, } @@ -1598,6 +1522,16 @@ func (s *CLITestSuite) TestTxSubmitProposal() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -1618,7 +1552,29 @@ func (s *CLITestSuite) TestTxVote() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ + { + "invalid vote", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + append( + []string{ + ids[0], + accounts[0].Address.String(), + "AYE", + "", + }, + s.commonFlags..., + ), + fmt.Sprintf("%s %s %s", ids[0], accounts[0].Address.String(), "AYE"), + "Error: 'AYE' is not a valid vote option", + }, { "correct data", func() client.Context { @@ -1638,6 +1594,7 @@ func (s *CLITestSuite) TestTxVote() { s.commonFlags..., ), fmt.Sprintf("%s %s %s", ids[0], accounts[0].Address.String(), "VOTE_OPTION_YES"), + "", }, { "with try exec", @@ -1659,6 +1616,7 @@ func (s *CLITestSuite) TestTxVote() { s.commonFlags..., ), fmt.Sprintf("%s %s %s %s --%s=try", ids[1], accounts[0].Address.String(), "VOTE_OPTION_YES", "", groupcli.FlagExec), + "", }, { "with amino-json", @@ -1680,26 +1638,7 @@ func (s *CLITestSuite) TestTxVote() { s.commonFlags..., ), fmt.Sprintf("%s %s %s %s --%s=%s", ids[3], accounts[0].Address.String(), "VOTE_OPTION_YES", "", flags.FlagSignMode, flags.SignModeLegacyAminoJSON), - }, - { - "metadata too long", - func() client.Context { - bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) - c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ - Value: bz, - }) - return s.baseCtx.WithClient(c) - }, - append( - []string{ - ids[2], - accounts[0].Address.String(), - "VOTE_OPTION_YES", - tooLongMetadata, - }, - s.commonFlags..., - ), - fmt.Sprintf("%s %s %s %s", ids[2], accounts[0].Address.String(), "VOTE_OPTION_YES", tooLongMetadata), + "", }, } @@ -1720,6 +1659,16 @@ func (s *CLITestSuite) TestTxVote() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } @@ -1739,6 +1688,7 @@ func (s *CLITestSuite) TestTxWithdrawProposal() { ctxGen func() client.Context args []string expCmdOutput string + expectErrMsg string }{ { "correct data", @@ -1757,6 +1707,7 @@ func (s *CLITestSuite) TestTxWithdrawProposal() { s.commonFlags..., ), fmt.Sprintf("%s %s", ids[0], accounts[0].Address.String()), + "", }, { "wrong admin", @@ -1775,6 +1726,26 @@ func (s *CLITestSuite) TestTxWithdrawProposal() { s.commonFlags..., ), fmt.Sprintf("%s %s", ids[1], "wrongAdmin"), + "key not found", + }, + { + "wrong proposal id", + func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockCometRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + }, + append( + []string{ + "abc", + accounts[0].Address.String(), + }, + s.commonFlags..., + ), + fmt.Sprintf("%s %s", "abc", accounts[0].Address.String()), + "Error: strconv.ParseUint: parsing \"abc\"", }, } @@ -1783,7 +1754,6 @@ func (s *CLITestSuite) TestTxWithdrawProposal() { s.Run(tc.name, func() { var outBuf bytes.Buffer - clientCtx := tc.ctxGen().WithOutput(&outBuf) ctx := svrcmd.CreateExecuteContext(context.Background()) @@ -1795,6 +1765,16 @@ func (s *CLITestSuite) TestTxWithdrawProposal() { if len(tc.args) != 0 { s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput) } + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args) + if tc.expectErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(out.String(), tc.expectErrMsg) + } else { + s.Require().NoError(err) + msg := &sdk.TxResponse{} + s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), msg), out.String()) + } }) } } diff --git a/x/group/client/cli/util.go b/x/group/client/cli/util.go index 7bd2bdb3b5..65ad49425a 100644 --- a/x/group/client/cli/util.go +++ b/x/group/client/cli/util.go @@ -31,7 +31,9 @@ func parseDecisionPolicy(cdc codec.Codec, decisionPolicyFile string) (group.Deci // parseMembers reads and parses the members. func parseMembers(membersFile string) ([]group.MemberRequest, error) { - members := group.MemberRequests{} + members := struct { + Members []group.MemberRequest `json:"members"` + }{} if membersFile == "" { return members.Members, nil @@ -42,8 +44,7 @@ func parseMembers(membersFile string) ([]group.MemberRequest, error) { return nil, err } - err = json.Unmarshal(contents, &members) - if err != nil { + if err := json.Unmarshal(contents, &members); err != nil { return nil, err } diff --git a/x/group/keeper/keeper_test.go b/x/group/keeper/keeper_test.go index 53d95b84a4..e57089e2cb 100644 --- a/x/group/keeper/keeper_test.go +++ b/x/group/keeper/keeper_test.go @@ -1,13 +1,9 @@ package keeper_test import ( - "bytes" "context" "encoding/binary" "errors" - "fmt" - "sort" - "strings" "testing" "time" @@ -22,14 +18,12 @@ import ( "github.com/cosmos/cosmos-sdk/codec/address" "github.com/cosmos/cosmos-sdk/testutil" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" - "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/bank" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/group" - "github.com/cosmos/cosmos-sdk/x/group/internal/math" "github.com/cosmos/cosmos-sdk/x/group/keeper" "github.com/cosmos/cosmos-sdk/x/group/module" grouptestutil "github.com/cosmos/cosmos-sdk/x/group/testutil" @@ -72,6 +66,7 @@ func (s *TestSuite) SetupTest() { // add empty string to the list of expected calls s.accountKeeper.EXPECT().StringToBytes("").Return(nil, errors.New("unable to decode")).AnyTimes() + s.accountKeeper.EXPECT().StringToBytes("invalid").Return(nil, errors.New("unable to decode")).AnyTimes() s.bankKeeper = grouptestutil.NewMockBankKeeper(ctrl) @@ -163,2673 +158,6 @@ func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(TestSuite)) } -func (s *TestSuite) TestCreateGroupWithLotsOfMembers() { - for i := 50; i < 70; i++ { - membersResp := s.createGroupAndGetMembers(i) - s.Require().Equal(len(membersResp), i) - } -} - -func (s *TestSuite) createGroupAndGetMembers(numMembers int) []*group.GroupMember { - addressPool := simtestutil.CreateIncrementalAccounts(numMembers) - members := make([]group.MemberRequest, numMembers) - for i := 0; i < len(members); i++ { - members[i] = group.MemberRequest{ - Address: addressPool[i].String(), - Weight: "1", - } - s.accountKeeper.EXPECT().StringToBytes(addressPool[i].String()).Return(addressPool[i].Bytes(), nil).AnyTimes() - s.accountKeeper.EXPECT().BytesToString(addressPool[i].Bytes()).Return(addressPool[i].String(), nil).AnyTimes() - } - - g, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: members[0].Address, - Members: members, - }) - s.Require().NoErrorf(err, "failed to create group with %d members", len(members)) - s.T().Logf("group %d created with %d members", g.GroupId, len(members)) - - groupMemberResp, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: g.GroupId}) - s.Require().NoError(err) - - s.T().Logf("got %d members from group %d", len(groupMemberResp.Members), g.GroupId) - - return groupMemberResp.Members -} - -func (s *TestSuite) TestCreateGroup() { - addrs := s.addrs - addr1 := addrs[0] - addr3 := addrs[2] - addr5 := addrs[4] - addr6 := addrs[5] - - members := []group.MemberRequest{{ - Address: addr5.String(), - Weight: "1", - }, { - Address: addr6.String(), - Weight: "2", - }} - - expGroups := []*group.GroupInfo{ - { - Id: s.groupID, - Version: 1, - Admin: addr1.String(), - TotalWeight: "3", - CreatedAt: s.blockTime, - }, - { - Id: 2, - Version: 1, - Admin: addr1.String(), - TotalWeight: "3", - CreatedAt: s.blockTime, - }, - } - - specs := map[string]struct { - req *group.MsgCreateGroup - expErr bool - expGroups []*group.GroupInfo - }{ - "all good": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: members, - }, - expGroups: expGroups, - }, - "group metadata too long": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: members, - Metadata: strings.Repeat("a", 256), - }, - expErr: true, - }, - "member metadata too long": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "1", - Metadata: strings.Repeat("a", 256), - }}, - }, - expErr: true, - }, - "zero member weight": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "0", - }}, - }, - expErr: true, - }, - "invalid member weight - Inf": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "inf", - }}, - }, - expErr: true, - }, - "invalid member weight - NaN": { - req: &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "NaN", - }}, - }, - expErr: true, - }, - } - - var seq uint32 = 1 - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() - res, err := s.groupKeeper.CreateGroup(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - _, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: uint64(seq + 1)}) - s.Require().Error(err) - return - } - s.Require().NoError(err) - id := res.GroupId - - seq++ - s.Assert().Equal(uint64(seq), id) - - // then all data persisted - loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id}) - s.Require().NoError(err) - s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin) - s.Assert().Equal(spec.req.Metadata, loadedGroupRes.Info.Metadata) - s.Assert().Equal(id, loadedGroupRes.Info.Id) - s.Assert().Equal(uint64(1), loadedGroupRes.Info.Version) - - // and members are stored as well - membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id}) - s.Require().NoError(err) - loadedMembers := membersRes.Members - s.Require().Equal(len(members), len(loadedMembers)) - // we reorder members by address to be able to compare them - sort.Slice(members, func(i, j int) bool { - addri, err := s.accountKeeper.StringToBytes(members[i].Address) - s.Require().NoError(err) - addrj, err := s.accountKeeper.StringToBytes(members[j].Address) - s.Require().NoError(err) - return bytes.Compare(addri, addrj) < 0 - }) - for i := range loadedMembers { - s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata) - s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address) - s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight) - s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt) - s.Assert().Equal(id, loadedMembers[i].GroupId) - } - - // query groups by admin - groupsRes, err := s.groupKeeper.GroupsByAdmin(s.ctx, &group.QueryGroupsByAdminRequest{Admin: addr1.String()}) - s.Require().NoError(err) - loadedGroups := groupsRes.Groups - s.Require().Equal(len(spec.expGroups), len(loadedGroups)) - for i := range loadedGroups { - s.Assert().Equal(spec.expGroups[i].Metadata, loadedGroups[i].Metadata) - s.Assert().Equal(spec.expGroups[i].Admin, loadedGroups[i].Admin) - s.Assert().Equal(spec.expGroups[i].TotalWeight, loadedGroups[i].TotalWeight) - s.Assert().Equal(spec.expGroups[i].Id, loadedGroups[i].Id) - s.Assert().Equal(spec.expGroups[i].Version, loadedGroups[i].Version) - s.Assert().Equal(spec.expGroups[i].CreatedAt, loadedGroups[i].CreatedAt) - } - }) - } -} - -func (s *TestSuite) TestUpdateGroupAdmin() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] - addr3 := addrs[2] - addr4 := addrs[3] - - members := []group.MemberRequest{{ - Address: addr1.String(), - Weight: "1", - }} - oldAdmin := addr2.String() - newAdmin := addr3.String() - groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: oldAdmin, - Members: members, - }) - s.Require().NoError(err) - groupID := groupRes.GroupId - specs := map[string]struct { - req *group.MsgUpdateGroupAdmin - expStored *group.GroupInfo - expErr bool - }{ - "with correct admin": { - req: &group.MsgUpdateGroupAdmin{ - GroupId: groupID, - Admin: oldAdmin, - NewAdmin: newAdmin, - }, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: newAdmin, - TotalWeight: "1", - Version: 2, - CreatedAt: s.blockTime, - }, - }, - "with wrong admin": { - req: &group.MsgUpdateGroupAdmin{ - GroupId: groupID, - Admin: addr4.String(), - NewAdmin: newAdmin, - }, - expErr: true, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: oldAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - }, - "with unknown groupID": { - req: &group.MsgUpdateGroupAdmin{ - GroupId: 999, - Admin: oldAdmin, - NewAdmin: newAdmin, - }, - expErr: true, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: oldAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - _, err := s.groupKeeper.UpdateGroupAdmin(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - // then - res, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: groupID}) - s.Require().NoError(err) - s.Assert().Equal(spec.expStored, res.Info) - }) - } -} - -func (s *TestSuite) TestUpdateGroupMetadata() { - addrs := s.addrs - addr1 := addrs[0] - addr3 := addrs[2] - - oldAdmin := addr1.String() - groupID := s.groupID - - specs := map[string]struct { - req *group.MsgUpdateGroupMetadata - expErr bool - expStored *group.GroupInfo - }{ - "with correct admin": { - req: &group.MsgUpdateGroupMetadata{ - GroupId: groupID, - Admin: oldAdmin, - }, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: oldAdmin, - TotalWeight: "3", - Version: 2, - CreatedAt: s.blockTime, - }, - }, - "with wrong admin": { - req: &group.MsgUpdateGroupMetadata{ - GroupId: groupID, - Admin: addr3.String(), - }, - expErr: true, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: oldAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - }, - "with unknown groupid": { - req: &group.MsgUpdateGroupMetadata{ - GroupId: 999, - Admin: oldAdmin, - }, - expErr: true, - expStored: &group.GroupInfo{ - Id: groupID, - Admin: oldAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - sdkCtx, _ := s.sdkCtx.CacheContext() - _, err := s.groupKeeper.UpdateGroupMetadata(sdkCtx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - // then - res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) - s.Require().NoError(err) - s.Assert().Equal(spec.expStored, res.Info) - - events := sdkCtx.EventManager().ABCIEvents() - s.Require().Len(events, 1) // EventUpdateGroup - }) - } -} - -func (s *TestSuite) TestUpdateGroupMembers() { - addrs := s.addrs - addr3 := addrs[2] - addr4 := addrs[3] - addr5 := addrs[4] - addr6 := addrs[5] - - member1 := addr5.String() - member2 := addr6.String() - members := []group.MemberRequest{{ - Address: member1, - Weight: "1", - }} - - myAdmin := addr4.String() - groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: myAdmin, - Members: members, - }) - s.Require().NoError(err) - groupID := groupRes.GroupId - - specs := map[string]struct { - req *group.MsgUpdateGroupMembers - expErr bool - expGroup *group.GroupInfo - expMembers []*group.GroupMember - }{ - "add new member": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: member2, - Weight: "2", - }}, - }, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "3", - Version: 2, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{ - { - Member: &group.Member{ - Address: member2, - Weight: "2", - AddedAt: s.sdkCtx.BlockTime(), - }, - GroupId: groupID, - }, - { - Member: &group.Member{ - Address: member1, - Weight: "1", - AddedAt: s.blockTime, - }, - GroupId: groupID, - }, - }, - }, - "update member": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: member1, - Weight: "2", - }}, - }, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "2", - Version: 2, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{ - { - GroupId: groupID, - Member: &group.Member{ - Address: member1, - Weight: "2", - AddedAt: s.blockTime, - }, - }, - }, - }, - "update member with same data": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: member1, - Weight: "1", - }}, - }, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "1", - Version: 2, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{ - { - GroupId: groupID, - Member: &group.Member{ - Address: member1, - Weight: "1", - AddedAt: s.blockTime, - }, - }, - }, - }, - "replace member": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{ - { - Address: member1, - Weight: "0", - }, - { - Address: member2, - Weight: "1", - }, - }, - }, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "1", - Version: 2, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{{ - GroupId: groupID, - Member: &group.Member{ - Address: member2, - Weight: "1", - AddedAt: s.sdkCtx.BlockTime(), - }, - }}, - }, - "remove existing member": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: member1, - Weight: "0", - }}, - }, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "0", - Version: 2, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{}, - }, - "remove unknown member": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: addr4.String(), - Weight: "0", - }}, - }, - expErr: true, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{{ - GroupId: groupID, - Member: &group.Member{ - Address: member1, - Weight: "1", - }, - }}, - }, - "with wrong admin": { - req: &group.MsgUpdateGroupMembers{ - GroupId: groupID, - Admin: addr3.String(), - MemberUpdates: []group.MemberRequest{{ - Address: member1, - Weight: "2", - }}, - }, - expErr: true, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{{ - GroupId: groupID, - Member: &group.Member{ - Address: member1, - Weight: "1", - }, - }}, - }, - "with unknown groupID": { - req: &group.MsgUpdateGroupMembers{ - GroupId: 999, - Admin: myAdmin, - MemberUpdates: []group.MemberRequest{{ - Address: member1, - Weight: "2", - }}, - }, - expErr: true, - expGroup: &group.GroupInfo{ - Id: groupID, - Admin: myAdmin, - TotalWeight: "1", - Version: 1, - CreatedAt: s.blockTime, - }, - expMembers: []*group.GroupMember{{ - GroupId: groupID, - Member: &group.Member{ - Address: member1, - Weight: "1", - }, - }}, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - sdkCtx, _ := s.sdkCtx.CacheContext() - _, err := s.groupKeeper.UpdateGroupMembers(sdkCtx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - // then - res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) - s.Require().NoError(err) - s.Assert().Equal(spec.expGroup, res.Info) - - // and members persisted - membersRes, err := s.groupKeeper.GroupMembers(sdkCtx, &group.QueryGroupMembersRequest{GroupId: groupID}) - s.Require().NoError(err) - loadedMembers := membersRes.Members - s.Require().Equal(len(spec.expMembers), len(loadedMembers)) - // we reorder group members by address to be able to compare them - sort.Slice(spec.expMembers, func(i, j int) bool { - addri, err := s.accountKeeper.StringToBytes(spec.expMembers[i].Member.Address) - s.Require().NoError(err) - addrj, err := s.accountKeeper.StringToBytes(spec.expMembers[j].Member.Address) - s.Require().NoError(err) - return bytes.Compare(addri, addrj) < 0 - }) - for i := range loadedMembers { - s.Assert().Equal(spec.expMembers[i].Member.Metadata, loadedMembers[i].Member.Metadata) - s.Assert().Equal(spec.expMembers[i].Member.Address, loadedMembers[i].Member.Address) - s.Assert().Equal(spec.expMembers[i].Member.Weight, loadedMembers[i].Member.Weight) - s.Assert().Equal(spec.expMembers[i].Member.AddedAt, loadedMembers[i].Member.AddedAt) - s.Assert().Equal(spec.expMembers[i].GroupId, loadedMembers[i].GroupId) - } - - events := sdkCtx.EventManager().ABCIEvents() - s.Require().Len(events, 1) // EventUpdateGroup - }) - } -} - -func (s *TestSuite) TestCreateGroupWithPolicy() { - addrs := s.addrs - addr1 := addrs[0] - addr3 := addrs[2] - addr5 := addrs[4] - addr6 := addrs[5] - - s.setNextAccount() - - members := []group.MemberRequest{{ - Address: addr5.String(), - Weight: "1", - }, { - Address: addr6.String(), - Weight: "2", - }} - - specs := map[string]struct { - req *group.MsgCreateGroupWithPolicy - policy group.DecisionPolicy - malleate func() - expErr bool - expErrMsg string - }{ - "all good": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupPolicyAsAdmin: false, - }, - malleate: func() { - s.setNextAccount() - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - }, - "group policy as admin is true": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupPolicyAsAdmin: true, - }, - malleate: func() { - s.setNextAccount() - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - }, - "group metadata too long": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupPolicyAsAdmin: false, - GroupMetadata: strings.Repeat("a", 256), - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "limit exceeded", - }, - "group policy metadata too long": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupPolicyAsAdmin: false, - GroupPolicyMetadata: strings.Repeat("a", 256), - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "limit exceeded", - }, - "member metadata too long": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "1", - Metadata: strings.Repeat("a", 256), - }}, - GroupPolicyAsAdmin: false, - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "limit exceeded", - }, - "zero member weight": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: []group.MemberRequest{{ - Address: addr3.String(), - Weight: "0", - }}, - GroupPolicyAsAdmin: false, - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "expected a positive decimal", - }, - "decision policy threshold > total group weight": { - req: &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupPolicyAsAdmin: false, - }, - malleate: func() { - s.setNextAccount() - }, - policy: group.NewThresholdDecisionPolicy( - "10", - time.Second, - 0, - ), - expErr: false, - }, - } - - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - s.setNextAccount() - err := spec.req.SetDecisionPolicy(spec.policy) - s.Require().NoError(err) - - blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() - res, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - s.Require().Contains(err.Error(), spec.expErrMsg) - return - } - s.Require().NoError(err) - id := res.GroupId - groupPolicyAddr := res.GroupPolicyAddress - - // then all data persisted in group - loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id}) - s.Require().NoError(err) - s.Assert().Equal(spec.req.GroupMetadata, loadedGroupRes.Info.Metadata) - s.Assert().Equal(id, loadedGroupRes.Info.Id) - if spec.req.GroupPolicyAsAdmin { - s.Assert().NotEqual(spec.req.Admin, loadedGroupRes.Info.Admin) - s.Assert().Equal(groupPolicyAddr, loadedGroupRes.Info.Admin) - } else { - s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin) - } - - // and members are stored as well - membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id}) - s.Require().NoError(err) - loadedMembers := membersRes.Members - s.Require().Equal(len(members), len(loadedMembers)) - // we reorder members by address to be able to compare them - sort.Slice(members, func(i, j int) bool { - addri, err := s.accountKeeper.StringToBytes(members[i].Address) - s.Require().NoError(err) - addrj, err := s.accountKeeper.StringToBytes(members[j].Address) - s.Require().NoError(err) - return bytes.Compare(addri, addrj) < 0 - }) - for i := range loadedMembers { - s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata) - s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address) - s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight) - s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt) - s.Assert().Equal(id, loadedMembers[i].GroupId) - } - - // then all data persisted in group policy - groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: groupPolicyAddr}) - s.Require().NoError(err) - - groupPolicy := groupPolicyRes.Info - s.Assert().Equal(groupPolicyAddr, groupPolicy.Address) - s.Assert().Equal(id, groupPolicy.GroupId) - s.Assert().Equal(spec.req.GroupPolicyMetadata, groupPolicy.Metadata) - dp, err := groupPolicy.GetDecisionPolicy() - s.Assert().NoError(err) - s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) - if spec.req.GroupPolicyAsAdmin { - s.Assert().NotEqual(spec.req.Admin, groupPolicy.Admin) - s.Assert().Equal(groupPolicyAddr, groupPolicy.Admin) - } else { - s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) - } - }) - } -} - -func (s *TestSuite) TestCreateGroupPolicy() { - addrs := s.addrs - addr1 := addrs[0] - addr4 := addrs[3] - - s.setNextAccount() - groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: nil, - }) - s.Require().NoError(err) - myGroupID := groupRes.GroupId - - specs := map[string]struct { - req *group.MsgCreateGroupPolicy - policy group.DecisionPolicy - expErr bool - expErrMsg string - }{ - "all good": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - }, - "all good with percentage decision policy": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - }, - policy: group.NewPercentageDecisionPolicy( - "0.5", - time.Second, - 0, - ), - }, - "decision policy threshold > total group weight": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - }, - policy: group.NewThresholdDecisionPolicy( - "10", - time.Second, - 0, - ), - }, - "group id does not exists": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: 9999, - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "not found", - }, - "admin not group admin": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr4.String(), - GroupId: myGroupID, - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "not group admin", - }, - "metadata too long": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - Metadata: strings.Repeat("a", 256), - }, - policy: group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "limit exceeded", - }, - "percentage decision policy with negative value": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - }, - policy: group.NewPercentageDecisionPolicy( - "-0.5", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "expected a positive decimal", - }, - "percentage decision policy with value greater than 1": { - req: &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - }, - policy: group.NewPercentageDecisionPolicy( - "2", - time.Second, - 0, - ), - expErr: true, - expErrMsg: "percentage must be > 0 and <= 1", - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - err := spec.req.SetDecisionPolicy(spec.policy) - s.Require().NoError(err) - - s.setNextAccount() - - res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - s.Require().Contains(err.Error(), spec.expErrMsg) - return - } - s.Require().NoError(err) - addr := res.Address - - // then all data persisted - groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: addr}) - s.Require().NoError(err) - - groupPolicy := groupPolicyRes.Info - s.Assert().Equal(addr, groupPolicy.Address) - s.Assert().Equal(myGroupID, groupPolicy.GroupId) - s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) - s.Assert().Equal(spec.req.Metadata, groupPolicy.Metadata) - s.Assert().Equal(uint64(1), groupPolicy.Version) - percentageDecisionPolicy, ok := spec.policy.(*group.PercentageDecisionPolicy) - if ok { - dp, err := groupPolicy.GetDecisionPolicy() - s.Assert().NoError(err) - s.Assert().Equal(percentageDecisionPolicy, dp) - } else { - dp, err := groupPolicy.GetDecisionPolicy() - s.Assert().NoError(err) - s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) - } - }) - } -} - -func (s *TestSuite) TestUpdateGroupPolicyAdmin() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] - addr5 := addrs[4] - - admin, newAdmin := addr1, addr2 - policy := group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ) - s.setNextAccount() - groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) - - specs := map[string]struct { - req *group.MsgUpdateGroupPolicyAdmin - expGroupPolicy *group.GroupPolicyInfo - expErr bool - }{ - "with wrong admin": { - req: &group.MsgUpdateGroupPolicyAdmin{ - Admin: addr5.String(), - GroupPolicyAddress: groupPolicyAddr, - NewAdmin: newAdmin.String(), - }, - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: admin.String(), - Address: groupPolicyAddr, - GroupId: myGroupID, - Version: 2, - DecisionPolicy: nil, - CreatedAt: s.blockTime, - }, - expErr: true, - }, - "with wrong group policy": { - req: &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - GroupPolicyAddress: addr5.String(), - NewAdmin: newAdmin.String(), - }, - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: admin.String(), - Address: groupPolicyAddr, - GroupId: myGroupID, - Version: 2, - DecisionPolicy: nil, - CreatedAt: s.blockTime, - }, - expErr: true, - }, - "correct data": { - req: &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - GroupPolicyAddress: groupPolicyAddr, - NewAdmin: newAdmin.String(), - }, - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: newAdmin.String(), - Address: groupPolicyAddr, - GroupId: myGroupID, - Version: 2, - DecisionPolicy: nil, - CreatedAt: s.blockTime, - }, - expErr: false, - }, - } - for msg, spec := range specs { - spec := spec - err := spec.expGroupPolicy.SetDecisionPolicy(policy) - s.Require().NoError(err) - - s.Run(msg, func() { - _, err := s.groupKeeper.UpdateGroupPolicyAdmin(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ - Address: groupPolicyAddr, - }) - s.Require().NoError(err) - s.Assert().Equal(spec.expGroupPolicy, res.Info) - }) - } -} - -func (s *TestSuite) TestUpdateGroupPolicyMetadata() { - addrs := s.addrs - addr1 := addrs[0] - addr5 := addrs[4] - - admin := addr1 - policy := group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ) - - s.setNextAccount() - groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) - - specs := map[string]struct { - req *group.MsgUpdateGroupPolicyMetadata - expGroupPolicy *group.GroupPolicyInfo - expErr bool - }{ - "with wrong admin": { - req: &group.MsgUpdateGroupPolicyMetadata{ - Admin: addr5.String(), - GroupPolicyAddress: groupPolicyAddr, - }, - expGroupPolicy: &group.GroupPolicyInfo{}, - expErr: true, - }, - "with wrong group policy": { - req: &group.MsgUpdateGroupPolicyMetadata{ - Admin: admin.String(), - GroupPolicyAddress: addr5.String(), - }, - expGroupPolicy: &group.GroupPolicyInfo{}, - expErr: true, - }, - "with comment too long": { - req: &group.MsgUpdateGroupPolicyMetadata{ - Admin: admin.String(), - GroupPolicyAddress: addr5.String(), - }, - expGroupPolicy: &group.GroupPolicyInfo{}, - expErr: true, - }, - "correct data": { - req: &group.MsgUpdateGroupPolicyMetadata{ - Admin: admin.String(), - GroupPolicyAddress: groupPolicyAddr, - }, - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: admin.String(), - Address: groupPolicyAddr, - GroupId: myGroupID, - Version: 2, - DecisionPolicy: nil, - CreatedAt: s.blockTime, - }, - expErr: false, - }, - } - for msg, spec := range specs { - spec := spec - err := spec.expGroupPolicy.SetDecisionPolicy(policy) - s.Require().NoError(err) - - s.Run(msg, func() { - _, err := s.groupKeeper.UpdateGroupPolicyMetadata(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ - Address: groupPolicyAddr, - }) - s.Require().NoError(err) - s.Assert().Equal(spec.expGroupPolicy, res.Info) - - // check events - var hasUpdateGroupPolicyEvent bool - events := s.ctx.(sdk.Context).EventManager().ABCIEvents() - for _, event := range events { - event, err := sdk.ParseTypedEvent(event) - s.Require().NoError(err) - - if e, ok := event.(*group.EventUpdateGroupPolicy); ok { - s.Require().Equal(e.Address, groupPolicyAddr) - hasUpdateGroupPolicyEvent = true - break - } - } - - s.Require().True(hasUpdateGroupPolicyEvent) - }) - } -} - -func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() { - addrs := s.addrs - addr1 := addrs[0] - addr5 := addrs[4] - - admin := addr1 - policy := group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ) - - s.setNextAccount() - groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) - - specs := map[string]struct { - preRun func(admin sdk.AccAddress) (policyAddr string, groupId uint64) - req *group.MsgUpdateGroupPolicyDecisionPolicy - policy group.DecisionPolicy - expGroupPolicy *group.GroupPolicyInfo - expErr bool - }{ - "with wrong admin": { - req: &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: addr5.String(), - GroupPolicyAddress: groupPolicyAddr, - }, - policy: policy, - expGroupPolicy: &group.GroupPolicyInfo{}, - expErr: true, - }, - "with wrong group policy": { - req: &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: admin.String(), - GroupPolicyAddress: addr5.String(), - }, - policy: policy, - expGroupPolicy: &group.GroupPolicyInfo{}, - expErr: true, - }, - "correct data": { - req: &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: admin.String(), - GroupPolicyAddress: groupPolicyAddr, - }, - policy: group.NewThresholdDecisionPolicy( - "2", - time.Duration(2)*time.Second, - 0, - ), - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: admin.String(), - Address: groupPolicyAddr, - GroupId: myGroupID, - Version: 2, - DecisionPolicy: nil, - CreatedAt: s.blockTime, - }, - expErr: false, - }, - "correct data with percentage decision policy": { - preRun: func(admin sdk.AccAddress) (string, uint64) { - s.setNextAccount() - return s.createGroupAndGroupPolicy(admin, nil, policy) - }, - req: &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: admin.String(), - GroupPolicyAddress: groupPolicyAddr, - }, - policy: group.NewPercentageDecisionPolicy( - "0.5", - time.Duration(2)*time.Second, - 0, - ), - expGroupPolicy: &group.GroupPolicyInfo{ - Admin: admin.String(), - DecisionPolicy: nil, - Version: 2, - CreatedAt: s.blockTime, - }, - expErr: false, - }, - } - for msg, spec := range specs { - spec := spec - policyAddr := groupPolicyAddr - err := spec.expGroupPolicy.SetDecisionPolicy(spec.policy) - s.Require().NoError(err) - if spec.preRun != nil { - policyAddr1, groupID := spec.preRun(admin) - policyAddr = policyAddr1 - - // update the expected info with new group policy details - spec.expGroupPolicy.Address = policyAddr1 - spec.expGroupPolicy.GroupId = groupID - - // update req with new group policy addr - spec.req.GroupPolicyAddress = policyAddr1 - } - - err = spec.req.SetDecisionPolicy(spec.policy) - s.Require().NoError(err) - - s.Run(msg, func() { - _, err := s.groupKeeper.UpdateGroupPolicyDecisionPolicy(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ - Address: policyAddr, - }) - s.Require().NoError(err) - s.Assert().Equal(spec.expGroupPolicy, res.Info) - }) - } -} - -func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() { - addrs := s.addrs - addr2 := addrs[1] - - admin := addr2 - - groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: admin.String(), - Members: nil, - }) - s.Require().NoError(err) - myGroupID := groupRes.GroupId - - policies := []group.DecisionPolicy{ - group.NewThresholdDecisionPolicy( - "1", - time.Second, - 0, - ), - group.NewThresholdDecisionPolicy( - "10", - time.Second, - 0, - ), - group.NewPercentageDecisionPolicy( - "0.5", - time.Second, - 0, - ), - } - - count := 3 - expectAccs := make([]*group.GroupPolicyInfo, count) - for i := range expectAccs { - req := &group.MsgCreateGroupPolicy{ - Admin: admin.String(), - GroupId: myGroupID, - } - err := req.SetDecisionPolicy(policies[i]) - s.Require().NoError(err) - - s.setNextAccount() - res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, req) - s.Require().NoError(err) - - expectAcc := &group.GroupPolicyInfo{ - Address: res.Address, - Admin: admin.String(), - GroupId: myGroupID, - Version: uint64(1), - CreatedAt: s.blockTime, - } - err = expectAcc.SetDecisionPolicy(policies[i]) - s.Require().NoError(err) - expectAccs[i] = expectAcc - } - sort.Slice(expectAccs, func(i, j int) bool { return expectAccs[i].Address < expectAccs[j].Address }) - - // query group policy by group - policiesByGroupRes, err := s.groupKeeper.GroupPoliciesByGroup(s.ctx, &group.QueryGroupPoliciesByGroupRequest{ - GroupId: myGroupID, - }) - s.Require().NoError(err) - policyAccs := policiesByGroupRes.GroupPolicies - s.Require().Equal(len(policyAccs), count) - // we reorder policyAccs by address to be able to compare them - sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) - for i := range policyAccs { - s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) - s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) - s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) - s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) - s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) - s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) - dp1, err := policyAccs[i].GetDecisionPolicy() - s.Assert().NoError(err) - dp2, err := expectAccs[i].GetDecisionPolicy() - s.Assert().NoError(err) - s.Assert().Equal(dp1, dp2) - } - - // query group policy by admin - policiesByAdminRes, err := s.groupKeeper.GroupPoliciesByAdmin(s.ctx, &group.QueryGroupPoliciesByAdminRequest{ - Admin: admin.String(), - }) - s.Require().NoError(err) - policyAccs = policiesByAdminRes.GroupPolicies - s.Require().Equal(len(policyAccs), count) - // we reorder policyAccs by address to be able to compare them - sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) - for i := range policyAccs { - s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) - s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) - s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) - s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) - s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) - s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) - dp1, err := policyAccs[i].GetDecisionPolicy() - s.Assert().NoError(err) - dp2, err := expectAccs[i].GetDecisionPolicy() - s.Assert().NoError(err) - s.Assert().Equal(dp1, dp2) - } -} - -func (s *TestSuite) TestSubmitProposal() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] // Has weight 2 - addr4 := addrs[3] - addr5 := addrs[4] // Has weight 1 - - myGroupID := s.groupID - accountAddr := s.groupPolicyAddr - - // Create a new group policy to test TRY_EXEC - policyReq := &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - } - noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy( - "2", - time.Second, - 0, // no MinExecutionPeriod to test TRY_EXEC - ) - err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy) - s.Require().NoError(err) - s.setNextAccount() - res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) - s.Require().NoError(err) - - s.accountKeeper.EXPECT().StringToBytes(res.Address).Return(sdk.MustAccAddressFromBech32(res.Address).Bytes(), nil).AnyTimes() - noMinExecPeriodPolicyAddr, err := s.accountKeeper.StringToBytes(res.Address) - s.Require().NoError(err) - - // Create a new group policy with super high threshold - bigThresholdPolicy := group.NewThresholdDecisionPolicy( - "100", - time.Second, - minExecutionPeriod, - ) - s.setNextAccount() - err = policyReq.SetDecisionPolicy(bigThresholdPolicy) - s.Require().NoError(err) - bigThresholdRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) - s.Require().NoError(err) - bigThresholdAddr := bigThresholdRes.Address - - msgSend := &banktypes.MsgSend{ - FromAddress: res.Address, - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - } - defaultProposal := group.Proposal{ - GroupPolicyAddress: accountAddr.String(), - Status: group.PROPOSAL_STATUS_SUBMITTED, - FinalTallyResult: group.TallyResult{ - YesCount: "0", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - } - specs := map[string]struct { - req *group.MsgSubmitProposal - msgs []sdk.Msg - expProposal group.Proposal - expErr bool - postRun func(sdkCtx sdk.Context) - preRun func(msg []sdk.Msg) - }{ - "all good with minimal fields set": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr2.String()}, - }, - expProposal: defaultProposal, - postRun: func(sdkCtx sdk.Context) {}, - }, - "all good with good msg payload": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr2.String()}, - }, - msgs: []sdk.Msg{&banktypes.MsgSend{ - FromAddress: accountAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("token", 100)}, - }}, - expProposal: defaultProposal, - postRun: func(sdkCtx sdk.Context) {}, - }, - "metadata too long": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr2.String()}, - Metadata: strings.Repeat("a", 256), - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "group policy required": { - req: &group.MsgSubmitProposal{ - Proposers: []string{addr2.String()}, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "existing group policy required": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: addr1.String(), - Proposers: []string{addr2.String()}, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "decision policy threshold > total group weight": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: bigThresholdAddr, - Proposers: []string{addr2.String()}, - }, - expErr: false, - expProposal: group.Proposal{ - GroupPolicyAddress: bigThresholdAddr, - Status: group.PROPOSAL_STATUS_SUBMITTED, - FinalTallyResult: group.DefaultTallyResult(), - ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - postRun: func(sdkCtx sdk.Context) {}, - }, - "only group members can create a proposal": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr4.String()}, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "all proposers must be in group": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr2.String(), addr4.String()}, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "admin that is not a group member can not create proposal": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr1.String()}, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "reject msgs that are not authz by group policy": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), - Proposers: []string{addr2.String()}, - }, - msgs: []sdk.Msg{&testdata.TestMsg{Signers: []string{addr1.String()}}}, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "with try exec": { - preRun: func(msgs []sdk.Msg) { - for i := 0; i < len(msgs); i++ { - s.bankKeeper.EXPECT().Send(gomock.Any(), msgs[i]).Return(nil, nil) - } - }, - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: res.Address, - Proposers: []string{addr2.String()}, - Exec: group.Exec_EXEC_TRY, - }, - msgs: []sdk.Msg{msgSend}, - expProposal: group.Proposal{ - GroupPolicyAddress: res.Address, - Status: group.PROPOSAL_STATUS_ACCEPTED, - FinalTallyResult: group.TallyResult{ - YesCount: "2", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - postRun: func(sdkCtx sdk.Context) { - s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) - s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) - - fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr) - s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) - toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) - s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) - }, - }, - "with try exec, not enough yes votes for proposal to pass": { - req: &group.MsgSubmitProposal{ - GroupPolicyAddress: res.Address, - Proposers: []string{addr5.String()}, - Exec: group.Exec_EXEC_TRY, - }, - msgs: []sdk.Msg{msgSend}, - expProposal: group.Proposal{ - GroupPolicyAddress: res.Address, - Status: group.PROPOSAL_STATUS_SUBMITTED, - FinalTallyResult: group.TallyResult{ - YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - postRun: func(sdkCtx sdk.Context) {}, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - err := spec.req.SetMsgs(spec.msgs) - s.Require().NoError(err) - - if spec.preRun != nil { - spec.preRun(spec.msgs) - } - - res, err := s.groupKeeper.SubmitProposal(s.ctx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - id := res.ProposalId - - if !(spec.expProposal.ExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { - // then all data persisted - proposalRes, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: id}) - s.Require().NoError(err) - proposal := proposalRes.Proposal - - s.Assert().Equal(spec.expProposal.GroupPolicyAddress, proposal.GroupPolicyAddress) - s.Assert().Equal(spec.req.Metadata, proposal.Metadata) - s.Assert().Equal(spec.req.Proposers, proposal.Proposers) - s.Assert().Equal(s.blockTime, proposal.SubmitTime) - s.Assert().Equal(uint64(1), proposal.GroupVersion) - s.Assert().Equal(uint64(1), proposal.GroupPolicyVersion) - s.Assert().Equal(spec.expProposal.Status, proposal.Status) - s.Assert().Equal(spec.expProposal.FinalTallyResult, proposal.FinalTallyResult) - s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult) - s.Assert().Equal(s.blockTime.Add(time.Second), proposal.VotingPeriodEnd) - - msgs, err := proposal.GetMsgs() - s.Assert().NoError(err) - if spec.msgs == nil { // then empty list is ok - s.Assert().Len(msgs, 0) - } else { - s.Assert().Equal(spec.msgs, msgs) - } - } - - spec.postRun(s.sdkCtx) - }) - } -} - -func (s *TestSuite) TestWithdrawProposal() { - addrs := s.addrs - addr2 := addrs[1] - addr5 := addrs[4] - - msgSend := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - } - - proposers := []string{addr2.String()} - proposalID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) - - specs := map[string]struct { - preRun func(sdkCtx sdk.Context) uint64 - proposalID uint64 - admin string - expErrMsg string - }{ - "wrong admin": { - preRun: func(sdkCtx sdk.Context) uint64 { - return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) - }, - admin: addr5.String(), - expErrMsg: "unauthorized", - }, - "wrong proposalId": { - preRun: func(sdkCtx sdk.Context) uint64 { - return 1111 - }, - admin: proposers[0], - expErrMsg: "not found", - }, - "happy case with proposer": { - preRun: func(sdkCtx sdk.Context) uint64 { - return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) - }, - proposalID: proposalID, - admin: proposers[0], - }, - "already closed proposal": { - preRun: func(sdkCtx sdk.Context) uint64 { - pID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) - _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ - ProposalId: pID, - Address: proposers[0], - }) - s.Require().NoError(err) - return pID - }, - proposalID: proposalID, - admin: proposers[0], - expErrMsg: "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN", - }, - "happy case with group admin address": { - preRun: func(sdkCtx sdk.Context) uint64 { - return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) - }, - proposalID: proposalID, - admin: proposers[0], - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - pID := spec.preRun(s.sdkCtx) - - _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ - ProposalId: pID, - Address: spec.admin, - }) - - if spec.expErrMsg != "" { - s.Require().Error(err) - s.Require().Contains(err.Error(), spec.expErrMsg) - return - } - - s.Require().NoError(err) - resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: pID}) - s.Require().NoError(err) - s.Require().Equal(resp.GetProposal().Status, group.PROPOSAL_STATUS_WITHDRAWN) - }) - } -} - -func (s *TestSuite) TestVote() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] - addr3 := addrs[2] - addr4 := addrs[3] - addr5 := addrs[4] - members := []group.MemberRequest{ - {Address: addr4.String(), Weight: "1"}, - {Address: addr3.String(), Weight: "2"}, - } - - groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ - Admin: addr1.String(), - Members: members, - }) - s.Require().NoError(err) - myGroupID := groupRes.GroupId - - policy := group.NewThresholdDecisionPolicy( - "2", - time.Duration(2), - 0, - ) - policyReq := &group.MsgCreateGroupPolicy{ - Admin: addr1.String(), - GroupId: myGroupID, - } - err = policyReq.SetDecisionPolicy(policy) - s.Require().NoError(err) - - s.setNextAccount() - policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) - s.Require().NoError(err) - accountAddr := policyRes.Address - // module account will be created and returned - addrbz, err := address.NewBech32Codec("cosmos").StringToBytes(accountAddr) - s.Require().NoError(err) - s.accountKeeper.EXPECT().StringToBytes(accountAddr).Return(addrbz, nil).AnyTimes() - groupPolicy, err := s.accountKeeper.StringToBytes(accountAddr) - s.Require().NoError(err) - s.Require().NotNil(groupPolicy) - - s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)}).Return(nil).AnyTimes() - s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) - - req := &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr, - Proposers: []string{addr4.String()}, - Messages: nil, - } - msg := &banktypes.MsgSend{ - FromAddress: accountAddr, - ToAddress: addr5.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - } - err = req.SetMsgs([]sdk.Msg{msg}) - s.Require().NoError(err) - - proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, req) - s.Require().NoError(err) - myProposalID := proposalRes.ProposalId - - // proposals by group policy - proposalsRes, err := s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{ - Address: accountAddr, - }) - s.Require().NoError(err) - proposals := proposalsRes.Proposals - s.Require().Equal(len(proposals), 1) - s.Assert().Equal(req.GroupPolicyAddress, proposals[0].GroupPolicyAddress) - s.Assert().Equal(req.Metadata, proposals[0].Metadata) - s.Assert().Equal(req.Proposers, proposals[0].Proposers) - s.Assert().Equal(s.blockTime, proposals[0].SubmitTime) - s.Assert().Equal(uint64(1), proposals[0].GroupVersion) - s.Assert().Equal(uint64(1), proposals[0].GroupPolicyVersion) - s.Assert().Equal(group.PROPOSAL_STATUS_SUBMITTED, proposals[0].Status) - s.Assert().Equal(group.DefaultTallyResult(), proposals[0].FinalTallyResult) - - specs := map[string]struct { - srcCtx sdk.Context - expTallyResult group.TallyResult // expected after tallying - isFinal bool // is the tally result final? - req *group.MsgVote - doBefore func(ctx context.Context) - postRun func(sdkCtx sdk.Context) - expProposalStatus group.ProposalStatus // expected after tallying - expExecutorResult group.ProposalExecutorResult // expected after tallying - expErr bool - }{ - "vote yes": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_YES, - }, - expTallyResult: group.TallyResult{ - YesCount: "1", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "with try exec": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr3.String(), - Option: group.VOTE_OPTION_YES, - Exec: group.Exec_EXEC_TRY, - }, - expTallyResult: group.TallyResult{ - YesCount: "2", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - isFinal: true, - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - doBefore: func(ctx context.Context) { - s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) - }, - postRun: func(sdkCtx sdk.Context) { - s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), groupPolicy).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) - s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), addr5).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) - - fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, groupPolicy) - s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) - toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr5) - s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) - }, - }, - "with try exec, not enough yes votes for proposal to pass": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_YES, - Exec: group.Exec_EXEC_TRY, - }, - expTallyResult: group.TallyResult{ - YesCount: "1", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "vote no": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - }, - expTallyResult: group.TallyResult{ - YesCount: "0", - NoCount: "1", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "vote abstain": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_ABSTAIN, - }, - expTallyResult: group.TallyResult{ - YesCount: "0", - NoCount: "0", - AbstainCount: "1", - NoWithVetoCount: "0", - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "vote veto": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO_WITH_VETO, - }, - expTallyResult: group.TallyResult{ - YesCount: "0", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "1", - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "apply decision policy early": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr3.String(), - Option: group.VOTE_OPTION_YES, - }, - expTallyResult: group.TallyResult{ - YesCount: "2", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - postRun: func(sdkCtx sdk.Context) {}, - }, - "reject new votes when final decision is made already": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_YES, - }, - doBefore: func(ctx context.Context) { - _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr3.String(), - Option: group.VOTE_OPTION_NO_WITH_VETO, - Exec: 1, // Execute the proposal so that its status is final - }) - s.Require().NoError(err) - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "metadata too long": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - Metadata: strings.Repeat("a", 256), - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "existing proposal required": { - req: &group.MsgVote{ - ProposalId: 999, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "empty vote option": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "invalid vote option": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: 5, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "voter must be in group": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr2.String(), - Option: group.VOTE_OPTION_NO, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "admin that is not a group member can not vote": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr1.String(), - Option: group.VOTE_OPTION_NO, - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "on voting period end": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - }, - srcCtx: s.sdkCtx.WithBlockTime(s.blockTime.Add(time.Second)), - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "closed already": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - }, - doBefore: func(ctx context.Context) { - s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) - - _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr3.String(), - Option: group.VOTE_OPTION_YES, - Exec: 1, // Execute to close the proposal. - }) - s.Require().NoError(err) - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - "voted already": { - req: &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_NO, - }, - doBefore: func(ctx context.Context) { - _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ - ProposalId: myProposalID, - Voter: addr4.String(), - Option: group.VOTE_OPTION_YES, - }) - s.Require().NoError(err) - }, - expErr: true, - postRun: func(sdkCtx sdk.Context) {}, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - sdkCtx := s.sdkCtx - if !spec.srcCtx.IsZero() { - sdkCtx = spec.srcCtx - } - sdkCtx, _ = sdkCtx.CacheContext() - if spec.doBefore != nil { - spec.doBefore(sdkCtx) - } - _, err := s.groupKeeper.Vote(sdkCtx, spec.req) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { - // vote is stored and all data persisted - res, err := s.groupKeeper.VoteByProposalVoter(sdkCtx, &group.QueryVoteByProposalVoterRequest{ - ProposalId: spec.req.ProposalId, - Voter: spec.req.Voter, - }) - s.Require().NoError(err) - loaded := res.Vote - s.Assert().Equal(spec.req.ProposalId, loaded.ProposalId) - s.Assert().Equal(spec.req.Voter, loaded.Voter) - s.Assert().Equal(spec.req.Option, loaded.Option) - s.Assert().Equal(spec.req.Metadata, loaded.Metadata) - s.Assert().Equal(s.blockTime, loaded.SubmitTime) - - // query votes by proposal - votesByProposalRes, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ - ProposalId: spec.req.ProposalId, - }) - s.Require().NoError(err) - votesByProposal := votesByProposalRes.Votes - s.Require().Equal(1, len(votesByProposal)) - vote := votesByProposal[0] - s.Assert().Equal(spec.req.ProposalId, vote.ProposalId) - s.Assert().Equal(spec.req.Voter, vote.Voter) - s.Assert().Equal(spec.req.Option, vote.Option) - s.Assert().Equal(spec.req.Metadata, vote.Metadata) - s.Assert().Equal(s.blockTime, vote.SubmitTime) - - // query votes by voter - voter := spec.req.Voter - votesByVoterRes, err := s.groupKeeper.VotesByVoter(sdkCtx, &group.QueryVotesByVoterRequest{ - Voter: voter, - }) - s.Require().NoError(err) - votesByVoter := votesByVoterRes.Votes - s.Require().Equal(1, len(votesByVoter)) - s.Assert().Equal(spec.req.ProposalId, votesByVoter[0].ProposalId) - s.Assert().Equal(voter, votesByVoter[0].Voter) - s.Assert().Equal(spec.req.Option, votesByVoter[0].Option) - s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata) - s.Assert().Equal(s.blockTime, votesByVoter[0].SubmitTime) - - proposalRes, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ - ProposalId: spec.req.ProposalId, - }) - s.Require().NoError(err) - - proposal := proposalRes.Proposal - if spec.isFinal { - s.Assert().Equal(spec.expTallyResult, proposal.FinalTallyResult) - s.Assert().Equal(spec.expProposalStatus, proposal.Status) - s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult) - } else { - s.Assert().Equal(group.DefaultTallyResult(), proposal.FinalTallyResult) // Make sure proposal isn't mutated. - - // do a round of tallying - tallyResult, err := s.groupKeeper.Tally(sdkCtx, *proposal, myGroupID) - s.Require().NoError(err) - - s.Assert().Equal(spec.expTallyResult, tallyResult) - } - } - - spec.postRun(sdkCtx) - }) - } - - s.T().Log("test tally result should not take into account the member who left the group") - require := s.Require() - members = []group.MemberRequest{ - {Address: addr2.String(), Weight: "3"}, - {Address: addr3.String(), Weight: "2"}, - {Address: addr4.String(), Weight: "1"}, - } - reqCreate := &group.MsgCreateGroupWithPolicy{ - Admin: addr1.String(), - Members: members, - GroupMetadata: "metadata", - } - - policy = group.NewThresholdDecisionPolicy( - "4", - time.Duration(10), - 0, - ) - require.NoError(reqCreate.SetDecisionPolicy(policy)) - s.setNextAccount() - - result, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, reqCreate) - require.NoError(err) - require.NotNil(result) - - policyAddr := result.GroupPolicyAddress - groupID := result.GroupId - reqProposal := &group.MsgSubmitProposal{ - GroupPolicyAddress: policyAddr, - Proposers: []string{addr4.String()}, - } - require.NoError(reqProposal.SetMsgs([]sdk.Msg{&banktypes.MsgSend{ - FromAddress: policyAddr, - ToAddress: addr5.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - }})) - - resSubmitProposal, err := s.groupKeeper.SubmitProposal(s.ctx, reqProposal) - require.NoError(err) - require.NotNil(resSubmitProposal) - proposalID := resSubmitProposal.ProposalId - - for _, voter := range []string{addr4.String(), addr3.String(), addr2.String()} { - _, err := s.groupKeeper.Vote(s.ctx, - &group.MsgVote{ProposalId: proposalID, Voter: voter, Option: group.VOTE_OPTION_YES}, - ) - require.NoError(err) - } - - qProposals, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ - ProposalId: proposalID, - }) - require.NoError(err) - - tallyResult, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) - require.NoError(err) - - _, err = s.groupKeeper.LeaveGroup(s.ctx, &group.MsgLeaveGroup{Address: addr4.String(), GroupId: groupID}) - require.NoError(err) - - tallyResult1, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) - require.NoError(err) - require.NotEqual(tallyResult.String(), tallyResult1.String()) -} - -func (s *TestSuite) TestExecProposal() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] - - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - } - msgSend2 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 10001)}, - } - proposers := []string{addr2.String()} - - specs := map[string]struct { - srcBlockTime time.Time - setupProposal func(ctx context.Context) uint64 - expErr bool - expProposalStatus group.ProposalStatus - expExecutorResult group.ProposalExecutorResult - expBalance bool - expFromBalances sdk.Coin - expToBalances sdk.Coin - }{ - "proposal executed when accepted": { - setupProposal: func(ctx context.Context) uint64 { - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - expBalance: true, - expFromBalances: sdk.NewInt64Coin("test", 9900), - expToBalances: sdk.NewInt64Coin("test", 100), - }, - "proposal with multiple messages executed when accepted": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1, msgSend1} - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) - - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - expBalance: true, - expFromBalances: sdk.NewInt64Coin("test", 9800), - expToBalances: sdk.NewInt64Coin("test", 200), - }, - "proposal not executed when rejected": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expProposalStatus: group.PROPOSAL_STATUS_REJECTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "open proposal must not fail": { - setupProposal: func(ctx context.Context) uint64 { - return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) - }, - expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "existing proposal required": { - setupProposal: func(ctx context.Context) uint64 { - return 9999 - }, - expErr: true, - }, - "Decision policy also applied on exactly voting period end": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) - }, - srcBlockTime: s.blockTime.Add(time.Second), // Voting period is 1s - expProposalStatus: group.PROPOSAL_STATUS_REJECTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "Decision policy also applied after voting period end": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) - }, - srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s - expProposalStatus: group.PROPOSAL_STATUS_REJECTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "exec proposal before MinExecutionPeriod should fail": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - srcBlockTime: s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed - }, - "exec proposal at exactly MinExecutionPeriod should pass": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1} - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - srcBlockTime: s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - "prevent double execution when successful": { - setupProposal: func(ctx context.Context) uint64 { - myProposalID := submitProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.VOTE_OPTION_YES) - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) - - // Wait after min execution period end before Exec - sdkCtx := sdk.UnwrapSDKContext(ctx) - sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s - _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) - s.Require().NoError(err) - return myProposalID - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expErr: true, // since proposal is pruned after a successful MsgExec - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - expBalance: true, - expFromBalances: sdk.NewInt64Coin("test", 9900), - expToBalances: sdk.NewInt64Coin("test", 100), - }, - "rollback all msg updates on failure": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend1, msgSend2} - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) - - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, - }, - "executable when failed before": { - setupProposal: func(ctx context.Context) uint64 { - msgs := []sdk.Msg{msgSend2} - myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - - // Wait after min execution period end before Exec - sdkCtx := sdk.UnwrapSDKContext(ctx) - sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) - _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) - - s.Require().NoError(err) - s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) - - return myProposalID - }, - srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end - expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - sdkCtx, _ := s.sdkCtx.CacheContext() - proposalID := spec.setupProposal(sdkCtx) - - if !spec.srcBlockTime.IsZero() { - sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) - } - - _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) - if spec.expErr { - s.Require().Error(err) - return - } - s.Require().NoError(err) - - if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { - - // and proposal is updated - res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) - s.Require().NoError(err) - proposal := res.Proposal - - exp := group.ProposalStatus_name[int32(spec.expProposalStatus)] - got := group.ProposalStatus_name[int32(proposal.Status)] - s.Assert().Equal(exp, got) - - exp = group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] - got = group.ProposalExecutorResult_name[int32(proposal.ExecutorResult)] - s.Assert().Equal(exp, got) - } - - if spec.expBalance { - s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, s.groupPolicyAddr).Return(sdk.Coins{spec.expFromBalances}) - s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.Coins{spec.expToBalances}) - - fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, s.groupPolicyAddr) - s.Require().Contains(fromBalances, spec.expFromBalances) - toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) - s.Require().Contains(toBalances, spec.expToBalances) - } - }) - } -} - -func (s *TestSuite) TestExecPrunedProposalsAndVotes() { - addrs := s.addrs - addr1 := addrs[0] - addr2 := addrs[1] - - proposers := []string{addr2.String()} - specs := map[string]struct { - srcBlockTime time.Time - setupProposal func(ctx context.Context) uint64 - expErr bool - expErrMsg string - expExecutorResult group.ProposalExecutorResult - }{ - "proposal pruned after executor result success": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 101)}, - } - msgs := []sdk.Msg{msgSend1} - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - expErrMsg: "load proposal: not found", - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - "proposal with multiple messages pruned when executed with result success": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 102)}, - } - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) - - msgs := []sdk.Msg{msgSend1, msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - expErrMsg: "load proposal: not found", - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - "proposal not pruned when not executed and rejected": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 103)}, - } - msgs := []sdk.Msg{msgSend1} - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) - }, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "open proposal is not pruned which must not fail ": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 104)}, - } - return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) - }, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "proposal not pruned with group modified before tally": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 105)}, - } - myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) - - // then modify group - _, err := s.groupKeeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{ - Admin: addr1.String(), - GroupId: s.groupID, - }) - s.Require().NoError(err) - return myProposalID - }, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "proposal not pruned with group policy modified before tally": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 106)}, - } - - myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) - _, err := s.groupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{ - Admin: addr1.String(), - GroupPolicyAddress: s.groupPolicyAddr.String(), - }) - s.Require().NoError(err) - return myProposalID - }, - expErr: true, // since proposal status will be `aborted` when group policy is modified - expErrMsg: "not possible to exec with proposal status", - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, - }, - "proposal exists when rollback all msg updates on failure": { - setupProposal: func(ctx context.Context) uint64 { - msgSend1 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 107)}, - } - - msgSend2 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 10002)}, - } - - msgs := []sdk.Msg{msgSend1, msgSend2} - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, fmt.Errorf("error")) - - return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - }, - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, - }, - "pruned when proposal is executable when failed before": { - setupProposal: func(ctx context.Context) uint64 { - msgSend2 := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 10003)}, - } - - msgs := []sdk.Msg{msgSend2} - - myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) - - // Wait for min execution period end - sdkCtx := sdk.UnwrapSDKContext(ctx) - sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) - _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) - s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) - - s.Require().NoError(err) - return myProposalID - }, - expErrMsg: "load proposal: not found", - expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, - }, - } - for msg, spec := range specs { - spec := spec - s.Run(msg, func() { - sdkCtx, _ := s.sdkCtx.CacheContext() - proposalID := spec.setupProposal(sdkCtx) - - if !spec.srcBlockTime.IsZero() { - sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) - } - - // Wait for min execution period end - sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) - _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) - if spec.expErr { - s.Require().Error(err) - s.Require().Contains(err.Error(), spec.expErrMsg) - return - } - s.Require().NoError(err) - - if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS { - // Make sure proposal is deleted from state - _, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) - s.Require().Contains(err.Error(), spec.expErrMsg) - res, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: proposalID}) - s.Require().NoError(err) - s.Require().Empty(res.GetVotes()) - - } else { - // Check that proposal and votes exists - res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) - s.Require().NoError(err) - _, err = s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id}) - s.Require().NoError(err) - s.Require().Equal("", spec.expErrMsg) - - exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] - got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)] - s.Assert().Equal(exp, got) - } - }) - } -} - func (s *TestSuite) TestProposalsByVPEnd() { addrs := s.addrs addr2 := addrs[1] @@ -2968,191 +296,6 @@ func (s *TestSuite) TestProposalsByVPEnd() { } } -func (s *TestSuite) TestLeaveGroup() { - addrs := simtestutil.CreateIncrementalAccounts(7) - - admin1 := addrs[0] - member1 := addrs[1] - member2 := addrs[2] - member3 := addrs[3] - member4 := addrs[4] - admin2 := addrs[5] - admin3 := addrs[6] - require := s.Require() - - for _, addr := range addrs { - s.accountKeeper.EXPECT().StringToBytes(addr.String()).Return(addr.Bytes(), nil).AnyTimes() - s.accountKeeper.EXPECT().BytesToString(addr).Return(addr.String(), nil).AnyTimes() - } - - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member2.String(), - Weight: "2", - Metadata: "metadata", - }, - { - Address: member3.String(), - Weight: "3", - Metadata: "metadata", - }, - } - policy := group.NewThresholdDecisionPolicy( - "3", - time.Hour, - time.Hour, - ) - s.setNextAccount() - _, groupID1 := s.createGroupAndGroupPolicy(admin1, members, policy) - - members = []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - } - - s.setNextAccount() - _, groupID2 := s.createGroupAndGroupPolicy(admin2, members, nil) - - members = []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member2.String(), - Weight: "2", - Metadata: "metadata", - }, - } - policy = &group.PercentageDecisionPolicy{ - Percentage: "0.5", - Windows: &group.DecisionPolicyWindows{VotingPeriod: time.Hour}, - } - - s.setNextAccount() - - _, groupID3 := s.createGroupAndGroupPolicy(admin3, members, policy) - testCases := []struct { - name string - req *group.MsgLeaveGroup - expErr bool - errMsg string - expMembersSize int - memberWeight math.Dec - }{ - { - "expect error: group not found", - &group.MsgLeaveGroup{ - GroupId: 100000, - Address: member1.String(), - }, - true, - "group: not found", - 0, - math.NewDecFromInt64(0), - }, - { - "expect error: member not part of group", - &group.MsgLeaveGroup{ - GroupId: groupID1, - Address: member4.String(), - }, - true, - "not part of group", - 0, - math.NewDecFromInt64(0), - }, - { - "valid testcase: decision policy is not present (and group total weight can be 0)", - &group.MsgLeaveGroup{ - GroupId: groupID2, - Address: member1.String(), - }, - false, - "", - 0, - math.NewDecFromInt64(1), - }, - { - "valid testcase: threshold decision policy", - &group.MsgLeaveGroup{ - GroupId: groupID1, - Address: member3.String(), - }, - false, - "", - 2, - math.NewDecFromInt64(3), - }, - { - "valid request: can leave group policy threshold more than group weight", - &group.MsgLeaveGroup{ - GroupId: groupID1, - Address: member2.String(), - }, - false, - "", - 1, - math.NewDecFromInt64(2), - }, - { - "valid request: can leave group (percentage decision policy)", - &group.MsgLeaveGroup{ - GroupId: groupID3, - Address: member2.String(), - }, - false, - "", - 1, - math.NewDecFromInt64(2), - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - var groupWeight1 math.Dec - if !tc.expErr { - groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) - require.NoError(err) - groupWeight1, err = math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) - require.NoError(err) - } - - res, err := s.groupKeeper.LeaveGroup(s.ctx, tc.req) - if tc.expErr { - require.Error(err) - require.Contains(err.Error(), tc.errMsg) - } else { - require.NoError(err) - require.NotNil(res) - res, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{ - GroupId: tc.req.GroupId, - }) - require.NoError(err) - require.Len(res.Members, tc.expMembersSize) - - groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) - require.NoError(err) - groupWeight2, err := math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) - require.NoError(err) - - rWeight, err := groupWeight1.Sub(tc.memberWeight) - require.NoError(err) - require.Equal(rWeight.Cmp(groupWeight2), 0) - } - }) - } -} - func (s *TestSuite) TestPruneProposals() { addrs := s.addrs expirationTime := time.Hour * 24 * 15 // 15 days diff --git a/x/group/keeper/msg_server.go b/x/group/keeper/msg_server.go index 4f16f25fc0..0619e0e709 100644 --- a/x/group/keeper/msg_server.go +++ b/x/group/keeper/msg_server.go @@ -5,6 +5,7 @@ import ( "context" "encoding/binary" "fmt" + "strings" errorsmod "cosmossdk.io/errors" @@ -23,28 +24,28 @@ var _ group.MsgServer = Keeper{} // Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072 const gasCostPerIteration = uint64(20) -func (k Keeper) CreateGroup(goCtx context.Context, req *group.MsgCreateGroup) (*group.MsgCreateGroupResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - metadata := req.Metadata - members := group.MemberRequests{Members: req.Members} - admin := req.Admin - - if err := members.ValidateBasic(); err != nil { - return nil, err +func (k Keeper) CreateGroup(goCtx context.Context, msg *group.MsgCreateGroup) (*group.MsgCreateGroupResponse, error) { + if _, err := k.accKeeper.StringToBytes(msg.Admin); err != nil { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid admin address: %s", msg.Admin) } - if err := k.assertMetadataLength(metadata, "group metadata"); err != nil { + if err := k.validateMembers(msg.Members); err != nil { + return nil, errorsmod.Wrap(err, "members") + } + + if err := k.assertMetadataLength(msg.Metadata, "group metadata"); err != nil { return nil, err } totalWeight := math.NewDecFromInt64(0) - for i := range members.Members { - m := members.Members[i] + for _, m := range msg.Members { if err := k.assertMetadataLength(m.Metadata, "member metadata"); err != nil { return nil, err } // Members of a group must have a positive weight. + // NOTE: group member with zero weight are only allowed when updating group members. + // If the member has a zero weight, it will be removed from the group. weight, err := math.NewPositiveDecFromString(m.Weight) if err != nil { return nil, err @@ -58,10 +59,11 @@ func (k Keeper) CreateGroup(goCtx context.Context, req *group.MsgCreateGroup) (* } // Create a new group in the groupTable. + ctx := sdk.UnwrapSDKContext(goCtx) groupInfo := &group.GroupInfo{ Id: k.groupTable.Sequence().PeekNextVal(ctx.KVStore(k.key)), - Admin: admin, - Metadata: metadata, + Admin: msg.Admin, + Metadata: msg.Metadata, Version: 1, TotalWeight: totalWeight.String(), CreatedAt: ctx.BlockTime(), @@ -72,8 +74,7 @@ func (k Keeper) CreateGroup(goCtx context.Context, req *group.MsgCreateGroup) (* } // Create new group members in the groupMemberTable. - for i := range members.Members { - m := members.Members[i] + for i, m := range msg.Members { err := k.groupMemberTable.Create(ctx.KVStore(k.key), &group.GroupMember{ GroupId: groupID, Member: &group.Member{ @@ -88,31 +89,43 @@ func (k Keeper) CreateGroup(goCtx context.Context, req *group.MsgCreateGroup) (* } } - err = ctx.EventManager().EmitTypedEvent(&group.EventCreateGroup{GroupId: groupID}) - if err != nil { + if err := ctx.EventManager().EmitTypedEvent(&group.EventCreateGroup{GroupId: groupID}); err != nil { return nil, err } return &group.MsgCreateGroupResponse{GroupId: groupID}, nil } -func (k Keeper) UpdateGroupMembers(goCtx context.Context, req *group.MsgUpdateGroupMembers) (*group.MsgUpdateGroupMembersResponse, error) { +func (k Keeper) UpdateGroupMembers(goCtx context.Context, msg *group.MsgUpdateGroupMembers) (*group.MsgUpdateGroupMembersResponse, error) { + if msg.GroupId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "group id") + } + + if len(msg.MemberUpdates) == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "member updates") + } + + if err := k.validateMembers(msg.MemberUpdates); err != nil { + return nil, errorsmod.Wrap(err, "members") + } + ctx := sdk.UnwrapSDKContext(goCtx) action := func(g *group.GroupInfo) error { totalWeight, err := math.NewNonNegativeDecFromString(g.TotalWeight) if err != nil { return errorsmod.Wrap(err, "group total weight") } - for i := range req.MemberUpdates { - if err := k.assertMetadataLength(req.MemberUpdates[i].Metadata, "group member metadata"); err != nil { + + for _, member := range msg.MemberUpdates { + if err := k.assertMetadataLength(member.Metadata, "group member metadata"); err != nil { return err } groupMember := group.GroupMember{ - GroupId: req.GroupId, + GroupId: msg.GroupId, Member: &group.Member{ - Address: req.MemberUpdates[i].Address, - Weight: req.MemberUpdates[i].Weight, - Metadata: req.MemberUpdates[i].Metadata, + Address: member.Address, + Weight: member.Weight, + Metadata: member.Metadata, }, } @@ -196,137 +209,160 @@ func (k Keeper) UpdateGroupMembers(goCtx context.Context, req *group.MsgUpdateGr return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g) } - err := k.doUpdateGroup(ctx, req, action, "members updated") - if err != nil { + if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "members updated"); err != nil { return nil, err } return &group.MsgUpdateGroupMembersResponse{}, nil } -func (k Keeper) UpdateGroupAdmin(goCtx context.Context, req *group.MsgUpdateGroupAdmin) (*group.MsgUpdateGroupAdminResponse, error) { +func (k Keeper) UpdateGroupAdmin(goCtx context.Context, msg *group.MsgUpdateGroupAdmin) (*group.MsgUpdateGroupAdminResponse, error) { + if msg.GroupId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "group id") + } + + if strings.EqualFold(msg.Admin, msg.NewAdmin) { + return nil, errorsmod.Wrap(errors.ErrInvalid, "new and old admin are the same") + } + + if _, err := k.accKeeper.StringToBytes(msg.Admin); err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "admin address") + } + + if _, err := k.accKeeper.StringToBytes(msg.NewAdmin); err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "new admin address") + } + ctx := sdk.UnwrapSDKContext(goCtx) action := func(g *group.GroupInfo) error { - g.Admin = req.NewAdmin + g.Admin = msg.NewAdmin g.Version++ return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g) } - err := k.doUpdateGroup(ctx, req, action, "admin updated") - if err != nil { + if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "admin updated"); err != nil { return nil, err } return &group.MsgUpdateGroupAdminResponse{}, nil } -func (k Keeper) UpdateGroupMetadata(goCtx context.Context, req *group.MsgUpdateGroupMetadata) (*group.MsgUpdateGroupMetadataResponse, error) { +func (k Keeper) UpdateGroupMetadata(goCtx context.Context, msg *group.MsgUpdateGroupMetadata) (*group.MsgUpdateGroupMetadataResponse, error) { + if msg.GroupId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "group id") + } + + if err := k.assertMetadataLength(msg.Metadata, "group metadata"); err != nil { + return nil, err + } + + if _, err := k.accKeeper.StringToBytes(msg.Admin); err != nil { + return nil, errorsmod.Wrap(sdkerrors.ErrInvalidAddress, "admin address") + } + ctx := sdk.UnwrapSDKContext(goCtx) action := func(g *group.GroupInfo) error { - g.Metadata = req.Metadata + g.Metadata = msg.Metadata g.Version++ return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g) } - if err := k.assertMetadataLength(req.Metadata, "group metadata"); err != nil { - return nil, err - } - - err := k.doUpdateGroup(ctx, req, action, "metadata updated") - if err != nil { + if err := k.doUpdateGroup(ctx, msg.GetGroupID(), msg.GetAdmin(), action, "metadata updated"); err != nil { return nil, err } return &group.MsgUpdateGroupMetadataResponse{}, nil } -func (k Keeper) CreateGroupWithPolicy(goCtx context.Context, req *group.MsgCreateGroupWithPolicy) (*group.MsgCreateGroupWithPolicyResponse, error) { - groupRes, err := k.CreateGroup(goCtx, &group.MsgCreateGroup{ - Admin: req.Admin, - Members: req.Members, - Metadata: req.GroupMetadata, +func (k Keeper) CreateGroupWithPolicy(ctx context.Context, msg *group.MsgCreateGroupWithPolicy) (*group.MsgCreateGroupWithPolicyResponse, error) { + // NOTE: admin, and group message validation is performed in the CreateGroup method + groupRes, err := k.CreateGroup(ctx, &group.MsgCreateGroup{ + Admin: msg.Admin, + Members: msg.Members, + Metadata: msg.GroupMetadata, }) if err != nil { return nil, errorsmod.Wrap(err, "group response") } groupID := groupRes.GroupId - var groupPolicyAddr sdk.AccAddress - groupPolicyRes, err := k.CreateGroupPolicy(goCtx, &group.MsgCreateGroupPolicy{ - Admin: req.Admin, + // NOTE: group policy message validation is performed in the CreateGroupPolicy method + groupPolicyRes, err := k.CreateGroupPolicy(ctx, &group.MsgCreateGroupPolicy{ + Admin: msg.Admin, GroupId: groupID, - Metadata: req.GroupPolicyMetadata, - DecisionPolicy: req.DecisionPolicy, + Metadata: msg.GroupPolicyMetadata, + DecisionPolicy: msg.DecisionPolicy, }) if err != nil { return nil, errorsmod.Wrap(err, "group policy response") } - policyAddr := groupPolicyRes.Address - groupPolicyAddr, err = k.accKeeper.StringToBytes(policyAddr) - if err != nil { - return nil, errorsmod.Wrap(err, "group policy address") - } - groupPolicyAddress := groupPolicyAddr.String() - - if req.GroupPolicyAsAdmin { + if msg.GroupPolicyAsAdmin { updateAdminReq := &group.MsgUpdateGroupAdmin{ GroupId: groupID, - Admin: req.Admin, - NewAdmin: groupPolicyAddress, + Admin: msg.Admin, + NewAdmin: groupPolicyRes.Address, } - _, err = k.UpdateGroupAdmin(goCtx, updateAdminReq) + _, err = k.UpdateGroupAdmin(ctx, updateAdminReq) if err != nil { return nil, err } updatePolicyAddressReq := &group.MsgUpdateGroupPolicyAdmin{ - Admin: req.Admin, - GroupPolicyAddress: groupPolicyAddress, - NewAdmin: groupPolicyAddress, + Admin: msg.Admin, + GroupPolicyAddress: groupPolicyRes.Address, + NewAdmin: groupPolicyRes.Address, } - _, err = k.UpdateGroupPolicyAdmin(goCtx, updatePolicyAddressReq) + _, err = k.UpdateGroupPolicyAdmin(ctx, updatePolicyAddressReq) if err != nil { return nil, err } } - return &group.MsgCreateGroupWithPolicyResponse{GroupId: groupID, GroupPolicyAddress: groupPolicyAddress}, nil + return &group.MsgCreateGroupWithPolicyResponse{GroupId: groupID, GroupPolicyAddress: groupPolicyRes.Address}, nil } -func (k Keeper) CreateGroupPolicy(goCtx context.Context, req *group.MsgCreateGroupPolicy) (*group.MsgCreateGroupPolicyResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - admin, err := k.accKeeper.StringToBytes(req.GetAdmin()) - if err != nil { - return nil, errorsmod.Wrap(err, "request admin") +func (k Keeper) CreateGroupPolicy(goCtx context.Context, msg *group.MsgCreateGroupPolicy) (*group.MsgCreateGroupPolicyResponse, error) { + if msg.GroupId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "group id") } - policy, err := req.GetDecisionPolicy() + + if err := k.assertMetadataLength(msg.GetMetadata(), "group policy metadata"); err != nil { + return nil, err + } + + policy, err := msg.GetDecisionPolicy() if err != nil { return nil, errorsmod.Wrap(err, "request decision policy") } - groupID := req.GetGroupID() - metadata := req.GetMetadata() - if err := k.assertMetadataLength(metadata, "group policy metadata"); err != nil { - return nil, err + if err := policy.ValidateBasic(); err != nil { + return nil, errorsmod.Wrap(err, "decision policy") } - g, err := k.getGroupInfo(ctx, groupID) + reqGroupAdmin, err := k.accKeeper.StringToBytes(msg.GetAdmin()) + if err != nil { + return nil, errorsmod.Wrap(err, "request admin") + } + + ctx := sdk.UnwrapSDKContext(goCtx) + groupInfo, err := k.getGroupInfo(ctx, msg.GetGroupID()) if err != nil { return nil, err } - groupAdmin, err := k.accKeeper.StringToBytes(g.Admin) + + groupAdmin, err := k.accKeeper.StringToBytes(groupInfo.Admin) if err != nil { return nil, errorsmod.Wrap(err, "group admin") } + // Only current group admin is authorized to create a group policy for this - if !bytes.Equal(groupAdmin, admin) { + if !bytes.Equal(groupAdmin, reqGroupAdmin) { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "not group admin") } - err = policy.Validate(g, k.config) - if err != nil { + if err := policy.Validate(groupInfo, k.config); err != nil { return nil, err } @@ -364,9 +400,9 @@ func (k Keeper) CreateGroupPolicy(goCtx context.Context, req *group.MsgCreateGro groupPolicy, err := group.NewGroupPolicyInfo( accountAddr, - groupID, - admin, - metadata, + msg.GetGroupID(), + reqGroupAdmin, + msg.GetMetadata(), 1, policy, ctx.BlockTime(), @@ -379,44 +415,50 @@ func (k Keeper) CreateGroupPolicy(goCtx context.Context, req *group.MsgCreateGro return nil, errorsmod.Wrap(err, "could not create group policy") } - err = ctx.EventManager().EmitTypedEvent(&group.EventCreateGroupPolicy{Address: accountAddr.String()}) - if err != nil { + if err := ctx.EventManager().EmitTypedEvent(&group.EventCreateGroupPolicy{Address: accountAddr.String()}); err != nil { return nil, err } return &group.MsgCreateGroupPolicyResponse{Address: accountAddr.String()}, nil } -func (k Keeper) UpdateGroupPolicyAdmin(goCtx context.Context, req *group.MsgUpdateGroupPolicyAdmin) (*group.MsgUpdateGroupPolicyAdminResponse, error) { +func (k Keeper) UpdateGroupPolicyAdmin(goCtx context.Context, msg *group.MsgUpdateGroupPolicyAdmin) (*group.MsgUpdateGroupPolicyAdminResponse, error) { + if strings.EqualFold(msg.Admin, msg.NewAdmin) { + return nil, errorsmod.Wrap(errors.ErrInvalid, "new and old admin are same") + } + ctx := sdk.UnwrapSDKContext(goCtx) action := func(groupPolicy *group.GroupPolicyInfo) error { - groupPolicy.Admin = req.NewAdmin + groupPolicy.Admin = msg.NewAdmin groupPolicy.Version++ return k.groupPolicyTable.Update(ctx.KVStore(k.key), groupPolicy) } - err := k.doUpdateGroupPolicy(ctx, req.GroupPolicyAddress, req.Admin, action, "group policy admin updated") - if err != nil { + if err := k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy admin updated"); err != nil { return nil, err } return &group.MsgUpdateGroupPolicyAdminResponse{}, nil } -func (k Keeper) UpdateGroupPolicyDecisionPolicy(goCtx context.Context, req *group.MsgUpdateGroupPolicyDecisionPolicy) (*group.MsgUpdateGroupPolicyDecisionPolicyResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - policy, err := req.GetDecisionPolicy() +func (k Keeper) UpdateGroupPolicyDecisionPolicy(goCtx context.Context, msg *group.MsgUpdateGroupPolicyDecisionPolicy) (*group.MsgUpdateGroupPolicyDecisionPolicyResponse, error) { + policy, err := msg.GetDecisionPolicy() if err != nil { - return nil, err + return nil, errorsmod.Wrap(err, "decision policy") } + if err := policy.ValidateBasic(); err != nil { + return nil, errorsmod.Wrap(err, "decision policy") + } + + ctx := sdk.UnwrapSDKContext(goCtx) action := func(groupPolicy *group.GroupPolicyInfo) error { - g, err := k.getGroupInfo(ctx, groupPolicy.GroupId) + groupInfo, err := k.getGroupInfo(ctx, groupPolicy.GroupId) if err != nil { return err } - err = policy.Validate(g, k.config) + err = policy.Validate(groupInfo, k.config) if err != nil { return err } @@ -430,17 +472,16 @@ func (k Keeper) UpdateGroupPolicyDecisionPolicy(goCtx context.Context, req *grou return k.groupPolicyTable.Update(ctx.KVStore(k.key), groupPolicy) } - err = k.doUpdateGroupPolicy(ctx, req.GroupPolicyAddress, req.Admin, action, "group policy's decision policy updated") - if err != nil { + if err = k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy's decision policy updated"); err != nil { return nil, err } return &group.MsgUpdateGroupPolicyDecisionPolicyResponse{}, nil } -func (k Keeper) UpdateGroupPolicyMetadata(goCtx context.Context, req *group.MsgUpdateGroupPolicyMetadata) (*group.MsgUpdateGroupPolicyMetadataResponse, error) { +func (k Keeper) UpdateGroupPolicyMetadata(goCtx context.Context, msg *group.MsgUpdateGroupPolicyMetadata) (*group.MsgUpdateGroupPolicyMetadataResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - metadata := req.GetMetadata() + metadata := msg.GetMetadata() action := func(groupPolicy *group.GroupPolicyInfo) error { groupPolicy.Metadata = metadata @@ -452,7 +493,7 @@ func (k Keeper) UpdateGroupPolicyMetadata(goCtx context.Context, req *group.MsgU return nil, err } - err := k.doUpdateGroupPolicy(ctx, req.GroupPolicyAddress, req.Admin, action, "group policy metadata updated") + err := k.doUpdateGroupPolicy(ctx, msg.GroupPolicyAddress, msg.Admin, action, "group policy metadata updated") if err != nil { return nil, err } @@ -460,45 +501,56 @@ func (k Keeper) UpdateGroupPolicyMetadata(goCtx context.Context, req *group.MsgU return &group.MsgUpdateGroupPolicyMetadataResponse{}, nil } -func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitProposal) (*group.MsgSubmitProposalResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - groupPolicyAddr, err := k.accKeeper.StringToBytes(req.GroupPolicyAddress) +func (k Keeper) SubmitProposal(goCtx context.Context, msg *group.MsgSubmitProposal) (*group.MsgSubmitProposalResponse, error) { + if len(msg.Proposers) == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "proposers") + } + + if err := k.validateProposers(msg.Proposers); err != nil { + return nil, err + } + + groupPolicyAddr, err := k.accKeeper.StringToBytes(msg.GroupPolicyAddress) if err != nil { return nil, errorsmod.Wrap(err, "request account address of group policy") } - metadata := req.Metadata - proposers := req.Proposers - msgs, err := req.GetMsgs() + + if err := k.assertMetadataLength(msg.Title, "proposal Title"); err != nil { + return nil, err + } + + if err := k.assertMetadataLength(msg.Summary, "proposal summary"); err != nil { + return nil, err + } + + if err := k.assertMetadataLength(msg.Metadata, "metadata"); err != nil { + return nil, err + } + + msgs, err := msg.GetMsgs() if err != nil { return nil, errorsmod.Wrap(err, "request msgs") } - if err := k.assertMetadataLength(metadata, "metadata"); err != nil { + if err := validateMsgs(msgs); err != nil { return nil, err } - if err := k.assertMetadataLength(req.Summary, "proposal summary"); err != nil { - return nil, err - } - - if err := k.assertMetadataLength(req.Title, "proposal Title"); err != nil { - return nil, err - } - - policyAcc, err := k.getGroupPolicyInfo(ctx, req.GroupPolicyAddress) + ctx := sdk.UnwrapSDKContext(goCtx) + policyAcc, err := k.getGroupPolicyInfo(ctx, msg.GroupPolicyAddress) if err != nil { - return nil, errorsmod.Wrapf(err, "load group policy: %s", req.GroupPolicyAddress) + return nil, errorsmod.Wrapf(err, "load group policy: %s", msg.GroupPolicyAddress) } - g, err := k.getGroupInfo(ctx, policyAcc.GroupId) + groupInfo, err := k.getGroupInfo(ctx, policyAcc.GroupId) if err != nil { return nil, errorsmod.Wrap(err, "get group by groupId of group policy") } // Only members of the group can submit a new proposal. - for i := range proposers { - if !k.groupMemberTable.Has(ctx.KVStore(k.key), orm.PrimaryKey(&group.GroupMember{GroupId: g.Id, Member: &group.Member{Address: proposers[i]}})) { - return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "not in group: %s", proposers[i]) + for _, proposer := range msg.Proposers { + if !k.groupMemberTable.Has(ctx.KVStore(k.key), orm.PrimaryKey(&group.GroupMember{GroupId: groupInfo.Id, Member: &group.Member{Address: proposer}})) { + return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "not in group: %s", proposer) } } @@ -512,26 +564,25 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos return nil, errorsmod.Wrap(err, "proposal group policy decision policy") } - // Prevent proposal that can not succeed. - err = policy.Validate(g, k.config) - if err != nil { + // Prevent proposal that cannot succeed. + if err = policy.Validate(groupInfo, k.config); err != nil { return nil, err } m := &group.Proposal{ Id: k.proposalTable.Sequence().PeekNextVal(ctx.KVStore(k.key)), - GroupPolicyAddress: req.GroupPolicyAddress, - Metadata: metadata, - Proposers: proposers, + GroupPolicyAddress: msg.GroupPolicyAddress, + Metadata: msg.Metadata, + Proposers: msg.Proposers, SubmitTime: ctx.BlockTime(), - GroupVersion: g.Version, + GroupVersion: groupInfo.Version, GroupPolicyVersion: policyAcc.Version, Status: group.PROPOSAL_STATUS_SUBMITTED, ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, VotingPeriodEnd: ctx.BlockTime().Add(policy.GetVotingPeriod()), // The voting window begins as soon as the proposal is submitted. FinalTallyResult: group.DefaultTallyResult(), - Title: req.Title, - Summary: req.Summary, + Title: msg.Title, + Summary: msg.Summary, } if err := m.SetMsgs(msgs); err != nil { @@ -543,23 +594,22 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos return nil, errorsmod.Wrap(err, "create proposal") } - err = ctx.EventManager().EmitTypedEvent(&group.EventSubmitProposal{ProposalId: id}) - if err != nil { + if err := ctx.EventManager().EmitTypedEvent(&group.EventSubmitProposal{ProposalId: id}); err != nil { return nil, err } // Try to execute proposal immediately - if req.Exec == group.Exec_EXEC_TRY { + if msg.Exec == group.Exec_EXEC_TRY { // Consider proposers as Yes votes - for i := range proposers { + for _, proposer := range msg.Proposers { ctx.GasMeter().ConsumeGas(gasCostPerIteration, "vote on proposal") _, err = k.Vote(ctx, &group.MsgVote{ ProposalId: id, - Voter: proposers[i], + Voter: proposer, Option: group.VOTE_OPTION_YES, }) if err != nil { - return &group.MsgSubmitProposalResponse{ProposalId: id}, errorsmod.Wrapf(err, "the proposal was created but failed on vote for voter %s", proposers[i]) + return &group.MsgSubmitProposalResponse{ProposalId: id}, errorsmod.Wrapf(err, "the proposal was created but failed on vote for voter %s", proposer) } } @@ -568,7 +618,7 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos ProposalId: id, // We consider the first proposer as the MsgExecRequest signer // but that could be revisited (eg using the group policy) - Executor: proposers[0], + Executor: msg.Proposers[0], }) if err != nil { return &group.MsgSubmitProposalResponse{ProposalId: id}, errorsmod.Wrap(err, "the proposal was created but failed on exec") @@ -578,12 +628,17 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos return &group.MsgSubmitProposalResponse{ProposalId: id}, nil } -func (k Keeper) WithdrawProposal(goCtx context.Context, req *group.MsgWithdrawProposal) (*group.MsgWithdrawProposalResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - id := req.ProposalId - address := req.Address +func (k Keeper) WithdrawProposal(goCtx context.Context, msg *group.MsgWithdrawProposal) (*group.MsgWithdrawProposalResponse, error) { + if msg.ProposalId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id") + } - proposal, err := k.getProposal(ctx, id) + if _, err := k.accKeeper.StringToBytes(msg.Address); err != nil { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid group policy admin / proposer address: %s", msg.Address) + } + + ctx := sdk.UnwrapSDKContext(goCtx) + proposal, err := k.getProposal(ctx, msg.ProposalId) if err != nil { return nil, err } @@ -599,41 +654,55 @@ func (k Keeper) WithdrawProposal(goCtx context.Context, req *group.MsgWithdrawPr } // check address is the group policy admin he is in proposers list.. - if address != policyInfo.Admin && !isProposer(proposal, address) { - return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "given address is neither group policy admin nor in proposers: %s", address) + if msg.Address != policyInfo.Admin && !isProposer(proposal, msg.Address) { + return nil, errorsmod.Wrapf(errors.ErrUnauthorized, "given address is neither group policy admin nor in proposers: %s", msg.Address) } proposal.Status = group.PROPOSAL_STATUS_WITHDRAWN - if err := k.proposalTable.Update(ctx.KVStore(k.key), id, &proposal); err != nil { + if err := k.proposalTable.Update(ctx.KVStore(k.key), msg.ProposalId, &proposal); err != nil { return nil, err } - err = ctx.EventManager().EmitTypedEvent(&group.EventWithdrawProposal{ProposalId: id}) - if err != nil { + if err := ctx.EventManager().EmitTypedEvent(&group.EventWithdrawProposal{ProposalId: msg.ProposalId}); err != nil { return nil, err } return &group.MsgWithdrawProposalResponse{}, nil } -func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - id := req.ProposalId - voteOption := req.Option - metadata := req.Metadata +func (k Keeper) Vote(goCtx context.Context, msg *group.MsgVote) (*group.MsgVoteResponse, error) { + if msg.ProposalId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id") + } - if err := k.assertMetadataLength(metadata, "metadata"); err != nil { + // verify vote options + if msg.Option == group.VOTE_OPTION_UNSPECIFIED { + return nil, errorsmod.Wrap(errors.ErrEmpty, "vote option") + } + + if _, ok := group.VoteOption_name[int32(msg.Option)]; !ok { + return nil, errorsmod.Wrap(errors.ErrInvalid, "vote option") + } + + if err := k.assertMetadataLength(msg.Metadata, "metadata"); err != nil { return nil, err } - proposal, err := k.getProposal(ctx, id) + if _, err := k.accKeeper.StringToBytes(msg.Voter); err != nil { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid voter address: %s", msg.Voter) + } + + ctx := sdk.UnwrapSDKContext(goCtx) + proposal, err := k.getProposal(ctx, msg.ProposalId) if err != nil { return nil, err } + // Ensure that we can still accept votes for this proposal. if proposal.Status != group.PROPOSAL_STATUS_SUBMITTED { return nil, errorsmod.Wrap(errors.ErrInvalid, "proposal not open for voting") } + if ctx.BlockTime().After(proposal.VotingPeriodEnd) { return nil, errorsmod.Wrap(errors.ErrExpired, "voting period has ended already") } @@ -643,22 +712,21 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR return nil, errorsmod.Wrap(err, "load group policy") } - electorate, err := k.getGroupInfo(ctx, policyInfo.GroupId) + groupInfo, err := k.getGroupInfo(ctx, policyInfo.GroupId) if err != nil { return nil, err } // Count and store votes. - voterAddr := req.Voter - voter := group.GroupMember{GroupId: electorate.Id, Member: &group.Member{Address: voterAddr}} + voter := group.GroupMember{GroupId: groupInfo.Id, Member: &group.Member{Address: msg.Voter}} if err := k.groupMemberTable.GetOne(ctx.KVStore(k.key), orm.PrimaryKey(&voter), &voter); err != nil { - return nil, errorsmod.Wrapf(err, "voter address: %s", voterAddr) + return nil, errorsmod.Wrapf(err, "voter address: %s", msg.Voter) } newVote := group.Vote{ - ProposalId: id, - Voter: voterAddr, - Option: voteOption, - Metadata: metadata, + ProposalId: msg.ProposalId, + Voter: msg.Voter, + Option: msg.Option, + Metadata: msg.Metadata, SubmitTime: ctx.BlockTime(), } @@ -668,17 +736,13 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR return nil, errorsmod.Wrap(err, "store vote") } - err = ctx.EventManager().EmitTypedEvent(&group.EventVote{ProposalId: id}) - if err != nil { + if err := ctx.EventManager().EmitTypedEvent(&group.EventVote{ProposalId: msg.ProposalId}); err != nil { return nil, err } // Try to execute proposal immediately - if req.Exec == group.Exec_EXEC_TRY { - _, err = k.Exec(ctx, &group.MsgExec{ - ProposalId: id, - Executor: voterAddr, - }) + if msg.Exec == group.Exec_EXEC_TRY { + _, err = k.Exec(ctx, &group.MsgExec{ProposalId: msg.ProposalId, Executor: msg.Voter}) if err != nil { return nil, err } @@ -690,7 +754,7 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR // doTallyAndUpdate performs a tally, and, if the tally result is final, then: // - updates the proposal's `Status` and `FinalTallyResult` fields, // - prune all the votes. -func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate group.GroupInfo, policyInfo group.GroupPolicyInfo) error { +func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, groupInfo group.GroupInfo, policyInfo group.GroupPolicyInfo) error { policy, err := policyInfo.GetDecisionPolicy() if err != nil { return err @@ -701,7 +765,7 @@ func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate return err } - result, err := policy.Allow(tallyResult, electorate.TotalWeight) + result, err := policy.Allow(tallyResult, groupInfo.TotalWeight) if err != nil { return errorsmod.Wrap(err, "policy allow") } @@ -724,11 +788,13 @@ func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate } // Exec executes the messages from a proposal. -func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - id := req.ProposalId +func (k Keeper) Exec(goCtx context.Context, msg *group.MsgExec) (*group.MsgExecResponse, error) { + if msg.ProposalId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "proposal id") + } - proposal, err := k.getProposal(ctx, id) + ctx := sdk.UnwrapSDKContext(goCtx) + proposal, err := k.getProposal(ctx, msg.ProposalId) if err != nil { return nil, err } @@ -746,12 +812,12 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR // didn't end yet, and tallying hasn't been done. In this case, we need to // tally first. if proposal.Status == group.PROPOSAL_STATUS_SUBMITTED { - electorate, err := k.getGroupInfo(ctx, policyInfo.GroupId) + groupInfo, err := k.getGroupInfo(ctx, policyInfo.GroupId) if err != nil { return nil, errorsmod.Wrap(err, "load group") } - if err := k.doTallyAndUpdate(ctx, &proposal, electorate, policyInfo); err != nil { + if err := k.doTallyAndUpdate(ctx, &proposal, groupInfo, policyInfo); err != nil { return nil, err } } @@ -770,8 +836,8 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR decisionPolicy := policyInfo.DecisionPolicy.GetCachedValue().(group.DecisionPolicy) if results, err := k.doExecuteMsgs(cacheCtx, k.router, proposal, addr, decisionPolicy); err != nil { proposal.ExecutorResult = group.PROPOSAL_EXECUTOR_RESULT_FAILURE - logs = fmt.Sprintf("proposal execution failed on proposal %d, because of error %s", id, err.Error()) - k.Logger(ctx).Info("proposal execution failed", "cause", err, "proposalID", id) + logs = fmt.Sprintf("proposal execution failed on proposal %d, because of error %s", proposal.Id, err.Error()) + k.Logger(ctx).Info("proposal execution failed", "cause", err, "proposalID", proposal.Id) } else { proposal.ExecutorResult = group.PROPOSAL_EXECUTOR_RESULT_SUCCESS flush() @@ -791,17 +857,16 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR } } else { store := ctx.KVStore(k.key) - if err := k.proposalTable.Update(store, id, &proposal); err != nil { + if err := k.proposalTable.Update(store, proposal.Id, &proposal); err != nil { return nil, err } } - err = ctx.EventManager().EmitTypedEvent(&group.EventExec{ - ProposalId: id, + if err := ctx.EventManager().EmitTypedEvent(&group.EventExec{ + ProposalId: proposal.Id, Logs: logs, Result: proposal.ExecutorResult, - }) - if err != nil { + }); err != nil { return nil, err } @@ -811,14 +876,18 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR } // LeaveGroup implements the MsgServer/LeaveGroup method. -func (k Keeper) LeaveGroup(goCtx context.Context, req *group.MsgLeaveGroup) (*group.MsgLeaveGroupResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - _, err := k.accKeeper.StringToBytes(req.Address) - if err != nil { - return nil, err +func (k Keeper) LeaveGroup(goCtx context.Context, msg *group.MsgLeaveGroup) (*group.MsgLeaveGroupResponse, error) { + if msg.GroupId == 0 { + return nil, errorsmod.Wrap(errors.ErrEmpty, "group-id") } - groupInfo, err := k.getGroupInfo(ctx, req.GroupId) + _, err := k.accKeeper.StringToBytes(msg.Address) + if err != nil { + return nil, errorsmod.Wrap(err, "group member") + } + + ctx := sdk.UnwrapSDKContext(goCtx) + groupInfo, err := k.getGroupInfo(ctx, msg.GroupId) if err != nil { return nil, errorsmod.Wrap(err, "group") } @@ -829,8 +898,8 @@ func (k Keeper) LeaveGroup(goCtx context.Context, req *group.MsgLeaveGroup) (*gr } gm, err := k.getGroupMember(ctx, &group.GroupMember{ - GroupId: req.GroupId, - Member: &group.Member{Address: req.Address}, + GroupId: msg.GroupId, + Member: &group.Member{Address: msg.Address}, }) if err != nil { return nil, err @@ -863,10 +932,12 @@ func (k Keeper) LeaveGroup(goCtx context.Context, req *group.MsgLeaveGroup) (*gr return nil, err } - ctx.EventManager().EmitTypedEvent(&group.EventLeaveGroup{ - GroupId: req.GroupId, - Address: req.Address, - }) + if err := ctx.EventManager().EmitTypedEvent(&group.EventLeaveGroup{ + GroupId: msg.GroupId, + Address: msg.Address, + }); err != nil { + return nil, err + } return &group.MsgLeaveGroupResponse{}, nil } @@ -886,11 +957,6 @@ func (k Keeper) getGroupMember(ctx sdk.Context, member *group.GroupMember) (*gro return &groupMember, nil } -type authNGroupReq interface { - GetGroupID() uint64 - GetAdmin() string -} - type ( actionFn func(m *group.GroupInfo) error groupPolicyActionFn func(m *group.GroupPolicyInfo) error @@ -898,24 +964,24 @@ type ( // doUpdateGroupPolicy first makes sure that the group policy admin initiated the group policy update, // before performing the group policy update and emitting an event. -func (k Keeper) doUpdateGroupPolicy(ctx sdk.Context, groupPolicy, admin string, action groupPolicyActionFn, note string) error { - groupPolicyInfo, err := k.getGroupPolicyInfo(ctx, groupPolicy) - if err != nil { - return errorsmod.Wrap(err, "load group policy") - } - - groupPolicyAddr, err := k.accKeeper.StringToBytes(groupPolicy) +func (k Keeper) doUpdateGroupPolicy(ctx sdk.Context, reqGroupPolicy, reqAdmin string, action groupPolicyActionFn, note string) error { + groupPolicyAddr, err := k.accKeeper.StringToBytes(reqGroupPolicy) if err != nil { return errorsmod.Wrap(err, "group policy address") } - _, err = k.accKeeper.StringToBytes(admin) + _, err = k.accKeeper.StringToBytes(reqAdmin) if err != nil { return errorsmod.Wrap(err, "group policy admin") } + groupPolicyInfo, err := k.getGroupPolicyInfo(ctx, reqGroupPolicy) + if err != nil { + return errorsmod.Wrap(err, "load group policy") + } + // Only current group policy admin is authorized to update a group policy. - if admin != groupPolicyInfo.Admin { + if reqAdmin != groupPolicyInfo.Admin { return errorsmod.Wrap(sdkerrors.ErrUnauthorized, "not group policy admin") } @@ -936,41 +1002,24 @@ func (k Keeper) doUpdateGroupPolicy(ctx sdk.Context, groupPolicy, admin string, // doUpdateGroup first makes sure that the group admin initiated the group update, // before performing the group update and emitting an event. -func (k Keeper) doUpdateGroup(ctx sdk.Context, req authNGroupReq, action actionFn, note string) error { - err := k.doAuthenticated(ctx, req, action, note) +func (k Keeper) doUpdateGroup(ctx sdk.Context, groupID uint64, reqGroupAdmin string, action actionFn, errNote string) error { + groupInfo, err := k.getGroupInfo(ctx, groupID) if err != nil { return err } - err = ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroup{GroupId: req.GetGroupID()}) - if err != nil { - return err + if !strings.EqualFold(groupInfo.Admin, reqGroupAdmin) { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "not group admin; got %s, expected %s", reqGroupAdmin, groupInfo.Admin) } - return nil -} - -// doAuthenticated makes sure that the group admin initiated the request, -// and perform the provided action on the group. -func (k Keeper) doAuthenticated(ctx sdk.Context, req authNGroupReq, action actionFn, errNote string) error { - group, err := k.getGroupInfo(ctx, req.GetGroupID()) - if err != nil { - return err - } - admin, err := k.accKeeper.StringToBytes(group.Admin) - if err != nil { - return errorsmod.Wrap(err, "group admin") - } - reqAdmin, err := k.accKeeper.StringToBytes(req.GetAdmin()) - if err != nil { - return errorsmod.Wrap(err, "request admin") - } - if !bytes.Equal(admin, reqAdmin) { - return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "not group admin; got %s, expected %s", req.GetAdmin(), group.Admin) - } - if err := action(&group); err != nil { + if err := action(&groupInfo); err != nil { return errorsmod.Wrap(err, errNote) } + + if err := ctx.EventManager().EmitTypedEvent(&group.EventUpdateGroup{GroupId: groupID}); err != nil { + return err + } + return nil } @@ -1011,6 +1060,55 @@ func (k Keeper) validateDecisionPolicies(ctx sdk.Context, g group.GroupInfo) err return nil } +// validateProposers checks that all proposers addresses are valid. +// It as well verifies that there is no duplicate address. +func (k Keeper) validateProposers(proposers []string) error { + index := make(map[string]struct{}, len(proposers)) + for _, proposer := range proposers { + if _, exists := index[proposer]; exists { + return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", proposer) + } + + _, err := k.accKeeper.StringToBytes(proposer) + if err != nil { + return errorsmod.Wrapf(err, "proposer address %s", proposer) + } + + index[proposer] = struct{}{} + } + + return nil +} + +// validateMembers checks that all members addresses are valid. +// additionally it verifies that there is no duplicate address +// and the member weight is non-negative. +// Note: in state, a member's weight MUST be positive. However, in some Msgs, +// it's possible to set a zero member weight, for example in +// MsgUpdateGroupMembers to denote that we're removing a member. +// It returns an error if any of the above conditions is not met. +func (k Keeper) validateMembers(members []group.MemberRequest) error { + index := make(map[string]struct{}, len(members)) + for _, member := range members { + if _, exists := index[member.Address]; exists { + return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", member.Address) + } + + _, err := k.accKeeper.StringToBytes(member.Address) + if err != nil { + return errorsmod.Wrapf(err, "member address %s", member.Address) + } + + if _, err := math.NewNonNegativeDecFromString(member.Weight); err != nil { + return errorsmod.Wrap(err, "weight must be non negative") + } + + index[member.Address] = struct{}{} + } + + return nil +} + // isProposer checks that an address is a proposer of a given proposal. func isProposer(proposal group.Proposal, address string) bool { for _, proposer := range proposal.Proposers { @@ -1021,3 +1119,18 @@ func isProposer(proposal group.Proposal, address string) bool { return false } + +func validateMsgs(msgs []sdk.Msg) error { + for i, msg := range msgs { + m, ok := msg.(sdk.HasValidateBasic) + if !ok { + continue + } + + if err := m.ValidateBasic(); err != nil { + return errorsmod.Wrapf(err, "msg %d", i) + } + } + + return nil +} diff --git a/x/group/keeper/msg_server_test.go b/x/group/keeper/msg_server_test.go new file mode 100644 index 0000000000..4f80159f98 --- /dev/null +++ b/x/group/keeper/msg_server_test.go @@ -0,0 +1,3078 @@ +package keeper_test + +import ( + "bytes" + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/codec/address" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/group" + "github.com/cosmos/cosmos-sdk/x/group/internal/math" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/golang/mock/gomock" +) + +func (s *TestSuite) TestCreateGroupWithLotsOfMembers() { + for i := 50; i < 70; i++ { + membersResp := s.createGroupAndGetMembers(i) + s.Require().Equal(len(membersResp), i) + } +} + +func (s *TestSuite) createGroupAndGetMembers(numMembers int) []*group.GroupMember { + addressPool := simtestutil.CreateIncrementalAccounts(numMembers) + members := make([]group.MemberRequest, numMembers) + for i := 0; i < len(members); i++ { + members[i] = group.MemberRequest{ + Address: addressPool[i].String(), + Weight: "1", + } + s.accountKeeper.EXPECT().StringToBytes(addressPool[i].String()).Return(addressPool[i].Bytes(), nil).AnyTimes() + s.accountKeeper.EXPECT().BytesToString(addressPool[i].Bytes()).Return(addressPool[i].String(), nil).AnyTimes() + } + + g, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: members[0].Address, + Members: members, + }) + s.Require().NoErrorf(err, "failed to create group with %d members", len(members)) + s.T().Logf("group %d created with %d members", g.GroupId, len(members)) + + groupMemberResp, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: g.GroupId}) + s.Require().NoError(err) + + s.T().Logf("got %d members from group %d", len(groupMemberResp.Members), g.GroupId) + + return groupMemberResp.Members +} + +func (s *TestSuite) TestCreateGroup() { + addrs := s.addrs + addr1 := addrs[0] + addr3 := addrs[2] + addr5 := addrs[4] + addr6 := addrs[5] + + members := []group.MemberRequest{{ + Address: addr5.String(), + Weight: "1", + }, { + Address: addr6.String(), + Weight: "2", + }} + + expGroups := []*group.GroupInfo{ + { + Id: s.groupID, + Version: 1, + Admin: addr1.String(), + TotalWeight: "3", + CreatedAt: s.blockTime, + }, + { + Id: 2, + Version: 1, + Admin: addr1.String(), + TotalWeight: "3", + CreatedAt: s.blockTime, + }, + } + + specs := map[string]struct { + req *group.MsgCreateGroup + expErr bool + expErrMsg string + expGroups []*group.GroupInfo + }{ + "all good": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + }, + expGroups: expGroups, + }, + "group metadata too long": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "group metadata: limit exceeded", + }, + "invalid member address": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: "invalid", + Weight: "1", + }}, + }, + expErr: true, + expErrMsg: "member address invalid", + }, + "member metadata too long": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "1", + Metadata: strings.Repeat("a", 256), + }}, + }, + expErr: true, + expErrMsg: "metadata: limit exceeded", + }, + "zero member weight": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "0", + }}, + }, + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid member weight - Inf": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "inf", + }}, + }, + expErr: true, + expErrMsg: "expected a finite decimal", + }, + "invalid member weight - NaN": { + req: &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "NaN", + }}, + }, + expErr: true, + expErrMsg: "expected a finite decimal", + }, + } + + var seq uint32 = 1 + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() + res, err := s.groupKeeper.CreateGroup(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + _, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: uint64(seq + 1)}) + s.Require().Error(err) + return + } + + s.Require().NoError(err) + id := res.GroupId + + seq++ + s.Assert().Equal(uint64(seq), id) + + // then all data persisted + loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id}) + s.Require().NoError(err) + s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin) + s.Assert().Equal(spec.req.Metadata, loadedGroupRes.Info.Metadata) + s.Assert().Equal(id, loadedGroupRes.Info.Id) + s.Assert().Equal(uint64(1), loadedGroupRes.Info.Version) + + // and members are stored as well + membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id}) + s.Require().NoError(err) + loadedMembers := membersRes.Members + s.Require().Equal(len(members), len(loadedMembers)) + // we reorder members by address to be able to compare them + sort.Slice(members, func(i, j int) bool { + addri, err := sdk.AccAddressFromBech32(members[i].Address) + s.Require().NoError(err) + addrj, err := sdk.AccAddressFromBech32(members[j].Address) + s.Require().NoError(err) + return bytes.Compare(addri, addrj) < 0 + }) + for i := range loadedMembers { + s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata) + s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address) + s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight) + s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(id, loadedMembers[i].GroupId) + } + + // query groups by admin + groupsRes, err := s.groupKeeper.GroupsByAdmin(s.ctx, &group.QueryGroupsByAdminRequest{Admin: addr1.String()}) + s.Require().NoError(err) + loadedGroups := groupsRes.Groups + s.Require().Equal(len(spec.expGroups), len(loadedGroups)) + for i := range loadedGroups { + s.Assert().Equal(spec.expGroups[i].Metadata, loadedGroups[i].Metadata) + s.Assert().Equal(spec.expGroups[i].Admin, loadedGroups[i].Admin) + s.Assert().Equal(spec.expGroups[i].TotalWeight, loadedGroups[i].TotalWeight) + s.Assert().Equal(spec.expGroups[i].Id, loadedGroups[i].Id) + s.Assert().Equal(spec.expGroups[i].Version, loadedGroups[i].Version) + s.Assert().Equal(spec.expGroups[i].CreatedAt, loadedGroups[i].CreatedAt) + } + }) + } +} + +func (s *TestSuite) TestUpdateGroupMembers() { + addrs := s.addrs + addr3 := addrs[2] + addr4 := addrs[3] + addr5 := addrs[4] + addr6 := addrs[5] + + member1 := addr5.String() + member2 := addr6.String() + members := []group.MemberRequest{{ + Address: member1, + Weight: "1", + }} + + myAdmin := addr4.String() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: myAdmin, + Members: members, + }) + s.Require().NoError(err) + groupID := groupRes.GroupId + + specs := map[string]struct { + req *group.MsgUpdateGroupMembers + expErr bool + expErrMsg string + expGroup *group.GroupInfo + expMembers []*group.GroupMember + }{ + "empty group id": { + req: &group.MsgUpdateGroupMembers{ + GroupId: 0, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member2, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "no new members": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{}, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "invalid member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + {}, + }, + }, + expErr: true, + expErrMsg: "unable to decode", + }, + "invalid member metadata too long": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + { + Address: member2, + Weight: "2", + Metadata: strings.Repeat("a", 256), + }, + }, + }, + expErr: true, + expErrMsg: "group member metadata: limit exceeded", + }, + "add new member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member2, + Weight: "2", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "3", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + Member: &group.Member{ + Address: member2, + Weight: "2", + AddedAt: s.sdkCtx.BlockTime(), + }, + GroupId: groupID, + }, + { + Member: &group.Member{ + Address: member1, + Weight: "1", + AddedAt: s.blockTime, + }, + GroupId: groupID, + }, + }, + }, + "update member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "2", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "2", + AddedAt: s.blockTime, + }, + }, + }, + }, + "update member with same data": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "1", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{ + { + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + AddedAt: s.blockTime, + }, + }, + }, + }, + "replace member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{ + { + Address: member1, + Weight: "0", + }, + { + Address: member2, + Weight: "1", + }, + }, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member2, + Weight: "1", + AddedAt: s.sdkCtx.BlockTime(), + }, + }}, + }, + "remove existing member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "0", + }}, + }, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "0", + Version: 2, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{}, + }, + "remove unknown member": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: addr4.String(), + Weight: "0", + }}, + }, + expErr: true, + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + }, + }}, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupMembers{ + GroupId: groupID, + Admin: addr3.String(), + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "not group admin", + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + }, + }}, + }, + "with unknown groupID": { + req: &group.MsgUpdateGroupMembers{ + GroupId: 999, + Admin: myAdmin, + MemberUpdates: []group.MemberRequest{{ + Address: member1, + Weight: "2", + }}, + }, + expErr: true, + expErrMsg: "not found", + expGroup: &group.GroupInfo{ + Id: groupID, + Admin: myAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + expMembers: []*group.GroupMember{{ + GroupId: groupID, + Member: &group.Member{ + Address: member1, + Weight: "1", + }, + }}, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + _, err := s.groupKeeper.UpdateGroupMembers(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroup, res.Info) + + // and members persisted + membersRes, err := s.groupKeeper.GroupMembers(sdkCtx, &group.QueryGroupMembersRequest{GroupId: groupID}) + s.Require().NoError(err) + loadedMembers := membersRes.Members + s.Require().Equal(len(spec.expMembers), len(loadedMembers)) + // we reorder group members by address to be able to compare them + sort.Slice(spec.expMembers, func(i, j int) bool { + addri, err := sdk.AccAddressFromBech32(spec.expMembers[i].Member.Address) + s.Require().NoError(err) + addrj, err := sdk.AccAddressFromBech32(spec.expMembers[j].Member.Address) + s.Require().NoError(err) + return bytes.Compare(addri, addrj) < 0 + }) + for i := range loadedMembers { + s.Assert().Equal(spec.expMembers[i].Member.Metadata, loadedMembers[i].Member.Metadata) + s.Assert().Equal(spec.expMembers[i].Member.Address, loadedMembers[i].Member.Address) + s.Assert().Equal(spec.expMembers[i].Member.Weight, loadedMembers[i].Member.Weight) + s.Assert().Equal(spec.expMembers[i].Member.AddedAt, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(spec.expMembers[i].GroupId, loadedMembers[i].GroupId) + } + + events := sdkCtx.EventManager().ABCIEvents() + s.Require().Len(events, 1) // EventUpdateGroup + }) + } +} + +func (s *TestSuite) TestUpdateGroupAdmin() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + addr3 := addrs[2] + addr4 := addrs[3] + + members := []group.MemberRequest{{ + Address: addr1.String(), + Weight: "1", + }} + oldAdmin := addr2.String() + newAdmin := addr3.String() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: oldAdmin, + Members: members, + }) + s.Require().NoError(err) + groupID := groupRes.GroupId + specs := map[string]struct { + req *group.MsgUpdateGroupAdmin + expStored *group.GroupInfo + expErr bool + expErrMsg string + }{ + "with no groupID": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: 0, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "value is empty", + }, + "with identical admin and new admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: oldAdmin, + NewAdmin: oldAdmin, + }, + expErr: true, + expErrMsg: "new and old admin are the same", + }, + "with correct admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: newAdmin, + TotalWeight: "1", + Version: 2, + CreatedAt: s.blockTime, + }, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: groupID, + Admin: addr4.String(), + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "not group admin", + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + "with unknown groupID": { + req: &group.MsgUpdateGroupAdmin{ + GroupId: 999, + Admin: oldAdmin, + NewAdmin: newAdmin, + }, + expErr: true, + expErrMsg: "not found", + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupAdmin(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: groupID}) + s.Require().NoError(err) + s.Assert().Equal(spec.expStored, res.Info) + }) + } +} + +func (s *TestSuite) TestUpdateGroupMetadata() { + addrs := s.addrs + addr1 := addrs[0] + addr3 := addrs[2] + + oldAdmin := addr1.String() + groupID := s.groupID + + specs := map[string]struct { + req *group.MsgUpdateGroupMetadata + expErr bool + expStored *group.GroupInfo + }{ + "with correct admin": { + req: &group.MsgUpdateGroupMetadata{ + GroupId: groupID, + Admin: oldAdmin, + }, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "3", + Version: 2, + CreatedAt: s.blockTime, + }, + }, + "with wrong admin": { + req: &group.MsgUpdateGroupMetadata{ + GroupId: groupID, + Admin: addr3.String(), + }, + expErr: true, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + "with unknown groupid": { + req: &group.MsgUpdateGroupMetadata{ + GroupId: 999, + Admin: oldAdmin, + }, + expErr: true, + expStored: &group.GroupInfo{ + Id: groupID, + Admin: oldAdmin, + TotalWeight: "1", + Version: 1, + CreatedAt: s.blockTime, + }, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + _, err := s.groupKeeper.UpdateGroupMetadata(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + // then + res, err := s.groupKeeper.GroupInfo(sdkCtx, &group.QueryGroupInfoRequest{GroupId: groupID}) + s.Require().NoError(err) + s.Assert().Equal(spec.expStored, res.Info) + + events := sdkCtx.EventManager().ABCIEvents() + s.Require().Len(events, 1) // EventUpdateGroup + }) + } +} + +func (s *TestSuite) TestCreateGroupWithPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr3 := addrs[2] + addr5 := addrs[4] + addr6 := addrs[5] + + s.setNextAccount() + + members := []group.MemberRequest{{ + Address: addr5.String(), + Weight: "1", + }, { + Address: addr6.String(), + Weight: "2", + }} + + specs := map[string]struct { + req *group.MsgCreateGroupWithPolicy + policy group.DecisionPolicy + malleate func() + expErr bool + expErrMsg string + }{ + "all good": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "group policy as admin is true": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: true, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "group metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + GroupMetadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "group metadata: limit exceeded", + }, + "group policy metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + GroupPolicyMetadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "group policy metadata: limit exceeded", + }, + "member metadata too long": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "1", + Metadata: strings.Repeat("a", 256), + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "member metadata: limit exceeded", + }, + "zero member weight": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: addr3.String(), + Weight: "0", + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid member address": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{{ + Address: "invalid", + Weight: "1", + }}, + GroupPolicyAsAdmin: false, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "unable to decode", + }, + "decision policy threshold > total group weight": { + req: &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupPolicyAsAdmin: false, + }, + malleate: func() { + s.setNextAccount() + }, + policy: group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + expErr: false, + }, + } + + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + s.setNextAccount() + err := spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + blockTime := sdk.UnwrapSDKContext(s.ctx).BlockTime() + res, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + id := res.GroupId + groupPolicyAddr := res.GroupPolicyAddress + + // then all data persisted in group + loadedGroupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: id}) + s.Require().NoError(err) + s.Assert().Equal(spec.req.GroupMetadata, loadedGroupRes.Info.Metadata) + s.Assert().Equal(id, loadedGroupRes.Info.Id) + if spec.req.GroupPolicyAsAdmin { + s.Assert().NotEqual(spec.req.Admin, loadedGroupRes.Info.Admin) + s.Assert().Equal(groupPolicyAddr, loadedGroupRes.Info.Admin) + } else { + s.Assert().Equal(spec.req.Admin, loadedGroupRes.Info.Admin) + } + + // and members are stored as well + membersRes, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{GroupId: id}) + s.Require().NoError(err) + loadedMembers := membersRes.Members + s.Require().Equal(len(members), len(loadedMembers)) + // we reorder members by address to be able to compare them + sort.Slice(members, func(i, j int) bool { + addri, err := sdk.AccAddressFromBech32(members[i].Address) + s.Require().NoError(err) + addrj, err := sdk.AccAddressFromBech32(members[j].Address) + s.Require().NoError(err) + return bytes.Compare(addri, addrj) < 0 + }) + for i := range loadedMembers { + s.Assert().Equal(members[i].Metadata, loadedMembers[i].Member.Metadata) + s.Assert().Equal(members[i].Address, loadedMembers[i].Member.Address) + s.Assert().Equal(members[i].Weight, loadedMembers[i].Member.Weight) + s.Assert().Equal(blockTime, loadedMembers[i].Member.AddedAt) + s.Assert().Equal(id, loadedMembers[i].GroupId) + } + + // then all data persisted in group policy + groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: groupPolicyAddr}) + s.Require().NoError(err) + + groupPolicy := groupPolicyRes.Info + s.Assert().Equal(groupPolicyAddr, groupPolicy.Address) + s.Assert().Equal(id, groupPolicy.GroupId) + s.Assert().Equal(spec.req.GroupPolicyMetadata, groupPolicy.Metadata) + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) + if spec.req.GroupPolicyAsAdmin { + s.Assert().NotEqual(spec.req.Admin, groupPolicy.Admin) + s.Assert().Equal(groupPolicyAddr, groupPolicy.Admin) + } else { + s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) + } + }) + } +} + +func (s *TestSuite) TestCreateGroupPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr4 := addrs[3] + + s.setNextAccount() + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: nil, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + specs := map[string]struct { + req *group.MsgCreateGroupPolicy + policy group.DecisionPolicy + expErr bool + expErrMsg string + }{ + "all good": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + }, + "all good with percentage decision policy": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "0.5", + time.Second, + 0, + ), + }, + "decision policy threshold > total group weight": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + }, + "group id does not exists": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: 9999, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "not found", + }, + "admin not group admin": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr4.String(), + GroupId: myGroupID, + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "not group admin", + }, + "metadata too long": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + Metadata: strings.Repeat("a", 256), + }, + policy: group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "limit exceeded", + }, + "percentage decision policy with negative value": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "-0.5", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "percentage decision policy with value greater than 1": { + req: &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + }, + policy: group.NewPercentageDecisionPolicy( + "2", + time.Second, + 0, + ), + expErr: true, + expErrMsg: "percentage must be > 0 and <= 1", + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + err := spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + s.setNextAccount() + + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + addr := res.Address + + // then all data persisted + groupPolicyRes, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{Address: addr}) + s.Require().NoError(err) + + groupPolicy := groupPolicyRes.Info + s.Assert().Equal(addr, groupPolicy.Address) + s.Assert().Equal(myGroupID, groupPolicy.GroupId) + s.Assert().Equal(spec.req.Admin, groupPolicy.Admin) + s.Assert().Equal(spec.req.Metadata, groupPolicy.Metadata) + s.Assert().Equal(uint64(1), groupPolicy.Version) + percentageDecisionPolicy, ok := spec.policy.(*group.PercentageDecisionPolicy) + if ok { + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(percentageDecisionPolicy, dp) + } else { + dp, err := groupPolicy.GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(spec.policy.(*group.ThresholdDecisionPolicy), dp) + } + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyAdmin() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + addr5 := addrs[4] + + admin, newAdmin := addr1, addr2 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + req *group.MsgUpdateGroupPolicyAdmin + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyAdmin{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + NewAdmin: newAdmin.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: newAdmin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + err := spec.expGroupPolicy.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyAdmin(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: groupPolicyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() { + addrs := s.addrs + addr1 := addrs[0] + addr5 := addrs[4] + + admin := addr1 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + preRun func(admin sdk.AccAddress) (policyAddr string, groupId uint64) + req *group.MsgUpdateGroupPolicyDecisionPolicy + policy group.DecisionPolicy + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: policy, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + }, + policy: policy, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "invalid percentage decision policy with negative value": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "-0.5", + time.Duration(1)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "expected a positive decimal", + }, + "invalid percentage decision policy with value greater than 1": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "2", + time.Duration(1)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: true, + expErrMsg: "percentage must be > 0 and <= 1", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewThresholdDecisionPolicy( + "2", + time.Duration(2)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + "correct data with percentage decision policy": { + preRun: func(admin sdk.AccAddress) (string, uint64) { + s.setNextAccount() + return s.createGroupAndGroupPolicy(admin, nil, policy) + }, + req: &group.MsgUpdateGroupPolicyDecisionPolicy{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + policy: group.NewPercentageDecisionPolicy( + "0.5", + time.Duration(2)*time.Second, + 0, + ), + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + DecisionPolicy: nil, + Version: 2, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + policyAddr := groupPolicyAddr + err := spec.expGroupPolicy.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + if spec.preRun != nil { + policyAddr1, groupID := spec.preRun(admin) + policyAddr = policyAddr1 + + // update the expected info with new group policy details + spec.expGroupPolicy.Address = policyAddr1 + spec.expGroupPolicy.GroupId = groupID + + // update req with new group policy addr + spec.req.GroupPolicyAddress = policyAddr1 + } + + err = spec.req.SetDecisionPolicy(spec.policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyDecisionPolicy(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: policyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + }) + } +} + +func (s *TestSuite) TestUpdateGroupPolicyMetadata() { + addrs := s.addrs + addr1 := addrs[0] + addr5 := addrs[4] + + admin := addr1 + policy := group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ) + + s.setNextAccount() + groupPolicyAddr, myGroupID := s.createGroupAndGroupPolicy(admin, nil, policy) + + specs := map[string]struct { + req *group.MsgUpdateGroupPolicyMetadata + expGroupPolicy *group.GroupPolicyInfo + expErr bool + expErrMsg string + }{ + "with wrong admin": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: addr5.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "not group policy admin: unauthorized", + }, + "with wrong group policy": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: addr5.String(), + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "load group policy: not found", + }, + "with metadata too long": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + Metadata: strings.Repeat("a", 1001), + }, + expGroupPolicy: &group.GroupPolicyInfo{}, + expErr: true, + expErrMsg: "group policy metadata: limit exceeded", + }, + "correct data": { + req: &group.MsgUpdateGroupPolicyMetadata{ + Admin: admin.String(), + GroupPolicyAddress: groupPolicyAddr, + }, + expGroupPolicy: &group.GroupPolicyInfo{ + Admin: admin.String(), + Address: groupPolicyAddr, + GroupId: myGroupID, + Version: 2, + DecisionPolicy: nil, + CreatedAt: s.blockTime, + }, + expErr: false, + }, + } + for msg, spec := range specs { + spec := spec + err := spec.expGroupPolicy.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.Run(msg, func() { + _, err := s.groupKeeper.UpdateGroupPolicyMetadata(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + res, err := s.groupKeeper.GroupPolicyInfo(s.ctx, &group.QueryGroupPolicyInfoRequest{ + Address: groupPolicyAddr, + }) + s.Require().NoError(err) + s.Assert().Equal(spec.expGroupPolicy, res.Info) + + // check events + var hasUpdateGroupPolicyEvent bool + events := s.ctx.(sdk.Context).EventManager().ABCIEvents() + for _, event := range events { + event, err := sdk.ParseTypedEvent(event) + s.Require().NoError(err) + + if e, ok := event.(*group.EventUpdateGroupPolicy); ok { + s.Require().Equal(e.Address, groupPolicyAddr) + hasUpdateGroupPolicyEvent = true + break + } + } + + s.Require().True(hasUpdateGroupPolicyEvent) + }) + } +} + +func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() { + addrs := s.addrs + addr2 := addrs[1] + + admin := addr2 + + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: admin.String(), + Members: nil, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + policies := []group.DecisionPolicy{ + group.NewThresholdDecisionPolicy( + "1", + time.Second, + 0, + ), + group.NewThresholdDecisionPolicy( + "10", + time.Second, + 0, + ), + group.NewPercentageDecisionPolicy( + "0.5", + time.Second, + 0, + ), + } + + count := 3 + expectAccs := make([]*group.GroupPolicyInfo, count) + for i := range expectAccs { + req := &group.MsgCreateGroupPolicy{ + Admin: admin.String(), + GroupId: myGroupID, + } + err := req.SetDecisionPolicy(policies[i]) + s.Require().NoError(err) + + s.setNextAccount() + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, req) + s.Require().NoError(err) + + expectAcc := &group.GroupPolicyInfo{ + Address: res.Address, + Admin: admin.String(), + GroupId: myGroupID, + Version: uint64(1), + CreatedAt: s.blockTime, + } + err = expectAcc.SetDecisionPolicy(policies[i]) + s.Require().NoError(err) + expectAccs[i] = expectAcc + } + sort.Slice(expectAccs, func(i, j int) bool { return expectAccs[i].Address < expectAccs[j].Address }) + + // query group policy by group + policiesByGroupRes, err := s.groupKeeper.GroupPoliciesByGroup(s.ctx, &group.QueryGroupPoliciesByGroupRequest{ + GroupId: myGroupID, + }) + s.Require().NoError(err) + policyAccs := policiesByGroupRes.GroupPolicies + s.Require().Equal(len(policyAccs), count) + // we reorder policyAccs by address to be able to compare them + sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) + for i := range policyAccs { + s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) + s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) + s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) + s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) + s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) + s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) + dp1, err := policyAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + dp2, err := expectAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(dp1, dp2) + } + + // query group policy by admin + policiesByAdminRes, err := s.groupKeeper.GroupPoliciesByAdmin(s.ctx, &group.QueryGroupPoliciesByAdminRequest{ + Admin: admin.String(), + }) + s.Require().NoError(err) + policyAccs = policiesByAdminRes.GroupPolicies + s.Require().Equal(len(policyAccs), count) + // we reorder policyAccs by address to be able to compare them + sort.Slice(policyAccs, func(i, j int) bool { return policyAccs[i].Address < policyAccs[j].Address }) + for i := range policyAccs { + s.Assert().Equal(policyAccs[i].Address, expectAccs[i].Address) + s.Assert().Equal(policyAccs[i].GroupId, expectAccs[i].GroupId) + s.Assert().Equal(policyAccs[i].Admin, expectAccs[i].Admin) + s.Assert().Equal(policyAccs[i].Metadata, expectAccs[i].Metadata) + s.Assert().Equal(policyAccs[i].Version, expectAccs[i].Version) + s.Assert().Equal(policyAccs[i].CreatedAt, expectAccs[i].CreatedAt) + dp1, err := policyAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + dp2, err := expectAccs[i].GetDecisionPolicy() + s.Assert().NoError(err) + s.Assert().Equal(dp1, dp2) + } +} + +func (s *TestSuite) TestSubmitProposal() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] // Has weight 2 + addr4 := addrs[3] + addr5 := addrs[4] // Has weight 1 + + myGroupID := s.groupID + accountAddr := s.groupPolicyAddr + + // Create a new group policy to test TRY_EXEC + policyReq := &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + } + noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy( + "2", + time.Second, + 0, // no MinExecutionPeriod to test TRY_EXEC + ) + err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy) + s.Require().NoError(err) + s.setNextAccount() + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + + s.accountKeeper.EXPECT().StringToBytes(res.Address).Return(sdk.MustAccAddressFromBech32(res.Address).Bytes(), nil).AnyTimes() + noMinExecPeriodPolicyAddr, err := s.accountKeeper.StringToBytes(res.Address) + s.Require().NoError(err) + + // Create a new group policy with super high threshold + bigThresholdPolicy := group.NewThresholdDecisionPolicy( + "100", + time.Second, + minExecutionPeriod, + ) + s.setNextAccount() + err = policyReq.SetDecisionPolicy(bigThresholdPolicy) + s.Require().NoError(err) + bigThresholdRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + bigThresholdAddr := bigThresholdRes.Address + + msgSend := &banktypes.MsgSend{ + FromAddress: res.Address, + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + defaultProposal := group.Proposal{ + GroupPolicyAddress: accountAddr.String(), + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + } + specs := map[string]struct { + req *group.MsgSubmitProposal + msgs []sdk.Msg + expProposal group.Proposal + expErr bool + expErrMsg string + postRun func(sdkCtx sdk.Context) + preRun func(msg []sdk.Msg) + }{ + "all good with minimal fields set": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + }, + expProposal: defaultProposal, + postRun: func(sdkCtx sdk.Context) {}, + }, + "all good with good msg payload": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + }, + msgs: []sdk.Msg{&banktypes.MsgSend{ + FromAddress: accountAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("token", 100)}, + }}, + expProposal: defaultProposal, + postRun: func(sdkCtx sdk.Context) {}, + }, + "metadata too long": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "limit exceeded", + postRun: func(sdkCtx sdk.Context) {}, + }, + "group policy required": { + req: &group.MsgSubmitProposal{ + Proposers: []string{addr2.String()}, + }, + expErr: true, + expErrMsg: "unable to decode", + postRun: func(sdkCtx sdk.Context) {}, + }, + "existing group policy required": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: addr1.String(), + Proposers: []string{addr2.String()}, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "decision policy threshold > total group weight": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: bigThresholdAddr, + Proposers: []string{addr2.String()}, + }, + expErr: false, + expProposal: group.Proposal{ + GroupPolicyAddress: bigThresholdAddr, + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.DefaultTallyResult(), + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + postRun: func(sdkCtx sdk.Context) {}, + }, + "only group members can create a proposal": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr4.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "all proposers must be in group": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String(), addr4.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "admin that is not a group member can not create proposal": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr1.String()}, + }, + expErr: true, + expErrMsg: "not in group", + postRun: func(sdkCtx sdk.Context) {}, + }, + "reject msgs that are not authz by group policy": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr.String(), + Proposers: []string{addr2.String()}, + }, + msgs: []sdk.Msg{&testdata.TestMsg{Signers: []string{addr1.String()}}}, + expErr: true, + expErrMsg: "msg does not have group policy authorization", + postRun: func(sdkCtx sdk.Context) {}, + }, + "with try exec": { + preRun: func(msgs []sdk.Msg) { + for i := 0; i < len(msgs); i++ { + s.bankKeeper.EXPECT().Send(gomock.Any(), msgs[i]).Return(nil, nil) + } + }, + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: res.Address, + Proposers: []string{addr2.String()}, + Exec: group.Exec_EXEC_TRY, + }, + msgs: []sdk.Msg{msgSend}, + expProposal: group.Proposal{ + GroupPolicyAddress: res.Address, + Status: group.PROPOSAL_STATUS_ACCEPTED, + FinalTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + postRun: func(sdkCtx sdk.Context) { + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr) + s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) + toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) + s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) + }, + }, + "with try exec, not enough yes votes for proposal to pass": { + req: &group.MsgSubmitProposal{ + GroupPolicyAddress: res.Address, + Proposers: []string{addr5.String()}, + Exec: group.Exec_EXEC_TRY, + }, + msgs: []sdk.Msg{msgSend}, + expProposal: group.Proposal{ + GroupPolicyAddress: res.Address, + Status: group.PROPOSAL_STATUS_SUBMITTED, + FinalTallyResult: group.TallyResult{ + YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + postRun: func(sdkCtx sdk.Context) {}, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + err := spec.req.SetMsgs(spec.msgs) + s.Require().NoError(err) + + if spec.preRun != nil { + spec.preRun(spec.msgs) + } + + res, err := s.groupKeeper.SubmitProposal(s.ctx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + id := res.ProposalId + + if !(spec.expProposal.ExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + // then all data persisted + proposalRes, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: id}) + s.Require().NoError(err) + proposal := proposalRes.Proposal + + s.Assert().Equal(spec.expProposal.GroupPolicyAddress, proposal.GroupPolicyAddress) + s.Assert().Equal(spec.req.Metadata, proposal.Metadata) + s.Assert().Equal(spec.req.Proposers, proposal.Proposers) + s.Assert().Equal(s.blockTime, proposal.SubmitTime) + s.Assert().Equal(uint64(1), proposal.GroupVersion) + s.Assert().Equal(uint64(1), proposal.GroupPolicyVersion) + s.Assert().Equal(spec.expProposal.Status, proposal.Status) + s.Assert().Equal(spec.expProposal.FinalTallyResult, proposal.FinalTallyResult) + s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult) + s.Assert().Equal(s.blockTime.Add(time.Second), proposal.VotingPeriodEnd) + + msgs, err := proposal.GetMsgs() + s.Assert().NoError(err) + if spec.msgs == nil { // then empty list is ok + s.Assert().Len(msgs, 0) + } else { + s.Assert().Equal(spec.msgs, msgs) + } + } + + spec.postRun(s.sdkCtx) + }) + } +} + +func (s *TestSuite) TestWithdrawProposal() { + addrs := s.addrs + addr2 := addrs[1] + addr5 := addrs[4] + + msgSend := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + + proposers := []string{addr2.String()} + proposalID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + + specs := map[string]struct { + preRun func(sdkCtx sdk.Context) uint64 + proposalID uint64 + admin string + expErrMsg string + }{ + "wrong admin": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + admin: addr5.String(), + expErrMsg: "unauthorized", + }, + "wrong proposal id": { + preRun: func(sdkCtx sdk.Context) uint64 { + return 1111 + }, + admin: proposers[0], + expErrMsg: "not found", + }, + "happy case with proposer": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + proposalID: proposalID, + admin: proposers[0], + }, + "already closed proposal": { + preRun: func(sdkCtx sdk.Context) uint64 { + pID := submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ + ProposalId: pID, + Address: proposers[0], + }) + s.Require().NoError(err) + return pID + }, + proposalID: proposalID, + admin: proposers[0], + expErrMsg: "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN", + }, + "happy case with group admin address": { + preRun: func(sdkCtx sdk.Context) uint64 { + return submitProposal(s.ctx, s, []sdk.Msg{msgSend}, proposers) + }, + proposalID: proposalID, + admin: proposers[0], + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + pID := spec.preRun(s.sdkCtx) + + _, err := s.groupKeeper.WithdrawProposal(s.ctx, &group.MsgWithdrawProposal{ + ProposalId: pID, + Address: spec.admin, + }) + + if spec.expErrMsg != "" { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + + s.Require().NoError(err) + resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: pID}) + s.Require().NoError(err) + s.Require().Equal(resp.GetProposal().Status, group.PROPOSAL_STATUS_WITHDRAWN) + }) + } +} + +func (s *TestSuite) TestVote() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + addr3 := addrs[2] + addr4 := addrs[3] + addr5 := addrs[4] + members := []group.MemberRequest{ + {Address: addr4.String(), Weight: "1"}, + {Address: addr3.String(), Weight: "2"}, + } + + groupRes, err := s.groupKeeper.CreateGroup(s.ctx, &group.MsgCreateGroup{ + Admin: addr1.String(), + Members: members, + }) + s.Require().NoError(err) + myGroupID := groupRes.GroupId + + policy := group.NewThresholdDecisionPolicy( + "2", + time.Duration(2), + 0, + ) + policyReq := &group.MsgCreateGroupPolicy{ + Admin: addr1.String(), + GroupId: myGroupID, + } + err = policyReq.SetDecisionPolicy(policy) + s.Require().NoError(err) + + s.setNextAccount() + policyRes, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + accountAddr := policyRes.Address + // module account will be created and returned + addrbz, err := address.NewBech32Codec("cosmos").StringToBytes(accountAddr) + s.Require().NoError(err) + s.accountKeeper.EXPECT().StringToBytes(accountAddr).Return(addrbz, nil).AnyTimes() + groupPolicy, err := s.accountKeeper.StringToBytes(accountAddr) + s.Require().NoError(err) + s.Require().NotNil(groupPolicy) + + s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)}).Return(nil).AnyTimes() + s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, groupPolicy, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) + + req := &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr, + Proposers: []string{addr4.String()}, + Messages: nil, + } + msg := &banktypes.MsgSend{ + FromAddress: accountAddr, + ToAddress: addr5.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + err = req.SetMsgs([]sdk.Msg{msg}) + s.Require().NoError(err) + + proposalRes, err := s.groupKeeper.SubmitProposal(s.ctx, req) + s.Require().NoError(err) + myProposalID := proposalRes.ProposalId + + // proposals by group policy + proposalsRes, err := s.groupKeeper.ProposalsByGroupPolicy(s.ctx, &group.QueryProposalsByGroupPolicyRequest{ + Address: accountAddr, + }) + s.Require().NoError(err) + proposals := proposalsRes.Proposals + s.Require().Equal(len(proposals), 1) + s.Assert().Equal(req.GroupPolicyAddress, proposals[0].GroupPolicyAddress) + s.Assert().Equal(req.Metadata, proposals[0].Metadata) + s.Assert().Equal(req.Proposers, proposals[0].Proposers) + s.Assert().Equal(s.blockTime, proposals[0].SubmitTime) + s.Assert().Equal(uint64(1), proposals[0].GroupVersion) + s.Assert().Equal(uint64(1), proposals[0].GroupPolicyVersion) + s.Assert().Equal(group.PROPOSAL_STATUS_SUBMITTED, proposals[0].Status) + s.Assert().Equal(group.DefaultTallyResult(), proposals[0].FinalTallyResult) + + specs := map[string]struct { + srcCtx sdk.Context + expTallyResult group.TallyResult // expected after tallying + isFinal bool // is the tally result final? + req *group.MsgVote + doBefore func(ctx context.Context) + postRun func(sdkCtx sdk.Context) + expProposalStatus group.ProposalStatus // expected after tallying + expExecutorResult group.ProposalExecutorResult // expected after tallying + expErr bool + expErrMsg string + }{ + "vote yes": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }, + expTallyResult: group.TallyResult{ + YesCount: "1", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "with try exec": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + Exec: group.Exec_EXEC_TRY, + }, + expTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + isFinal: true, + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + doBefore: func(ctx context.Context) { + s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) + }, + postRun: func(sdkCtx sdk.Context) { + s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), groupPolicy).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) + s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), addr5).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, groupPolicy) + s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) + toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr5) + s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) + }, + }, + "with try exec, not enough yes votes for proposal to pass": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + Exec: group.Exec_EXEC_TRY, + }, + expTallyResult: group.TallyResult{ + YesCount: "1", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote no": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "1", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote abstain": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_ABSTAIN, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "1", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote veto": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO_WITH_VETO, + }, + expTallyResult: group.TallyResult{ + YesCount: "0", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "1", + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "apply decision policy early": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + }, + expTallyResult: group.TallyResult{ + YesCount: "2", + NoCount: "0", + AbstainCount: "0", + NoWithVetoCount: "0", + }, + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + postRun: func(sdkCtx sdk.Context) {}, + }, + "reject new votes when final decision is made already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }, + doBefore: func(ctx context.Context) { + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_NO_WITH_VETO, + Exec: 1, // Execute the proposal so that its status is final + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "proposal not open for voting", + postRun: func(sdkCtx sdk.Context) {}, + }, + "metadata too long": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + Metadata: strings.Repeat("a", 256), + }, + expErr: true, + expErrMsg: "metadata: limit exceeded", + postRun: func(sdkCtx sdk.Context) {}, + }, + "existing proposal required": { + req: &group.MsgVote{ + ProposalId: 999, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "load proposal: not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "empty vote option": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + }, + expErr: true, + expErrMsg: "vote option: value is empty", + postRun: func(sdkCtx sdk.Context) {}, + }, + "invalid vote option": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: 5, + }, + expErr: true, + expErrMsg: "ote option: invalid value", + postRun: func(sdkCtx sdk.Context) {}, + }, + "voter must be in group": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr2.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "admin that is not a group member can not vote": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr1.String(), + Option: group.VOTE_OPTION_NO, + }, + expErr: true, + expErrMsg: "not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "on voting period end": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + srcCtx: s.sdkCtx.WithBlockTime(s.blockTime.Add(time.Second)), + expErr: true, + expErrMsg: "voting period has ended already: expired", + postRun: func(sdkCtx sdk.Context) {}, + }, + "vote closed already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + doBefore: func(ctx context.Context) { + s.bankKeeper.EXPECT().Send(gomock.Any(), msg).Return(nil, nil) + + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr3.String(), + Option: group.VOTE_OPTION_YES, + Exec: 1, // Execute to close the proposal. + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "load proposal: not found", + postRun: func(sdkCtx sdk.Context) {}, + }, + "voted already": { + req: &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_NO, + }, + doBefore: func(ctx context.Context) { + _, err := s.groupKeeper.Vote(ctx, &group.MsgVote{ + ProposalId: myProposalID, + Voter: addr4.String(), + Option: group.VOTE_OPTION_YES, + }) + s.Require().NoError(err) + }, + expErr: true, + expErrMsg: "store vote: unique constraint violation", + postRun: func(sdkCtx sdk.Context) {}, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx := s.sdkCtx + if !spec.srcCtx.IsZero() { + sdkCtx = spec.srcCtx + } + sdkCtx, _ = sdkCtx.CacheContext() + if spec.doBefore != nil { + spec.doBefore(sdkCtx) + } + _, err := s.groupKeeper.Vote(sdkCtx, spec.req) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + // vote is stored and all data persisted + res, err := s.groupKeeper.VoteByProposalVoter(sdkCtx, &group.QueryVoteByProposalVoterRequest{ + ProposalId: spec.req.ProposalId, + Voter: spec.req.Voter, + }) + s.Require().NoError(err) + loaded := res.Vote + s.Assert().Equal(spec.req.ProposalId, loaded.ProposalId) + s.Assert().Equal(spec.req.Voter, loaded.Voter) + s.Assert().Equal(spec.req.Option, loaded.Option) + s.Assert().Equal(spec.req.Metadata, loaded.Metadata) + s.Assert().Equal(s.blockTime, loaded.SubmitTime) + + // query votes by proposal + votesByProposalRes, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ + ProposalId: spec.req.ProposalId, + }) + s.Require().NoError(err) + votesByProposal := votesByProposalRes.Votes + s.Require().Equal(1, len(votesByProposal)) + vote := votesByProposal[0] + s.Assert().Equal(spec.req.ProposalId, vote.ProposalId) + s.Assert().Equal(spec.req.Voter, vote.Voter) + s.Assert().Equal(spec.req.Option, vote.Option) + s.Assert().Equal(spec.req.Metadata, vote.Metadata) + s.Assert().Equal(s.blockTime, vote.SubmitTime) + + // query votes by voter + voter := spec.req.Voter + votesByVoterRes, err := s.groupKeeper.VotesByVoter(sdkCtx, &group.QueryVotesByVoterRequest{ + Voter: voter, + }) + s.Require().NoError(err) + votesByVoter := votesByVoterRes.Votes + s.Require().Equal(1, len(votesByVoter)) + s.Assert().Equal(spec.req.ProposalId, votesByVoter[0].ProposalId) + s.Assert().Equal(voter, votesByVoter[0].Voter) + s.Assert().Equal(spec.req.Option, votesByVoter[0].Option) + s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata) + s.Assert().Equal(s.blockTime, votesByVoter[0].SubmitTime) + + proposalRes, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ + ProposalId: spec.req.ProposalId, + }) + s.Require().NoError(err) + + proposal := proposalRes.Proposal + if spec.isFinal { + s.Assert().Equal(spec.expTallyResult, proposal.FinalTallyResult) + s.Assert().Equal(spec.expProposalStatus, proposal.Status) + s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult) + } else { + s.Assert().Equal(group.DefaultTallyResult(), proposal.FinalTallyResult) // Make sure proposal isn't mutated. + + // do a round of tallying + tallyResult, err := s.groupKeeper.Tally(sdkCtx, *proposal, myGroupID) + s.Require().NoError(err) + + s.Assert().Equal(spec.expTallyResult, tallyResult) + } + } + + spec.postRun(sdkCtx) + }) + } + + s.T().Log("test tally result should not take into account the member who left the group") + members = []group.MemberRequest{ + {Address: addr2.String(), Weight: "3"}, + {Address: addr3.String(), Weight: "2"}, + {Address: addr4.String(), Weight: "1"}, + } + reqCreate := &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: members, + GroupMetadata: "metadata", + } + + policy = group.NewThresholdDecisionPolicy( + "4", + time.Duration(10), + 0, + ) + s.Require().NoError(reqCreate.SetDecisionPolicy(policy)) + s.setNextAccount() + + result, err := s.groupKeeper.CreateGroupWithPolicy(s.ctx, reqCreate) + s.Require().NoError(err) + s.Require().NotNil(result) + + policyAddr := result.GroupPolicyAddress + groupID := result.GroupId + reqProposal := &group.MsgSubmitProposal{ + GroupPolicyAddress: policyAddr, + Proposers: []string{addr4.String()}, + } + s.Require().NoError(reqProposal.SetMsgs([]sdk.Msg{&banktypes.MsgSend{ + FromAddress: policyAddr, + ToAddress: addr5.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + }})) + + resSubmitProposal, err := s.groupKeeper.SubmitProposal(s.ctx, reqProposal) + s.Require().NoError(err) + s.Require().NotNil(resSubmitProposal) + proposalID := resSubmitProposal.ProposalId + + for _, voter := range []string{addr4.String(), addr3.String(), addr2.String()} { + _, err := s.groupKeeper.Vote(s.ctx, + &group.MsgVote{ProposalId: proposalID, Voter: voter, Option: group.VOTE_OPTION_YES}, + ) + s.Require().NoError(err) + } + + qProposals, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ + ProposalId: proposalID, + }) + s.Require().NoError(err) + + tallyResult, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) + s.Require().NoError(err) + + _, err = s.groupKeeper.LeaveGroup(s.ctx, &group.MsgLeaveGroup{Address: addr4.String(), GroupId: groupID}) + s.Require().NoError(err) + + tallyResult1, err := s.groupKeeper.Tally(s.sdkCtx, *qProposals.Proposal, groupID) + s.Require().NoError(err) + s.Require().NotEqual(tallyResult.String(), tallyResult1.String()) +} + +func (s *TestSuite) TestExecProposal() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 10001)}, + } + proposers := []string{addr2.String()} + + specs := map[string]struct { + srcBlockTime time.Time + setupProposal func(ctx context.Context) uint64 + expErr bool + expErrMsg string + expProposalStatus group.ProposalStatus + expExecutorResult group.ProposalExecutorResult + expBalance bool + expFromBalances sdk.Coin + expToBalances sdk.Coin + }{ + "proposal executed when accepted": { + setupProposal: func(ctx context.Context) uint64 { + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9900), + expToBalances: sdk.NewInt64Coin("test", 100), + }, + "proposal with multiple messages executed when accepted": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1, msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9800), + expToBalances: sdk.NewInt64Coin("test", 200), + }, + "proposal not executed when rejected": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "open proposal must not fail": { + setupProposal: func(ctx context.Context) uint64 { + return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + }, + expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "invalid proposal id": { + setupProposal: func(ctx context.Context) uint64 { + return 0 + }, + expErr: true, + expErrMsg: "proposal id: value is empty", + }, + "existing proposal required": { + setupProposal: func(ctx context.Context) uint64 { + return 9999 + }, + expErr: true, + expErrMsg: "load proposal: not found", + }, + "Decision policy also applied on exactly voting period end": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(time.Second), // Voting period is 1s + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "Decision policy also applied after voting period end": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s + expProposalStatus: group.PROPOSAL_STATUS_REJECTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "exec proposal before MinExecutionPeriod should fail": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed + }, + "exec proposal at exactly MinExecutionPeriod should pass": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + "prevent double execution when successful": { + setupProposal: func(ctx context.Context) uint64 { + myProposalID := submitProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.VOTE_OPTION_YES) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + + // Wait after min execution period end before Exec + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.Require().NoError(err) + return myProposalID + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expErr: true, // since proposal is pruned after a successful MsgExec + expErrMsg: "load proposal: not found", + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + expBalance: true, + expFromBalances: sdk.NewInt64Coin("test", 9900), + expToBalances: sdk.NewInt64Coin("test", 100), + }, + "rollback all msg updates on failure": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1, msgSend2} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, + }, + "executable when failed before": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend2} + myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + + // Wait after min execution period end before Exec + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) + + s.Require().NoError(err) + s.Require().NoError(s.bankKeeper.SendCoinsFromModuleToAccount(s.sdkCtx, minttypes.ModuleName, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) + + return myProposalID + }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + proposalID := spec.setupProposal(sdkCtx) + + if !spec.srcBlockTime.IsZero() { + sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) + } + + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if !(spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS) { + + // and proposal is updated + res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + proposal := res.Proposal + + exp := group.ProposalStatus_name[int32(spec.expProposalStatus)] + got := group.ProposalStatus_name[int32(proposal.Status)] + s.Assert().Equal(exp, got) + + exp = group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] + got = group.ProposalExecutorResult_name[int32(proposal.ExecutorResult)] + s.Assert().Equal(exp, got) + } + + if spec.expBalance { + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, s.groupPolicyAddr).Return(sdk.Coins{spec.expFromBalances}) + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.Coins{spec.expToBalances}) + + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, s.groupPolicyAddr) + s.Require().Contains(fromBalances, spec.expFromBalances) + toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) + s.Require().Contains(toBalances, spec.expToBalances) + } + }) + } +} + +func (s *TestSuite) TestExecPrunedProposalsAndVotes() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + + proposers := []string{addr2.String()} + specs := map[string]struct { + srcBlockTime time.Time + setupProposal func(ctx context.Context) uint64 + expErr bool + expErrMsg string + expExecutorResult group.ProposalExecutorResult + }{ + "proposal pruned after executor result success": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 101)}, + } + msgs := []sdk.Msg{msgSend1} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + "proposal with multiple messages pruned when executed with result success": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 102)}, + } + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil).MaxTimes(2) + + msgs := []sdk.Msg{msgSend1, msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + "proposal not pruned when not executed and rejected": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 103)}, + } + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "open proposal is not pruned which must not fail ": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 104)}, + } + return submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal not pruned with group modified before tally": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 105)}, + } + myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + + // then modify group + _, err := s.groupKeeper.UpdateGroupMetadata(ctx, &group.MsgUpdateGroupMetadata{ + Admin: addr1.String(), + GroupId: s.groupID, + }) + s.Require().NoError(err) + return myProposalID + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal not pruned with group policy modified before tally": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 106)}, + } + + myProposalID := submitProposal(ctx, s, []sdk.Msg{msgSend1}, proposers) + _, err := s.groupKeeper.UpdateGroupPolicyMetadata(ctx, &group.MsgUpdateGroupPolicyMetadata{ + Admin: addr1.String(), + GroupPolicyAddress: s.groupPolicyAddr.String(), + }) + s.Require().NoError(err) + return myProposalID + }, + expErr: true, // since proposal status will be `aborted` when group policy is modified + expErrMsg: "not possible to exec with proposal status", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, + }, + "proposal exists when rollback all msg updates on failure": { + setupProposal: func(ctx context.Context) uint64 { + msgSend1 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 107)}, + } + + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 10002)}, + } + + msgs := []sdk.Msg{msgSend1, msgSend2} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, fmt.Errorf("error")) + + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, + }, + "pruned when proposal is executable when failed before": { + setupProposal: func(ctx context.Context) uint64 { + msgSend2 := &banktypes.MsgSend{ + FromAddress: s.groupPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 10003)}, + } + + msgs := []sdk.Msg{msgSend2} + + myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + + // Wait for min execution period end + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) + + s.Require().NoError(err) + return myProposalID + }, + expErrMsg: "load proposal: not found", + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, + } + for msg, spec := range specs { + spec := spec + s.Run(msg, func() { + sdkCtx, _ := s.sdkCtx.CacheContext() + proposalID := spec.setupProposal(sdkCtx) + + if !spec.srcBlockTime.IsZero() { + sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) + } + + // Wait for min execution period end + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + _, err := s.groupKeeper.Exec(sdkCtx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) + if spec.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), spec.expErrMsg) + return + } + s.Require().NoError(err) + + if spec.expExecutorResult == group.PROPOSAL_EXECUTOR_RESULT_SUCCESS { + // Make sure proposal is deleted from state + _, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().Contains(err.Error(), spec.expErrMsg) + res, err := s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + s.Require().Empty(res.GetVotes()) + + } else { + // Check that proposal and votes exists + res, err := s.groupKeeper.Proposal(sdkCtx, &group.QueryProposalRequest{ProposalId: proposalID}) + s.Require().NoError(err) + _, err = s.groupKeeper.VotesByProposal(sdkCtx, &group.QueryVotesByProposalRequest{ProposalId: res.Proposal.Id}) + s.Require().NoError(err) + s.Require().Equal("", spec.expErrMsg) + + exp := group.ProposalExecutorResult_name[int32(spec.expExecutorResult)] + got := group.ProposalExecutorResult_name[int32(res.Proposal.ExecutorResult)] + s.Assert().Equal(exp, got) + } + }) + } +} + +func (s *TestSuite) TestLeaveGroup() { + addrs := simtestutil.CreateIncrementalAccounts(7) + + admin1 := addrs[0] + member1 := addrs[1] + member2 := addrs[2] + member3 := addrs[3] + member4 := addrs[4] + admin2 := addrs[5] + admin3 := addrs[6] + + for _, addr := range addrs { + s.accountKeeper.EXPECT().StringToBytes(addr.String()).Return(addr.Bytes(), nil).AnyTimes() + s.accountKeeper.EXPECT().BytesToString(addr).Return(addr.String(), nil).AnyTimes() + } + + members := []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + { + Address: member2.String(), + Weight: "2", + Metadata: "metadata", + }, + { + Address: member3.String(), + Weight: "3", + Metadata: "metadata", + }, + } + policy := group.NewThresholdDecisionPolicy( + "3", + time.Hour, + time.Hour, + ) + s.setNextAccount() + _, groupID1 := s.createGroupAndGroupPolicy(admin1, members, policy) + + members = []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + } + + s.setNextAccount() + _, groupID2 := s.createGroupAndGroupPolicy(admin2, members, nil) + + members = []group.MemberRequest{ + { + Address: member1.String(), + Weight: "1", + Metadata: "metadata", + }, + { + Address: member2.String(), + Weight: "2", + Metadata: "metadata", + }, + } + policy = &group.PercentageDecisionPolicy{ + Percentage: "0.5", + Windows: &group.DecisionPolicyWindows{VotingPeriod: time.Hour}, + } + + s.setNextAccount() + + _, groupID3 := s.createGroupAndGroupPolicy(admin3, members, policy) + testCases := []struct { + name string + req *group.MsgLeaveGroup + expErr bool + expErrMsg string + expMembersSize int + memberWeight math.Dec + }{ + { + "group not found", + &group.MsgLeaveGroup{ + GroupId: 100000, + Address: member1.String(), + }, + true, + "group: not found", + 0, + math.NewDecFromInt64(0), + }, + { + "member address invalid", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: "invalid", + }, + true, + "unable to decode", + 0, + math.NewDecFromInt64(0), + }, + { + "member not part of group", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member4.String(), + }, + true, + "not part of group", + 0, + math.NewDecFromInt64(0), + }, + { + "valid testcase: decision policy is not present (and group total weight can be 0)", + &group.MsgLeaveGroup{ + GroupId: groupID2, + Address: member1.String(), + }, + false, + "", + 0, + math.NewDecFromInt64(1), + }, + { + "valid testcase: threshold decision policy", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member3.String(), + }, + false, + "", + 2, + math.NewDecFromInt64(3), + }, + { + "valid request: can leave group policy threshold more than group weight", + &group.MsgLeaveGroup{ + GroupId: groupID1, + Address: member2.String(), + }, + false, + "", + 1, + math.NewDecFromInt64(2), + }, + { + "valid request: can leave group (percentage decision policy)", + &group.MsgLeaveGroup{ + GroupId: groupID3, + Address: member2.String(), + }, + false, + "", + 1, + math.NewDecFromInt64(2), + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + var groupWeight1 math.Dec + if !tc.expErr { + groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) + s.Require().NoError(err) + groupWeight1, err = math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) + s.Require().NoError(err) + } + + res, err := s.groupKeeper.LeaveGroup(s.ctx, tc.req) + if tc.expErr { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expErrMsg) + } else { + s.Require().NoError(err) + s.Require().NotNil(res) + res, err := s.groupKeeper.GroupMembers(s.ctx, &group.QueryGroupMembersRequest{ + GroupId: tc.req.GroupId, + }) + s.Require().NoError(err) + s.Require().Len(res.Members, tc.expMembersSize) + + groupRes, err := s.groupKeeper.GroupInfo(s.ctx, &group.QueryGroupInfoRequest{GroupId: tc.req.GroupId}) + s.Require().NoError(err) + groupWeight2, err := math.NewNonNegativeDecFromString(groupRes.Info.TotalWeight) + s.Require().NoError(err) + + rWeight, err := groupWeight1.Sub(tc.memberWeight) + s.Require().NoError(err) + s.Require().Equal(rWeight.Cmp(groupWeight2), 0) + } + }) + } +} diff --git a/x/group/msgs.go b/x/group/msgs.go index a22a8fc403..ed00d5cc47 100644 --- a/x/group/msgs.go +++ b/x/group/msgs.go @@ -11,8 +11,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" "github.com/cosmos/cosmos-sdk/x/group/codec" - errors "github.com/cosmos/cosmos-sdk/x/group/errors" - "github.com/cosmos/cosmos-sdk/x/group/internal/math" ) var ( @@ -32,34 +30,6 @@ func (m MsgCreateGroup) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgCreateGroup) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - return strictValidateMembers(m.Members) -} - -// ValidateBasic performs stateless validation on a group member, such as -// making sure the address is well-formed, and the weight is non-negative. -// Note: in state, a member's weight MUST be positive. However, in some Msgs, -// it's possible to set a zero member weight, for example in -// MsgUpdateGroupMembers to denote that we're removing a member. -func (m MemberRequest) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Address) - if err != nil { - return errorsmod.Wrap(err, "address") - } - - if _, err := math.NewNonNegativeDecFromString(m.Weight); err != nil { - return errorsmod.Wrap(err, "weight") - } - - return nil -} - var ( _ sdk.Msg = &MsgUpdateGroupAdmin{} _ legacytx.LegacyMsg = &MsgUpdateGroupAdmin{} @@ -77,28 +47,6 @@ func (m MsgUpdateGroupAdmin) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data. -func (m MsgUpdateGroupAdmin) ValidateBasic() error { - if m.GroupId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "group id") - } - - admin, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - newAdmin, err := sdk.AccAddressFromBech32(m.NewAdmin) - if err != nil { - return errorsmod.Wrap(err, "new admin") - } - - if admin.Equals(newAdmin) { - return errorsmod.Wrap(errors.ErrInvalid, "new and old admin are the same") - } - return nil -} - // GetGroupID gets the group id of the MsgUpdateGroupAdmin. func (m *MsgUpdateGroupAdmin) GetGroupID() uint64 { return m.GroupId @@ -121,19 +69,6 @@ func (m MsgUpdateGroupMetadata) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgUpdateGroupMetadata) ValidateBasic() error { - if m.GroupId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "group id") - } - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - return nil -} - // GetGroupID gets the group id of the MsgUpdateGroupMetadata. func (m *MsgUpdateGroupMetadata) GetGroupID() uint64 { return m.GroupId @@ -158,26 +93,6 @@ func (m MsgUpdateGroupMembers) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgUpdateGroupMembers) ValidateBasic() error { - if m.GroupId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "group id") - } - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - if len(m.MemberUpdates) == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "member updates") - } - members := MemberRequests{Members: m.MemberUpdates} - if err := members.ValidateBasic(); err != nil { - return errorsmod.Wrap(err, "members") - } - return nil -} - // GetGroupID gets the group id of the MsgUpdateGroupMembers. func (m *MsgUpdateGroupMembers) GetGroupID() uint64 { return m.GroupId @@ -242,23 +157,6 @@ func (m MsgCreateGroupWithPolicy) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgCreateGroupWithPolicy) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - policy, err := m.GetDecisionPolicy() - if err != nil { - return errorsmod.Wrap(err, "decision policy") - } - if err := policy.ValidateBasic(); err != nil { - return errorsmod.Wrap(err, "decision policy") - } - - return strictValidateMembers(m.Members) -} - var ( _ sdk.Msg = &MsgCreateGroupPolicy{} _ legacytx.LegacyMsg = &MsgCreateGroupPolicy{} @@ -275,27 +173,6 @@ func (m MsgCreateGroupPolicy) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgCreateGroupPolicy) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - if m.GroupId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "group id") - } - - policy, err := m.GetDecisionPolicy() - if err != nil { - return errorsmod.Wrap(err, "decision policy") - } - - if err := policy.ValidateBasic(); err != nil { - return errorsmod.Wrap(err, "decision policy") - } - return nil -} - var ( _ sdk.Msg = &MsgUpdateGroupPolicyAdmin{} _ legacytx.LegacyMsg = &MsgUpdateGroupPolicyAdmin{} @@ -313,29 +190,6 @@ func (m MsgUpdateGroupPolicyAdmin) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgUpdateGroupPolicyAdmin) ValidateBasic() error { - admin, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - newAdmin, err := sdk.AccAddressFromBech32(m.NewAdmin) - if err != nil { - return errorsmod.Wrap(err, "new admin") - } - - _, err = sdk.AccAddressFromBech32(m.GroupPolicyAddress) - if err != nil { - return errorsmod.Wrap(err, "group policy") - } - - if admin.Equals(newAdmin) { - return errorsmod.Wrap(errors.ErrInvalid, "new and old admin are same") - } - return nil -} - var ( _ sdk.Msg = &MsgUpdateGroupPolicyDecisionPolicy{} _ legacytx.LegacyMsg = &MsgUpdateGroupPolicyDecisionPolicy{} @@ -382,30 +236,6 @@ func (m MsgUpdateGroupPolicyDecisionPolicy) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgUpdateGroupPolicyDecisionPolicy) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - _, err = sdk.AccAddressFromBech32(m.GroupPolicyAddress) - if err != nil { - return errorsmod.Wrap(err, "group policy") - } - - policy, err := m.GetDecisionPolicy() - if err != nil { - return errorsmod.Wrap(err, "decision policy") - } - - if err := policy.ValidateBasic(); err != nil { - return errorsmod.Wrap(err, "decision policy") - } - - return nil -} - // GetDecisionPolicy gets the decision policy of MsgUpdateGroupPolicyDecisionPolicy. func (m *MsgUpdateGroupPolicyDecisionPolicy) GetDecisionPolicy() (DecisionPolicy, error) { decisionPolicy, ok := m.DecisionPolicy.GetCachedValue().(DecisionPolicy) @@ -439,21 +269,6 @@ func (m MsgUpdateGroupPolicyMetadata) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgUpdateGroupPolicyMetadata) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Admin) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - _, err = sdk.AccAddressFromBech32(m.GroupPolicyAddress) - if err != nil { - return errorsmod.Wrap(err, "group policy") - } - - return nil -} - var ( _ sdk.Msg = &MsgCreateGroupPolicy{} _ legacytx.LegacyMsg = &MsgCreateGroupPolicy{} @@ -552,54 +367,6 @@ func (m MsgSubmitProposal) GetSigners() []sdk.AccAddress { return addrs } -// ValidateBasic does a sanity check on the provided proposal, such as -// verifying proposer addresses, and performing ValidateBasic on each -// individual `sdk.Msg`. -func (m MsgSubmitProposal) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.GroupPolicyAddress) - if err != nil { - return errorsmod.Wrap(err, "group policy") - } - - if m.Title == "" { - return errorsmod.Wrap(errors.ErrEmpty, "title") - } - - if m.Summary == "" { - return errorsmod.Wrap(errors.ErrEmpty, "summary") - } - - if len(m.Proposers) == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "proposers") - } - - addrs, err := m.getProposerAccAddresses() - if err != nil { - return errorsmod.Wrap(err, "group proposers") - } - - if err := accAddresses(addrs).ValidateBasic(); err != nil { - return errorsmod.Wrap(err, "proposers") - } - - msgs, err := m.GetMsgs() - if err != nil { - return err - } - - for i, msg := range msgs { - m, ok := msg.(sdk.HasValidateBasic) - if !ok { - continue - } - - if err := m.ValidateBasic(); err != nil { - return errorsmod.Wrapf(err, "msg %d", i) - } - } - return nil -} - // getProposerAccAddresses returns the proposers as `[]sdk.AccAddress`. func (m *MsgSubmitProposal) getProposerAccAddresses() ([]sdk.AccAddress, error) { addrs := make([]sdk.AccAddress, len(m.Proposers)) @@ -651,20 +418,6 @@ func (m MsgWithdrawProposal) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{admin} } -// ValidateBasic does a sanity check on the provided data -func (m MsgWithdrawProposal) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Address) - if err != nil { - return errorsmod.Wrap(err, "admin") - } - - if m.ProposalId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "proposal id") - } - - return nil -} - var ( _ sdk.Msg = &MsgVote{} _ legacytx.LegacyMsg = &MsgVote{} @@ -682,24 +435,6 @@ func (m MsgVote) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{addr} } -// ValidateBasic does a sanity check on the provided data -func (m MsgVote) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Voter) - if err != nil { - return errorsmod.Wrap(err, "voter") - } - if m.ProposalId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "proposal id") - } - if m.Option == VOTE_OPTION_UNSPECIFIED { - return errorsmod.Wrap(errors.ErrEmpty, "vote option") - } - if _, ok := VoteOption_name[int32(m.Option)]; !ok { - return errorsmod.Wrap(errors.ErrInvalid, "vote option") - } - return nil -} - var ( _ sdk.Msg = &MsgExec{} _ legacytx.LegacyMsg = &MsgExec{} @@ -717,18 +452,6 @@ func (m MsgExec) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{signer} } -// ValidateBasic does a sanity check on the provided data -func (m MsgExec) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Executor) - if err != nil { - return errorsmod.Wrap(err, "signer") - } - if m.ProposalId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "proposal id") - } - return nil -} - var ( _ sdk.Msg = &MsgLeaveGroup{} _ legacytx.LegacyMsg = &MsgLeaveGroup{} @@ -745,34 +468,3 @@ func (m MsgLeaveGroup) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{signer} } - -// ValidateBasic does a sanity check on the provided data -func (m MsgLeaveGroup) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(m.Address) - if err != nil { - return errorsmod.Wrap(err, "group member") - } - - if m.GroupId == 0 { - return errorsmod.Wrap(errors.ErrEmpty, "group-id") - } - return nil -} - -// strictValidateMembers performs ValidateBasic on Members, but also checks -// that all members weights are positive (whereas `Members{members}.ValidateBasic()` -// only checks that they are non-negative. -func strictValidateMembers(members []MemberRequest) error { - err := MemberRequests{members}.ValidateBasic() - if err != nil { - return err - } - - for _, m := range members { - if _, err := math.NewPositiveDecFromString(m.Weight); err != nil { - return errorsmod.Wrap(err, "weight") - } - } - - return nil -} diff --git a/x/group/msgs_test.go b/x/group/msgs_test.go deleted file mode 100644 index acfdeb5b6f..0000000000 --- a/x/group/msgs_test.go +++ /dev/null @@ -1,1232 +0,0 @@ -package group_test - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module/testutil" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/cosmos/cosmos-sdk/x/group" - "github.com/cosmos/cosmos-sdk/x/group/module" -) - -var ( - admin = sdk.AccAddress("admin") - member1 = sdk.AccAddress("member1") - member2 = sdk.AccAddress("member2") - member3 = sdk.AccAddress("member3") - member4 = sdk.AccAddress("member4") - member5 = sdk.AccAddress("member5") -) - -func TestMsgCreateGroup(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgCreateGroup - expErr bool - errMsg string - }{ - { - "invalid admin address", - &group.MsgCreateGroup{ - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "invalid member address", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: "invalid address", - }, - }, - }, - true, - "address: decoding bech32 failed", - }, - { - "negitive member's weight not allowed", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "-1", - }, - }, - }, - true, - "expected a non-negative decimal", - }, - { - "zero member's weight not allowed", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "0", - }, - }, - }, - true, - "expected a positive decimal", - }, - { - "duplicate member not allowed", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - }, - }, - true, - "duplicate value", - }, - { - "valid test case with single member", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - }, - }, - false, - "", - }, - { - "minimum fields", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{}, - }, - false, - "", - }, - { - "valid test case with multiple members", - &group.MsgCreateGroup{ - Admin: admin.String(), - Members: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member2.String(), - Weight: "1", - Metadata: "metadata", - }, - }, - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(tc.msg), sdk.MsgTypeURL(&group.MsgCreateGroup{})) - } - }) - } -} - -func TestMsgUpdateGroupAdmin(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgUpdateGroupAdmin - expErr bool - errMsg string - }{ - { - "empty group id", - &group.MsgUpdateGroupAdmin{ - Admin: admin.String(), - NewAdmin: member1.String(), - }, - true, - "group id: value is empty", - }, - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupAdmin{ - GroupId: 1, - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "new admin: invalid bech32 address", - &group.MsgUpdateGroupAdmin{ - GroupId: 1, - Admin: admin.String(), - NewAdmin: "new-admin", - }, - true, - "new admin: decoding bech32 failed", - }, - { - "admin & new admin is same", - &group.MsgUpdateGroupAdmin{ - GroupId: 1, - Admin: admin.String(), - NewAdmin: admin.String(), - }, - true, - "new and old admin are the same", - }, - { - "valid case", - &group.MsgUpdateGroupAdmin{ - GroupId: 1, - Admin: admin.String(), - NewAdmin: member1.String(), - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(tc.msg), sdk.MsgTypeURL(&group.MsgUpdateGroupAdmin{})) - } - }) - } -} - -func TestMsgUpdateGroupMetadata(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgUpdateGroupMetadata - expErr bool - errMsg string - }{ - { - "empty group id", - &group.MsgUpdateGroupMetadata{ - Admin: admin.String(), - }, - true, - "group id: value is empty", - }, - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupMetadata{ - GroupId: 1, - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "valid test", - &group.MsgUpdateGroupMetadata{ - GroupId: 1, - Admin: admin.String(), - Metadata: "metadata", - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(tc.msg), sdk.MsgTypeURL(&group.MsgUpdateGroupMetadata{})) - } - }) - } -} - -func TestMsgUpdateGroupMembers(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgUpdateGroupMembers - expErr bool - errMsg string - }{ - { - "empty group id", - &group.MsgUpdateGroupMembers{}, - true, - "group id: value is empty", - }, - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupMembers{ - GroupId: 1, - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "empty member list", - &group.MsgUpdateGroupMembers{ - GroupId: 1, - Admin: admin.String(), - MemberUpdates: []group.MemberRequest{}, - }, - true, - "member updates: value is empty", - }, - { - "valid test", - &group.MsgUpdateGroupMembers{ - GroupId: 1, - Admin: admin.String(), - MemberUpdates: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - }, - }, - false, - "", - }, - { - "valid test with zero weight", - &group.MsgUpdateGroupMembers{ - GroupId: 1, - Admin: admin.String(), - MemberUpdates: []group.MemberRequest{ - { - Address: member1.String(), - Weight: "0", - Metadata: "metadata", - }, - }, - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := tc.msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(tc.msg), sdk.MsgTypeURL(&group.MsgUpdateGroupMembers{})) - } - }) - } -} - -func TestMsgCreateGroupWithPolicy(t *testing.T) { - testCases := []struct { - name string - msg func() *group.MsgCreateGroupWithPolicy - expErr bool - errMsg string - }{ - { - "invalid admin address", - func() *group.MsgCreateGroupWithPolicy { - admin := "admin" - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin, members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "admin: decoding bech32 failed", - }, - { - "invalid member address", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: "invalid_address", - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "address: decoding bech32 failed", - }, - { - "negative member's weight not allowed", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "-1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "expected a non-negative decimal", - }, - { - "zero member's weight not allowed", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "0", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "duplicate member not allowed", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "duplicate value", - }, - { - "invalid threshold policy", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("-1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "valid test case with single member", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - false, - "", - }, - { - "valid test case with multiple members", - func() *group.MsgCreateGroupWithPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - members := []group.MemberRequest{ - { - Address: member1.String(), - Weight: "1", - Metadata: "metadata", - }, - { - Address: member2.String(), - Weight: "1", - Metadata: "metadata", - }, - } - req, err := group.NewMsgCreateGroupWithPolicy(admin.String(), members, "group_metadata", "group_policy_metadata", false, policy) - require.NoError(t, err) - return req - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg() - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgCreateGroupWithPolicy{})) - } - }) - } -} - -func TestMsgCreateGroupPolicy(t *testing.T) { - testCases := []struct { - name string - msg func() *group.MsgCreateGroupPolicy - expErr bool - errMsg string - }{ - { - "empty group id", - func() *group.MsgCreateGroupPolicy { - return &group.MsgCreateGroupPolicy{ - Admin: admin.String(), - } - }, - true, - "group id: value is empty", - }, - { - "admin: invalid bech32 address", - func() *group.MsgCreateGroupPolicy { - return &group.MsgCreateGroupPolicy{ - Admin: "admin", - GroupId: 1, - } - }, - true, - "admin: decoding bech32 failed", - }, - { - "invalid threshold policy", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("-1", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "invalid voting period", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("-1", time.Duration(0), 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "invalid execution period", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("-1", time.Minute, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "valid test case, only voting period", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - false, - "", - }, - { - "valid test case, voting and execution, empty min exec period", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - false, - "", - }, - { - "valid test case, voting and execution, non-empty min exec period", - func() *group.MsgCreateGroupPolicy { - policy := group.NewThresholdDecisionPolicy("1", time.Second, time.Minute) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", policy) - require.NoError(t, err) - return req - }, - false, - "", - }, - { - "invalid percentage decision policy with zero value", - func() *group.MsgCreateGroupPolicy { - percentagePolicy := group.NewPercentageDecisionPolicy("0", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", percentagePolicy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "invalid percentage decision policy with negative value", - func() *group.MsgCreateGroupPolicy { - percentagePolicy := group.NewPercentageDecisionPolicy("-0.2", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", percentagePolicy) - require.NoError(t, err) - return req - }, - true, - "expected a positive decimal", - }, - { - "invalid percentage decision policy with value greater than 1", - func() *group.MsgCreateGroupPolicy { - percentagePolicy := group.NewPercentageDecisionPolicy("2", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", percentagePolicy) - require.NoError(t, err) - return req - }, - true, - "percentage must be > 0 and <= 1", - }, - { - "valid test case with percentage decision policy", - func() *group.MsgCreateGroupPolicy { - percentagePolicy := group.NewPercentageDecisionPolicy("0.5", time.Second, 0) - req, err := group.NewMsgCreateGroupPolicy(admin, 1, "metadata", percentagePolicy) - require.NoError(t, err) - return req - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg() - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgCreateGroupPolicy{})) - } - }) - } -} - -func TestMsgUpdateGroupPolicyDecisionPolicy(t *testing.T) { - validPolicy := group.NewThresholdDecisionPolicy("1", time.Second, 0) - msg1, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(admin, member1, validPolicy) - require.NoError(t, err) - - invalidPolicy := group.NewThresholdDecisionPolicy("-1", time.Second, 0) - msg2, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(admin, member2, invalidPolicy) - require.NoError(t, err) - - validPercentagePolicy := group.NewPercentageDecisionPolicy("0.7", time.Second, 0) - msg3, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(admin, member3, validPercentagePolicy) - require.NoError(t, err) - - invalidPercentagePolicy := group.NewPercentageDecisionPolicy("-0.1", time.Second, 0) - msg4, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(admin, member4, invalidPercentagePolicy) - require.NoError(t, err) - - invalidPercentagePolicy2 := group.NewPercentageDecisionPolicy("2", time.Second, 0) - msg5, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(admin, member5, invalidPercentagePolicy2) - require.NoError(t, err) - - testCases := []struct { - name string - msg *group.MsgUpdateGroupPolicyDecisionPolicy - expErr bool - errMsg string - }{ - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "group policy: invalid bech32 address", - &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: admin.String(), - GroupPolicyAddress: "address", - }, - true, - "group policy: decoding bech32 failed", - }, - { - "group policy: invalid bech32 address", - &group.MsgUpdateGroupPolicyDecisionPolicy{ - Admin: admin.String(), - GroupPolicyAddress: "address", - }, - true, - "group policy: decoding bech32 failed", - }, - { - "invalid decision policy", - msg2, - true, - "decision policy: threshold: expected a positive decimal", - }, - { - "valid decision policy", - msg1, - false, - "", - }, - { - "valid percentage decision policy", - msg3, - false, - "", - }, - { - "invalid percentage decision policy with negative value", - msg4, - true, - "decision policy: percentage threshold: expected a positive decimal", - }, - { - "invalid percentage decision policy with value greater than 1", - msg5, - true, - "decision policy: percentage must be > 0 and <= 1", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgUpdateGroupPolicyDecisionPolicy{})) - } - }) - } -} - -func TestMsgUpdateGroupPolicyAdmin(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgUpdateGroupPolicyAdmin - expErr bool - errMsg string - }{ - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupPolicyAdmin{ - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "policy address: invalid bech32 address", - &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - NewAdmin: member1.String(), - GroupPolicyAddress: "address", - }, - true, - "group policy: decoding bech32 failed", - }, - { - "new admin: invalid bech32 address", - &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - GroupPolicyAddress: admin.String(), - NewAdmin: "new-admin", - }, - true, - "new admin: decoding bech32 failed", - }, - { - "same old and new admin", - &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - GroupPolicyAddress: admin.String(), - NewAdmin: admin.String(), - }, - true, - "new and old admin are same", - }, - { - "valid test", - &group.MsgUpdateGroupPolicyAdmin{ - Admin: admin.String(), - GroupPolicyAddress: admin.String(), - NewAdmin: member1.String(), - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgUpdateGroupPolicyAdmin{})) - } - }) - } -} - -func TestMsgUpdateGroupPolicyMetadata(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgUpdateGroupPolicyMetadata - expErr bool - errMsg string - }{ - { - "admin: invalid bech32 address", - &group.MsgUpdateGroupPolicyMetadata{ - Admin: "admin", - }, - true, - "admin: decoding bech32 failed", - }, - { - "group policy address: invalid bech32 address", - &group.MsgUpdateGroupPolicyMetadata{ - Admin: admin.String(), - GroupPolicyAddress: "address", - }, - true, - "group policy: decoding bech32 failed", - }, - { - "valid testcase", - &group.MsgUpdateGroupPolicyMetadata{ - Admin: admin.String(), - GroupPolicyAddress: member1.String(), - Metadata: "metadata", - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgUpdateGroupPolicyMetadata{})) - } - }) - } -} - -func TestMsgSubmitProposal(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgSubmitProposal - expErr bool - errMsg string - }{ - { - "invalid group policy address", - &group.MsgSubmitProposal{ - GroupPolicyAddress: "address", - }, - true, - "group policy: decoding bech32 failed", - }, - { - "proposers required", - &group.MsgSubmitProposal{ - GroupPolicyAddress: admin.String(), - Title: "Title", - Summary: "Summary", - }, - true, - "proposers: value is empty", - }, - { - "valid testcase", - &group.MsgSubmitProposal{ - GroupPolicyAddress: admin.String(), - Proposers: []string{member1.String(), member2.String()}, - Title: "Title", - Summary: "Summary", - }, - false, - "", - }, - { - "missing title", - &group.MsgSubmitProposal{ - GroupPolicyAddress: admin.String(), - Proposers: []string{member1.String(), member2.String()}, - Summary: "Summary", - }, - true, - "title: value is empty", - }, - { - "missing summary", - &group.MsgSubmitProposal{ - GroupPolicyAddress: admin.String(), - Proposers: []string{member1.String(), member2.String()}, - Title: "title", - }, - true, - "summary: value is empty", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgSubmitProposal{})) - } - }) - } -} - -func TestMsgSubmitProposalGetSignBytes(t *testing.T) { - testcases := []struct { - name string - proposal []sdk.Msg - expSignBz string - }{ - { - "MsgSend", - []sdk.Msg{banktypes.NewMsgSend(member1, member1, sdk.NewCoins())}, - fmt.Sprintf(`{"type":"cosmos-sdk/group/MsgSubmitProposal","value":{"messages":[{"type":"cosmos-sdk/MsgSend","value":{"amount":[],"from_address":"%s","to_address":"%s"}}],"proposers":[""],"summary":"This is a test","title":"MsgSend"}}`, member1, member1), - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - msg, err := group.NewMsgSubmitProposal(sdk.AccAddress{}.String(), []string{sdk.AccAddress{}.String()}, tc.proposal, "", group.Exec_EXEC_UNSPECIFIED, "MsgSend", "This is a test") - require.NoError(t, err) - var bz []byte - require.NotPanics(t, func() { - bz = msg.GetSignBytes() - }) - require.Equal(t, tc.expSignBz, string(bz)) - }) - } -} - -func TestMsgVote(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgVote - expErr bool - errMsg string - }{ - { - "invalid voter address", - &group.MsgVote{ - Voter: "voter", - }, - true, - "voter: decoding bech32 failed", - }, - { - "proposal id is required", - &group.MsgVote{ - Voter: member1.String(), - }, - true, - "proposal id: value is empty", - }, - { - "unspecified vote option", - &group.MsgVote{ - Voter: member1.String(), - ProposalId: 1, - }, - true, - "vote option: value is empty", - }, - { - "valid test case", - &group.MsgVote{ - Voter: member1.String(), - ProposalId: 1, - Option: group.VOTE_OPTION_YES, - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgVote{})) - } - }) - } -} - -func TestMsgWithdrawProposal(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgWithdrawProposal - expErr bool - errMsg string - }{ - { - "invalid address", - &group.MsgWithdrawProposal{ - Address: "address", - }, - true, - "decoding bech32 failed", - }, - { - "proposal id is required", - &group.MsgWithdrawProposal{ - Address: member1.String(), - }, - true, - "proposal id: value is empty", - }, - { - "valid msg", - &group.MsgWithdrawProposal{ - Address: member1.String(), - ProposalId: 1, - }, - false, - "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgWithdrawProposal{})) - } - }) - } -} - -func TestMsgExec(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgExec - expErr bool - errMsg string - }{ - { - "invalid signer address", - &group.MsgExec{ - Executor: "signer", - }, - true, - "signer: decoding bech32 failed", - }, - { - "proposal is required", - &group.MsgExec{ - Executor: admin.String(), - }, - true, - "proposal id: value is empty", - }, - { - "valid testcase", - &group.MsgExec{ - Executor: admin.String(), - ProposalId: 1, - }, - false, - "", - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgExec{})) - } - }) - } -} - -func TestMsgLeaveGroup(t *testing.T) { - testCases := []struct { - name string - msg *group.MsgLeaveGroup - expErr bool - errMsg string - }{ - { - "invalid group member address", - &group.MsgLeaveGroup{ - Address: "member", - }, - true, - "group member: decoding bech32 failed", - }, - { - "group id is required", - &group.MsgLeaveGroup{ - Address: admin.String(), - }, - true, - "group-id: value is empty", - }, - { - "valid testcase", - &group.MsgLeaveGroup{ - Address: admin.String(), - GroupId: 1, - }, - false, - "", - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - msg := tc.msg - err := msg.ValidateBasic() - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, sdk.MsgTypeURL(msg), sdk.MsgTypeURL(&group.MsgLeaveGroup{})) - } - }) - } -} - -func TestAmino(t *testing.T) { - cdc := testutil.MakeTestEncodingConfig(module.AppModuleBasic{}) - - out, err := cdc.Amino.MarshalJSON(group.MsgSubmitProposal{Proposers: []string{member1.String()}}) - require.NoError(t, err) - require.Equal(t, - `{"type":"cosmos-sdk/group/MsgSubmitProposal","value":{"proposers":["cosmos1d4jk6cn9wgcsj540xq"]}}`, - string(out), - ) -} diff --git a/x/group/types.go b/x/group/types.go index ff18cc0eeb..529b5be66d 100644 --- a/x/group/types.go +++ b/x/group/types.go @@ -364,10 +364,14 @@ func (g GroupMember) ValidateBasic() error { return errorsmod.Wrap(errors.ErrEmpty, "group member's group id") } - err := MemberToMemberRequest(g.Member).ValidateBasic() - if err != nil { - return errorsmod.Wrap(err, "group member") + if _, err := sdk.AccAddressFromBech32(g.Member.Address); err != nil { + return errorsmod.Wrap(err, "group member's address") } + + if _, err := math.NewNonNegativeDecFromString(g.Member.Weight); err != nil { + return errorsmod.Wrap(err, "weight must be non negative") + } + return nil } diff --git a/x/group/typesupport.go b/x/group/typesupport.go deleted file mode 100644 index c1108606c9..0000000000 --- a/x/group/typesupport.go +++ /dev/null @@ -1,49 +0,0 @@ -package group - -import ( - errorsmod "cosmossdk.io/errors" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/group/errors" -) - -// MemberRequests defines a repeated slice of MemberRequest objects. -type MemberRequests struct { - Members []MemberRequest -} - -// ValidateBasic performs stateless validation on an array of members. On top -// of validating each member individually, it also makes sure there are no -// duplicate addresses. -func (ms MemberRequests) ValidateBasic() error { - index := make(map[string]struct{}, len(ms.Members)) - for i := range ms.Members { - member := ms.Members[i] - if err := member.ValidateBasic(); err != nil { - return err - } - addr := member.Address - if _, exists := index[addr]; exists { - return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", addr) - } - index[addr] = struct{}{} - } - return nil -} - -type accAddresses []sdk.AccAddress - -// ValidateBasic verifies that there's no duplicate address. -// Individual account address validation has to be done separately. -func (a accAddresses) ValidateBasic() error { - index := make(map[string]struct{}, len(a)) - for i := range a { - accAddr := a[i] - addr := string(accAddr) - if _, exists := index[addr]; exists { - return errorsmod.Wrapf(errors.ErrDuplicate, "address: %s", accAddr.String()) - } - index[addr] = struct{}{} - } - return nil -}