Skip to content

Commit

Permalink
test(accounts): add multisig e2e tests (#20310)
Browse files Browse the repository at this point in the history
  • Loading branch information
facundomedica committed May 13, 2024
1 parent 559f784 commit 1a8425a
Show file tree
Hide file tree
Showing 7 changed files with 497 additions and 26 deletions.
265 changes: 265 additions & 0 deletions 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("<nil>", execErr.Value)

rejectErr, found := v.GetAttribute("reject_err")
s.True(found)
s.Equal("<nil>", 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("<nil>", 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)
}
118 changes: 118 additions & 0 deletions 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)
}

0 comments on commit 1a8425a

Please sign in to comment.