From 1a8425a343c41ade1944c06f47174ba08c8617b4 Mon Sep 17 00:00:00 2001 From: Facundo Medica <14063057+facundomedica@users.noreply.github.com> Date: Mon, 13 May 2024 14:53:15 +0200 Subject: [PATCH] test(accounts): add multisig e2e tests (#20310) --- tests/e2e/accounts/multisig/account_test.go | 265 ++++++++++++++++++ tests/e2e/accounts/multisig/test_suite.go | 118 ++++++++ x/accounts/defaults/multisig/account.go | 26 +- x/accounts/defaults/multisig/account_test.go | 104 ++++++- x/accounts/defaults/multisig/update_config.go | 6 + x/accounts/defaults/multisig/utils_test.go | 2 +- x/accounts/depinject.go | 2 + 7 files changed, 497 insertions(+), 26 deletions(-) create mode 100644 tests/e2e/accounts/multisig/account_test.go create mode 100644 tests/e2e/accounts/multisig/test_suite.go diff --git a/tests/e2e/accounts/multisig/account_test.go b/tests/e2e/accounts/multisig/account_test.go new file mode 100644 index 0000000000..f3fc0c8da9 --- /dev/null +++ b/tests/e2e/accounts/multisig/account_test.go @@ -0,0 +1,265 @@ +package multisig + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + + bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" + basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" + "cosmossdk.io/core/header" + "cosmossdk.io/math" + v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" + accountsv1 "cosmossdk.io/x/accounts/v1" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestE2ETestSuite(t *testing.T) { + suite.Run(t, NewE2ETestSuite()) +} + +// TestSimpleSendProposal creates a multisig account with 1 member, sends a tx, votes and executes it. +func (s *E2ETestSuite) TestSimpleSendProposal() { + ctx := sdk.NewContext(s.app.CommitMultiStore(), false, s.app.Logger()).WithHeaderInfo(header.Info{ + Time: time.Now(), + }) + + randAcc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr, err := s.app.AuthKeeper.AddressCodec().BytesToString(randAcc) + s.NoError(err) + + initialMembers := map[string]uint64{ + s.membersAddr[0]: 100, + } + accountAddr, accAddrStr := s.initAccount(ctx, s.members[0], initialMembers) + + balance := s.app.BankKeeper.GetBalance(ctx, randAcc, "stake") + s.Equal(math.NewInt(0), balance.Amount) + + // do a simple bank send + msg := &bankv1beta1.MsgSend{ + FromAddress: accAddrStr, + ToAddress: addr, + Amount: []*basev1beta1.Coin{ + { + Denom: "stake", + Amount: "100", + }, + }, + } + anyMsg, err := codectypes.NewAnyWithValue(msg) + s.NoError(err) + + s.createProposal(ctx, accountAddr, s.members[0], anyMsg) + + // now we vote for it + voteReq := &v1.MsgVote{ + ProposalId: 0, + Vote: v1.VoteOption_VOTE_OPTION_YES, + } + + err = s.executeTx(ctx, voteReq, accountAddr, s.members[0]) + s.NoError(err) + + // now we execute it + err = s.executeProposal(ctx, accountAddr, s.members[0], 0) + s.NoError(err) + + foundPropResult := false + for _, v := range ctx.EventManager().Events() { + if v.Type == "proposal_tally" { + foundPropResult = true + status, found := v.GetAttribute("status") + s.True(found) + s.Equal(v1.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), status.Value) + + yesVotes, found := v.GetAttribute("yes_votes") + s.True(found) + s.Equal("100", yesVotes.Value) + + noVotes, found := v.GetAttribute("no_votes") + s.True(found) + s.Equal("0", noVotes.Value) + + propID, found := v.GetAttribute("proposal_id") + s.True(found) + s.Equal("0", propID.Value) + + execErr, found := v.GetAttribute("exec_err") + s.True(found) + s.Equal("", execErr.Value) + + rejectErr, found := v.GetAttribute("reject_err") + s.True(found) + s.Equal("", rejectErr.Value) + } + } + s.True(foundPropResult) + + balance = s.app.BankKeeper.GetBalance(ctx, randAcc, "stake") + s.Equal(int64(100), balance.Amount.Int64()) + + // try to execute again, should fail + err = s.executeProposal(ctx, accountAddr, s.members[0], 0) + s.Error(err) +} + +// TestConfigUpdate creates a multisig with 1 member, adds 2 more members and +// changes the config to require 2/3 majority (also through a proposal). +func (s *E2ETestSuite) TestConfigUpdate() { + ctx := sdk.NewContext(s.app.CommitMultiStore(), false, s.app.Logger()).WithHeaderInfo(header.Info{ + Time: time.Now(), + }) + + initialMembers := map[string]uint64{ + s.membersAddr[0]: 100, + } + accountAddr, accAddrStr := s.initAccount(ctx, s.members[0], initialMembers) + + // Add 2 members and pass the proposal + // create proposal + updateMsg := &v1.MsgUpdateConfig{ + UpdateMembers: []*v1.Member{ + { + Address: s.membersAddr[1], + Weight: 100, + }, + { + Address: s.membersAddr[2], + Weight: 100, + }, + }, + Config: &v1.Config{ + Threshold: 200, // 3 members with 100 power each, 2/3 majority + Quorum: 200, + VotingPeriod: 120, + Revote: false, + EarlyExecution: false, + }, + } + + msgExec := &accountsv1.MsgExecute{ + Sender: accAddrStr, + Target: accAddrStr, + Message: codectypes.UnsafePackAny(updateMsg), + Funds: []sdk.Coin{}, + } + + s.createProposal(ctx, accountAddr, s.members[0], codectypes.UnsafePackAny(msgExec)) + + // vote + voteReq := &v1.MsgVote{ + ProposalId: 0, + Vote: v1.VoteOption_VOTE_OPTION_YES, + } + + err := s.executeTx(ctx, voteReq, accountAddr, s.members[0]) + s.NoError(err) + + err = s.executeProposal(ctx, accountAddr, s.members[0], 0) + s.NoError(err) + + // get members + res, err := s.queryAcc(ctx, &v1.QueryConfig{}, accountAddr) + s.NoError(err) + resp := res.(*v1.QueryConfigResponse) + s.Len(resp.Members, 3) + s.Equal(int64(200), resp.Config.Threshold) + + // Try to remove a member, but it doesn't reach passing threshold + // create proposal + msgExec = &accountsv1.MsgExecute{ + Sender: accAddrStr, + Target: accAddrStr, + Message: codectypes.UnsafePackAny(&v1.MsgUpdateConfig{ + UpdateMembers: []*v1.Member{ + { + Address: s.membersAddr[1], + Weight: 0, + }, + }, + Config: &v1.Config{ + Threshold: 200, // 3 members with 100 power each, 2/3 majority + Quorum: 200, + VotingPeriod: 120, + Revote: false, + EarlyExecution: false, + }, + }), + Funds: []sdk.Coin{}, + } + + s.createProposal(ctx, accountAddr, s.members[0], codectypes.UnsafePackAny(msgExec)) + + // vote + voteReq = &v1.MsgVote{ + ProposalId: 1, + Vote: v1.VoteOption_VOTE_OPTION_NO, + } + + err = s.executeTx(ctx, voteReq, accountAddr, s.members[0]) + s.NoError(err) + + // need to wait until voting period is over because we disabled early execution on the last + // config update + err = s.executeProposal(ctx, accountAddr, s.members[0], 0) + s.ErrorContains(err, "voting period has not ended yet, and early execution is not enabled") + + // vote with member 1 + voteReq = &v1.MsgVote{ + ProposalId: 1, + Vote: v1.VoteOption_VOTE_OPTION_NO, + } + + err = s.executeTx(ctx, voteReq, accountAddr, s.members[1]) + s.NoError(err) + + // need to wait until voting period is over because we disabled early execution on the last + // config update + err = s.executeProposal(ctx, accountAddr, s.members[0], 1) + s.ErrorContains(err, "voting period has not ended yet, and early execution is not enabled") + + headerInfo := ctx.HeaderInfo() + headerInfo.Time = headerInfo.Time.Add(time.Second * 121) + ctx = ctx.WithHeaderInfo(headerInfo) + + // now it should work, but the proposal will fail + err = s.executeProposal(ctx, accountAddr, s.members[0], 1) + s.NoError(err) + + foundPropResult := false + for _, v := range ctx.EventManager().Events() { + if v.Type == "proposal_tally" { + propID, found := v.GetAttribute("proposal_id") + s.True(found) + + if propID.Value == "1" { + foundPropResult = true + status, found := v.GetAttribute("status") + s.True(found) + s.Equal(v1.ProposalStatus_PROPOSAL_STATUS_REJECTED.String(), status.Value) + + // exec_err is nil because the proposal didn't execute + execErr, found := v.GetAttribute("exec_err") + s.True(found) + s.Equal("", execErr.Value) + + rejectErr, found := v.GetAttribute("reject_err") + s.True(found) + s.Equal("threshold not reached", rejectErr.Value) + } + } + } + s.True(foundPropResult) + + // get members + res, err = s.queryAcc(ctx, &v1.QueryConfig{}, accountAddr) + s.NoError(err) + resp = res.(*v1.QueryConfigResponse) + s.Len(resp.Members, 3) + s.Equal(int64(200), resp.Config.Threshold) +} diff --git a/tests/e2e/accounts/multisig/test_suite.go b/tests/e2e/accounts/multisig/test_suite.go new file mode 100644 index 0000000000..1f333cf2a0 --- /dev/null +++ b/tests/e2e/accounts/multisig/test_suite.go @@ -0,0 +1,118 @@ +package multisig + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/protobuf/runtime/protoiface" + + "cosmossdk.io/math" + "cosmossdk.io/simapp" + multisigaccount "cosmossdk.io/x/accounts/defaults/multisig" + v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" + "cosmossdk.io/x/bank/testutil" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ProtoMsg = protoiface.MessageV1 + +type E2ETestSuite struct { + suite.Suite + + app *simapp.SimApp + members []sdk.AccAddress + membersAddr []string +} + +func NewE2ETestSuite() *E2ETestSuite { + return &E2ETestSuite{} +} + +func (s *E2ETestSuite) SetupSuite() { + s.app = setupApp(s.T()) + + s.members = []sdk.AccAddress{} + for i := 0; i < 10; i++ { + addr := secp256k1.GenPrivKey().PubKey().Address() + addrStr, err := s.app.AuthKeeper.AddressCodec().BytesToString(addr) + require.NoError(s.T(), err) + s.membersAddr = append(s.membersAddr, addrStr) + s.members = append(s.members, sdk.AccAddress(addr)) + } +} + +func (s *E2ETestSuite) TearDownSuite() {} + +func setupApp(t *testing.T) *simapp.SimApp { + t.Helper() + app := simapp.Setup(t, false) + return app +} + +func (s *E2ETestSuite) executeTx(ctx context.Context, msg sdk.Msg, accAddr, sender []byte) error { + _, err := s.app.AccountsKeeper.Execute(ctx, accAddr, sender, msg, nil) + return err +} + +func (s *E2ETestSuite) queryAcc(ctx context.Context, req sdk.Msg, accAddr []byte) (ProtoMsg, error) { + resp, err := s.app.AccountsKeeper.Query(ctx, accAddr, req) + return resp, err +} + +func (s *E2ETestSuite) fundAccount(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) { + require.NoError(s.T(), testutil.FundAccount(ctx, s.app.BankKeeper, addr, amt)) +} + +// initAccount initializes a multisig account with the given members and powers +// and returns the account address +func (s *E2ETestSuite) initAccount(ctx context.Context, sender []byte, membersPowers map[string]uint64) ([]byte, string) { + s.fundAccount(ctx, sender, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000000))}) + + members := []*v1.Member{} + for addrStr, power := range membersPowers { + members = append(members, &v1.Member{Address: addrStr, Weight: power}) + } + + _, accountAddr, err := s.app.AccountsKeeper.Init(ctx, multisigaccount.MULTISIG_ACCOUNT, sender, + &v1.MsgInit{ + Members: members, + Config: &v1.Config{ + Threshold: 100, + Quorum: 100, + VotingPeriod: 120, + Revote: false, + EarlyExecution: true, + }, + }, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}) + s.NoError(err) + + accountAddrStr, err := s.app.AuthKeeper.AddressCodec().BytesToString(accountAddr) + s.NoError(err) + + return accountAddr, accountAddrStr +} + +// createProposal +func (s *E2ETestSuite) createProposal(ctx context.Context, accAddr, sender []byte, msgs ...*codectypes.Any) { + propReq := &v1.MsgCreateProposal{ + Proposal: &v1.Proposal{ + Title: "test", + Summary: "test", + Messages: msgs, + }, + } + err := s.executeTx(ctx, propReq, accAddr, sender) + s.NoError(err) +} + +func (s *E2ETestSuite) executeProposal(ctx context.Context, accAddr, sender []byte, proposalID uint64) error { + execReq := &v1.MsgExecuteProposal{ + ProposalId: proposalID, + } + return s.executeTx(ctx, execReq, accAddr, sender) +} diff --git a/x/accounts/defaults/multisig/account.go b/x/accounts/defaults/multisig/account.go index 8b93d14412..6ab25a8440 100644 --- a/x/accounts/defaults/multisig/account.go +++ b/x/accounts/defaults/multisig/account.go @@ -16,6 +16,8 @@ import ( "github.com/cosmos/cosmos-sdk/codec" ) +var MULTISIG_ACCOUNT = "multisig-account" + var ( MembersPrefix = collections.NewPrefix(0) SequencePrefix = collections.NewPrefix(1) @@ -43,19 +45,17 @@ type Account struct { } // NewAccount returns a new multisig account creator function. -func NewAccount(name string) accountstd.AccountCreatorFunc { - return func(deps accountstd.Dependencies) (string, accountstd.Interface, error) { - return name, &Account{ - Members: collections.NewMap(deps.SchemaBuilder, MembersPrefix, "members", collections.BytesKey, collections.Uint64Value), - Sequence: collections.NewSequence(deps.SchemaBuilder, SequencePrefix, "sequence"), - Config: collections.NewItem(deps.SchemaBuilder, ConfigPrefix, "config", codec.CollValue[v1.Config](deps.LegacyStateCodec)), - Proposals: collections.NewMap(deps.SchemaBuilder, ProposalsPrefix, "proposals", collections.Uint64Key, codec.CollValue[v1.Proposal](deps.LegacyStateCodec)), - Votes: collections.NewMap(deps.SchemaBuilder, VotesPrefix, "votes", collections.PairKeyCodec(collections.Uint64Key, collections.BytesKey), collections.Int32Value), - addrCodec: deps.AddressCodec, - headerService: deps.Environment.HeaderService, - eventService: deps.Environment.EventService, - }, nil - } +func NewAccount(deps accountstd.Dependencies) (*Account, error) { + return &Account{ + Members: collections.NewMap(deps.SchemaBuilder, MembersPrefix, "members", collections.BytesKey, collections.Uint64Value), + Sequence: collections.NewSequence(deps.SchemaBuilder, SequencePrefix, "sequence"), + Config: collections.NewItem(deps.SchemaBuilder, ConfigPrefix, "config", codec.CollValue[v1.Config](deps.LegacyStateCodec)), + Proposals: collections.NewMap(deps.SchemaBuilder, ProposalsPrefix, "proposals", collections.Uint64Key, codec.CollValue[v1.Proposal](deps.LegacyStateCodec)), + Votes: collections.NewMap(deps.SchemaBuilder, VotesPrefix, "votes", collections.PairKeyCodec(collections.Uint64Key, collections.BytesKey), collections.Int32Value), + addrCodec: deps.AddressCodec, + headerService: deps.Environment.HeaderService, + eventService: deps.Environment.EventService, + }, nil } // Init initializes the multisig account with the given configuration and members. diff --git a/x/accounts/defaults/multisig/account_test.go b/x/accounts/defaults/multisig/account_test.go index 9da906ffbe..ab95b5be48 100644 --- a/x/accounts/defaults/multisig/account_test.go +++ b/x/accounts/defaults/multisig/account_test.go @@ -11,17 +11,17 @@ import ( "cosmossdk.io/core/store" "cosmossdk.io/x/accounts/accountstd" v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" + accountsv1 "cosmossdk.io/x/accounts/v1" ) func setup(t *testing.T, ctx context.Context, ss store.KVStoreService, timefn func() time.Time) *Account { t.Helper() deps := makeMockDependencies(ss, timefn) - multisig := NewAccount("multisig") - _, acc, err := multisig(deps) + acc, err := NewAccount(deps) require.NoError(t, err) - return acc.(*Account) + return acc } func TestInit(t *testing.T) { @@ -321,6 +321,8 @@ func TestUpdateConfig(t *testing.T) { _, err := acc.Init(ctx, startAcc) require.NoError(t, err) + ctx = accountstd.SetSender(ctx, []byte("mock_multisig_account")) + _, err = acc.UpdateConfig(ctx, tc.msg) if tc.expErr != "" { require.EqualError(t, err, tc.expErr) @@ -342,6 +344,51 @@ func TestUpdateConfig(t *testing.T) { } } +func TestProposal_WrongSender(t *testing.T) { + startAcc := &v1.MsgInit{ + Config: &v1.Config{ + Threshold: 2640, + Quorum: 2000, + VotingPeriod: 60, + }, + Members: []*v1.Member{ + { + Address: "addr1", + Weight: 1000, + }, + { + Address: "addr2", + Weight: 1000, + }, + { + Address: "addr3", + Weight: 1000, + }, + { + Address: "addr4", + Weight: 1000, + }, + }, + } + + ctx, ss := newMockContext(t) + acc := setup(t, ctx, ss, nil) + _, err := acc.Init(ctx, startAcc) + require.NoError(t, err) + + // change the sender to be something else to trigger an error + ctx = accountstd.SetSender(ctx, []byte("wrong_sender_here")) + + newCfg := startAcc.Config + newCfg.VotingPeriod = 120 + updateCfg := &v1.MsgUpdateConfig{ + Config: newCfg, + } + + _, err = acc.UpdateConfig(ctx, updateCfg) + require.ErrorContains(t, err, "only the account itself can update the config (through a proposal)") +} + func TestProposal_NotPassing(t *testing.T) { // all test cases start from the same initial state startAcc := &v1.MsgInit{ @@ -491,12 +538,20 @@ func TestProposalPassing(t *testing.T) { } var acc *Account - ctx, ss := accountstd.NewMockContext( + var ctx context.Context + var ss store.KVStoreService + ctx, ss = accountstd.NewMockContext( 0, []byte("multisig_acc"), []byte("addr1"), TestFunds, func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error { return nil - }, func(ctx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) { - if _, ok := msg.(*v1.MsgUpdateConfig); ok { - return acc.UpdateConfig(ctx, msg.(*v1.MsgUpdateConfig)) + }, func(ictx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) { + if execmsg, ok := msg.(*accountsv1.MsgExecute); ok { + updateCfg, err := accountstd.UnpackAny[v1.MsgUpdateConfig](execmsg.GetMessage()) + if err != nil { + return nil, err + } + + ctx = accountstd.SetSender(ctx, []byte("multisig_acc")) + return acc.UpdateConfig(ctx, updateCfg) } return nil, nil }, func(ctx context.Context, req, resp ProtoMsg) error { @@ -523,12 +578,21 @@ func TestProposalPassing(t *testing.T) { anymsg, err := accountstd.PackAny(msg) require.NoError(t, err) + execMsg := &accountsv1.MsgExecute{ + Sender: "multisig_acc", + Target: "multisig_acc", + Message: anymsg, + Funds: nil, + } + execMsgAny, err := accountstd.PackAny(execMsg) + require.NoError(t, err) + // create a proposal createRes, err := acc.CreateProposal(ctx, &v1.MsgCreateProposal{ Proposal: &v1.Proposal{ Title: "test", Summary: "test", - Messages: []*types.Any{anymsg}, + Messages: []*types.Any{execMsgAny}, }, }) require.NoError(t, err) @@ -572,9 +636,25 @@ func TestProposalPassing(t *testing.T) { // check if addr1's weight changed cfg, err := acc.QueryConfig(ctx, &v1.QueryConfig{}) require.NoError(t, err) - for _, v := range cfg.Members { - if v.Address == "addr1" { - require.Equal(t, uint64(500), v.Weight) - } + + expectedMembers := []*v1.Member{ + { + Address: "addr1", + Weight: 500, + }, + { + Address: "addr2", + Weight: 1000, + }, + { + Address: "addr3", + Weight: 1000, + }, + { + Address: "addr4", + Weight: 1000, + }, } + + require.Equal(t, expectedMembers, cfg.Members) } diff --git a/x/accounts/defaults/multisig/update_config.go b/x/accounts/defaults/multisig/update_config.go index 8a88abc605..a1966c787b 100644 --- a/x/accounts/defaults/multisig/update_config.go +++ b/x/accounts/defaults/multisig/update_config.go @@ -4,11 +4,17 @@ import ( "context" "errors" + "cosmossdk.io/x/accounts/accountstd" v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" ) // UpdateConfig updates the configuration of the multisig account. func (a Account) UpdateConfig(ctx context.Context, msg *v1.MsgUpdateConfig) (*v1.MsgUpdateConfigResponse, error) { + // this function can only be executed by the account itself + if !accountstd.SenderIsSelf(ctx) { + return nil, errors.New("only the account itself can update the config (through a proposal)") + } + // set members for i := range msg.UpdateMembers { addrBz, err := a.addrCodec.StringToBytes(msg.UpdateMembers[i].Address) diff --git a/x/accounts/defaults/multisig/utils_test.go b/x/accounts/defaults/multisig/utils_test.go index 32df0f1bb8..d90ead9fba 100644 --- a/x/accounts/defaults/multisig/utils_test.go +++ b/x/accounts/defaults/multisig/utils_test.go @@ -163,7 +163,7 @@ func (a addressCodec) BytesToString(bz []byte) (string, error) { return string func newMockContext(t *testing.T) (context.Context, store.KVStoreService) { t.Helper() return accountstd.NewMockContext( - 0, []byte("multisig_account"), []byte("sender"), TestFunds, func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error { + 0, []byte("mock_multisig_account"), []byte("sender"), TestFunds, func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error { return nil }, func(ctx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) { return nil, nil diff --git a/x/accounts/depinject.go b/x/accounts/depinject.go index 54c65251a4..04a45c5334 100644 --- a/x/accounts/depinject.go +++ b/x/accounts/depinject.go @@ -12,6 +12,7 @@ import ( "cosmossdk.io/x/accounts/accountstd" baseaccount "cosmossdk.io/x/accounts/defaults/base" "cosmossdk.io/x/accounts/defaults/lockup" + "cosmossdk.io/x/accounts/defaults/multisig" "cosmossdk.io/x/tx/signing" "github.com/cosmos/cosmos-sdk/codec" @@ -70,6 +71,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { accountstd.AddAccount(lockup.PERIODIC_LOCKING_ACCOUNT, lockup.NewPeriodicLockingAccount), accountstd.AddAccount(lockup.DELAYED_LOCKING_ACCOUNT, lockup.NewDelayedLockingAccount), accountstd.AddAccount(lockup.PERMANENT_LOCKING_ACCOUNT, lockup.NewPermanentLockingAccount), + accountstd.AddAccount(multisig.MULTISIG_ACCOUNT, multisig.NewAccount), ) if err != nil { panic(err)