diff --git a/.github/workflows/e2e-manual-simd.yaml b/.github/workflows/e2e-manual-simd.yaml index 76bf85c6e2d..58d3fd0f3c6 100644 --- a/.github/workflows/e2e-manual-simd.yaml +++ b/.github/workflows/e2e-manual-simd.yaml @@ -17,6 +17,7 @@ on: - TestInterchainAccountsGroupsTestSuite - TestInterchainAccountsGovTestSuite - TestIncentivizedInterchainAccountsTestSuite + - TestAuthzTransferTestSuite chain-image: description: 'The image to use for chain A' required: true diff --git a/e2e/testconfig/testconfig.go b/e2e/testconfig/testconfig.go index d4cb5eab329..84451f109ab 100644 --- a/e2e/testconfig/testconfig.go +++ b/e2e/testconfig/testconfig.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/cosmos/cosmos-sdk/codec" simappparams "github.com/cosmos/cosmos-sdk/simapp/params" @@ -129,6 +130,13 @@ func GetChainBTag() string { return chainBTag } +// IsCI returns true if the tests are running in CI, false is returned +// if the tests are running locally. +// Note: github actions passes a CI env value of true by default to all runners. +func IsCI() bool { + return strings.ToLower(os.Getenv("CI")) == "true" +} + // ChainOptions stores chain configurations for the chains that will be // created for the tests. They can be modified by passing ChainOptionConfiguration // to E2ETestSuite.GetChains. diff --git a/e2e/tests/transfer/authz_test.go b/e2e/tests/transfer/authz_test.go new file mode 100644 index 00000000000..4da10d7d523 --- /dev/null +++ b/e2e/tests/transfer/authz_test.go @@ -0,0 +1,156 @@ +package transfer + +import ( + "context" + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + test "github.com/strangelove-ventures/ibctest/v6/testutil" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" +) + +func TestAuthzTransferTestSuite(t *testing.T) { + suite.Run(t, new(AuthzTransferTestSuite)) +} + +type AuthzTransferTestSuite struct { + testsuite.E2ETestSuite +} + +func (suite *AuthzTransferTestSuite) TestAuthz_MsgTransfer_Succeeds() { + t := suite.T() + ctx := context.TODO() + + relayer, channelA := suite.SetupChainsRelayerAndChannel(ctx, transferChannelOptions()) + chainA, chainB := suite.GetChains() + + chainADenom := chainA.Config().Denom + + granterWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granterAddress := granterWallet.Bech32Address(chainA.Config().Bech32Prefix) + + granteeWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granteeAddress := granteeWallet.Bech32Address(chainA.Config().Bech32Prefix) + + receiverWallet := suite.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + receiverWalletAddress := receiverWallet.Bech32Address(chainB.Config().Bech32Prefix) + + t.Run("start relayer", func(t *testing.T) { + suite.StartRelayer(relayer) + }) + + // createMsgGrantFn initializes a TransferAuthorization and broadcasts a MsgGrant message. + createMsgGrantFn := func(t *testing.T) { + transferAuth := transfertypes.TransferAuthorization{ + Allocations: []transfertypes.Allocation{ + { + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + SpendLimit: sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(testvalues.StartingTokenAmount))), + AllowList: []string{receiverWalletAddress}, + }, + }, + } + + authAny, err := codectypes.NewAnyWithValue(&transferAuth) + suite.Require().NoError(err) + + msgGrant := &authz.MsgGrant{ + Granter: granterAddress, + Grantee: granteeAddress, + Grant: authz.Grant{ + Authorization: authAny, + // no expiration + Expiration: nil, + }, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granterWallet, msgGrant) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + } + + // verifyGrantFn returns a test function which asserts chainA has a grant authorization + // with the given spend limit. + verifyGrantFn := func(expectedLimit int64) func(t *testing.T) { + return func(t *testing.T) { + grantAuths, err := suite.QueryGranterGrants(ctx, chainA, granterAddress) + + suite.Require().NoError(err) + suite.Require().Len(grantAuths, 1) + grantAuthorization := grantAuths[0] + + transferAuth := suite.extractTransferAuthorizationFromGrantAuthorization(grantAuthorization) + expectedSpendLimit := sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(expectedLimit))) + suite.Require().Equal(expectedSpendLimit, transferAuth.Allocations[0].SpendLimit) + } + } + + t.Run("broadcast MsgGrant", createMsgGrantFn) + + t.Run("broadcast MsgExec for ibc MsgTransfer", func(t *testing.T) { + transferMsg := transfertypes.MsgTransfer{ + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + Token: testvalues.DefaultTransferAmount(chainADenom), + Sender: granterAddress, + Receiver: receiverWalletAddress, + TimeoutHeight: suite.GetTimeoutHeight(ctx, chainB), + } + + transferAny, err := codectypes.NewAnyWithValue(&transferMsg) + suite.Require().NoError(err) + + msgExec := &authz.MsgExec{ + Grantee: granteeAddress, + Msgs: []*codectypes.Any{transferAny}, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granteeWallet, msgExec) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + }) + + t.Run("verify granter wallet amount", func(t *testing.T) { + actualBalance, err := suite.GetChainANativeBalance(ctx, granterWallet) + suite.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + suite.Require().Equal(expected, actualBalance) + }) + + suite.Require().NoError(test.WaitForBlocks(context.TODO(), 10, chainB)) + + t.Run("verify receiver wallet amount", func(t *testing.T) { + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + actualBalance, err := chainB.GetBalance(ctx, receiverWalletAddress, chainBIBCToken.IBCDenom()) + suite.Require().NoError(err) + suite.Require().Equal(testvalues.IBCTransferAmount, actualBalance) + }) + + t.Run("granter grant spend limit reduced", verifyGrantFn(testvalues.StartingTokenAmount-testvalues.IBCTransferAmount)) + + t.Run("re-initialize MsgGrant", createMsgGrantFn) + + t.Run("granter grant was reinitialized", verifyGrantFn(testvalues.StartingTokenAmount)) + +} + +// extractTransferAuthorizationFromGrantAuthorization extracts a TransferAuthorization from the given +// GrantAuthorization. +func (suite *AuthzTransferTestSuite) extractTransferAuthorizationFromGrantAuthorization(grantAuth *authz.GrantAuthorization) *transfertypes.TransferAuthorization { + cfg := testsuite.EncodingConfig() + var authorization authz.Authorization + err := cfg.InterfaceRegistry.UnpackAny(grantAuth.Authorization, &authorization) + suite.Require().NoError(err) + + transferAuth, ok := authorization.(*transfertypes.TransferAuthorization) + suite.Require().True(ok) + return transferAuth +} diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go index dcee96ced4f..4181ef2ff0f 100644 --- a/e2e/testsuite/codec.go +++ b/e2e/testsuite/codec.go @@ -3,9 +3,11 @@ package testsuite import ( "github.com/cosmos/cosmos-sdk/codec" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" simappparams "github.com/cosmos/ibc-go/v6/testing/simapp/params" ) @@ -24,6 +26,8 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, simappparams.EncodingConfig) { banktypes.RegisterInterfaces(cfg.InterfaceRegistry) govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) authtypes.RegisterInterfaces(cfg.InterfaceRegistry) + authz.RegisterInterfaces(cfg.InterfaceRegistry) + transfertypes.RegisterInterfaces(cfg.InterfaceRegistry) cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) return cdc, cfg } diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index b8c03f0e5f8..c2fb576d442 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" grouptypes "github.com/cosmos/cosmos-sdk/x/group" @@ -81,6 +82,7 @@ type GRPCClients struct { GroupsQueryClient grouptypes.QueryClient ParamsQueryClient paramsproposaltypes.QueryClient AuthQueryClient authtypes.QueryClient + AuthZQueryClient authz.QueryClient } // path is a pairing of two chains which will be used in a test. @@ -391,6 +393,7 @@ func (s *E2ETestSuite) InitGRPCClients(chain *cosmos.CosmosChain) { GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), AuthQueryClient: authtypes.NewQueryClient(grpcConn), + AuthZQueryClient: authz.NewQueryClient(grpcConn), } } @@ -425,7 +428,7 @@ func (s *E2ETestSuite) createCosmosChains(chainOptions testconfig.ChainOptions) logger := zaptest.NewLogger(s.T()) - numValidators, numFullNodes := 4, 1 + numValidators, numFullNodes := getValidatorsAndFullNodes() chainA := cosmos.NewCosmosChain(s.T().Name(), *chainOptions.ChainAConfig, numValidators, numFullNodes, logger) chainB := cosmos.NewCosmosChain(s.T().Name(), *chainOptions.ChainBConfig, numValidators, numFullNodes, logger) @@ -539,7 +542,32 @@ func (s *E2ETestSuite) QueryModuleAccountAddress(ctx context.Context, moduleName return moduleAccount.GetAddress(), nil } +// QueryGranterGrants returns all GrantAuthorizations for the given granterAddress. +func (s *E2ETestSuite) QueryGranterGrants(ctx context.Context, chain *cosmos.CosmosChain, granterAddress string) ([]*authz.GrantAuthorization, error) { + authzClient := s.GetChainGRCPClients(chain).AuthZQueryClient + queryRequest := &authz.QueryGranterGrantsRequest{ + Granter: granterAddress, + } + + grants, err := authzClient.GranterGrants(ctx, queryRequest) + if err != nil { + return nil, err + } + + return grants.Grants, nil +} + // GetIBCToken returns the denomination of the full token denom sent to the receiving channel func GetIBCToken(fullTokenDenom string, portID, channelID string) transfertypes.DenomTrace { return transfertypes.ParseDenomTrace(fmt.Sprintf("%s/%s/%s", portID, channelID, fullTokenDenom)) } + +// getValidatorsAndFullNodes returns the number of validators and full nodes respectively that should be used for +// the test. If the test is running in CI, more nodes are used, when running locally a single node is used to +// use less resources and allow the tests to run faster. +func getValidatorsAndFullNodes() (int, int) { + if testconfig.IsCI() { + return 4, 1 + } + return 1, 0 +}