From 79990014784e606fd3769ec34c410650af0e4a44 Mon Sep 17 00:00:00 2001
From: Damian Nolan <damiannolan@gmail.com>
Date: Wed, 8 Jun 2022 12:35:26 +0200
Subject: [PATCH] test: adding fee middleware tests for ics27 interchain
 accounts (#1433)

* adding interchain accounts integration tests for ics29 fee

* updating simapp to include fee middleware in ica stack

* updating simapp to include ics29 fee in ica stacks

* updating RegisterInterchainAccount to pass through version arg and support fee middleware functionality

* updating tests to support additional version arg in RegisterInterchainAccount

* adding migration docs for ICS27 fee middleware support

* remove unnecessary spacing

* fixing typo in godoc

* adding changelog entry

* adding godoc for NewICAPath in fee test suite

* adding helper for default metadata version string to ics27

* unexported vars, register counterparty address for recv fee distribution, add comments and adjust assertions
---
 .../27-interchain-accounts/types/metadata.go  |  14 ++
 modules/apps/29-fee/ica_test.go               | 197 ++++++++++++++++++
 2 files changed, 211 insertions(+)
 create mode 100644 modules/apps/29-fee/ica_test.go

diff --git a/modules/apps/27-interchain-accounts/types/metadata.go b/modules/apps/27-interchain-accounts/types/metadata.go
index 3a7eae51cdf..15f27314d47 100644
--- a/modules/apps/27-interchain-accounts/types/metadata.go
+++ b/modules/apps/27-interchain-accounts/types/metadata.go
@@ -27,6 +27,20 @@ func NewMetadata(version, controllerConnectionID, hostConnectionID, accAddress,
 	}
 }
 
+// NewDefaultMetadataString creates and returns a new JSON encoded version string containing the default ICS27 Metadata values
+// with the provided controller and host connection identifiers
+func NewDefaultMetadataString(controllerConnectionID, hostConnectionID string) string {
+	metadata := Metadata{
+		Version:                Version,
+		ControllerConnectionId: controllerConnectionID,
+		HostConnectionId:       hostConnectionID,
+		Encoding:               EncodingProtobuf,
+		TxType:                 TxTypeSDKMultiMsg,
+	}
+
+	return string(ModuleCdc.MustMarshalJSON(&metadata))
+}
+
 // IsPreviousMetadataEqual compares a metadata to a previous version string set in a channel struct.
 // It ensures all fields are equal except the Address string
 func IsPreviousMetadataEqual(previousVersion string, metadata Metadata) bool {
diff --git a/modules/apps/29-fee/ica_test.go b/modules/apps/29-fee/ica_test.go
new file mode 100644
index 00000000000..88b3d088412
--- /dev/null
+++ b/modules/apps/29-fee/ica_test.go
@@ -0,0 +1,197 @@
+package fee_test
+
+import (
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
+	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
+
+	icahosttypes "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts/host/types"
+	icatypes "github.com/cosmos/ibc-go/v3/modules/apps/27-interchain-accounts/types"
+	"github.com/cosmos/ibc-go/v3/modules/apps/29-fee/types"
+	clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
+	channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types"
+	ibctesting "github.com/cosmos/ibc-go/v3/testing"
+)
+
+var (
+	// defaultOwnerAddress defines a reusable bech32 address for testing purposes
+	defaultOwnerAddress = "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs"
+
+	// defaultPortID defines a resuable port identifier for testing purposes
+	defaultPortID, _ = icatypes.NewControllerPortID(defaultOwnerAddress)
+
+	// defaultICAVersion defines a resuable interchainaccounts version string for testing purposes
+	defaultICAVersion = icatypes.NewDefaultMetadataString(ibctesting.FirstConnectionID, ibctesting.FirstConnectionID)
+)
+
+// NewIncentivizedICAPath creates and returns a new ibctesting path configured for a fee enabled interchain accounts channel
+func NewIncentivizedICAPath(chainA, chainB *ibctesting.TestChain) *ibctesting.Path {
+	path := ibctesting.NewPath(chainA, chainB)
+
+	feeMetadata := types.Metadata{
+		FeeVersion: types.Version,
+		AppVersion: defaultICAVersion,
+	}
+
+	feeICAVersion := string(types.ModuleCdc.MustMarshalJSON(&feeMetadata))
+
+	path.SetChannelOrdered()
+	path.EndpointA.ChannelConfig.Version = feeICAVersion
+	path.EndpointB.ChannelConfig.Version = feeICAVersion
+	path.EndpointA.ChannelConfig.PortID = defaultPortID
+	path.EndpointB.ChannelConfig.PortID = icatypes.PortID
+
+	return path
+}
+
+// SetupPath performs the InterchainAccounts channel creation handshake using an ibctesting path
+func SetupPath(path *ibctesting.Path, owner string) error {
+	if err := RegisterInterchainAccount(path.EndpointA, owner); err != nil {
+		return err
+	}
+
+	if err := path.EndpointB.ChanOpenTry(); err != nil {
+		return err
+	}
+
+	if err := path.EndpointA.ChanOpenAck(); err != nil {
+		return err
+	}
+
+	if err := path.EndpointB.ChanOpenConfirm(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RegisterInterchainAccount invokes the the InterchainAccounts entrypoint, routes a new MsgChannelOpenInit to the appropriate handler,
+// commits state changes and updates the testing endpoint accordingly
+func RegisterInterchainAccount(endpoint *ibctesting.Endpoint, owner string) error {
+	portID, err := icatypes.NewControllerPortID(owner)
+	if err != nil {
+		return err
+	}
+
+	channelSequence := endpoint.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(endpoint.Chain.GetContext())
+
+	if err := endpoint.Chain.GetSimApp().ICAControllerKeeper.RegisterInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, owner, endpoint.ChannelConfig.Version); err != nil {
+		return err
+	}
+
+	// commit state changes for proof verification
+	endpoint.Chain.NextBlock()
+
+	// update port/channel ids
+	endpoint.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence)
+	endpoint.ChannelConfig.PortID = portID
+
+	return nil
+}
+
+// TestFeeInterchainAccounts Integration test to ensure ics29 works with ics27
+func (suite *FeeTestSuite) TestFeeInterchainAccounts() {
+	path := NewIncentivizedICAPath(suite.chainA, suite.chainB)
+	suite.coordinator.SetupConnections(path)
+
+	err := SetupPath(path, defaultOwnerAddress)
+	suite.Require().NoError(err)
+
+	// assert the newly established channel is fee enabled on both ends
+	suite.Require().True(suite.chainA.GetSimApp().IBCFeeKeeper.IsFeeEnabled(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
+	suite.Require().True(suite.chainB.GetSimApp().IBCFeeKeeper.IsFeeEnabled(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID))
+
+	// register counterparty address on destination chainB as chainA.SenderAccounts[1] for recv fee distribution
+	suite.chainB.GetSimApp().IBCFeeKeeper.SetCounterpartyAddress(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccounts[1].SenderAccount.GetAddress().String(), path.EndpointB.ChannelID)
+
+	// escrow a packet fee for the next send sequence
+	expectedFee := types.NewFee(defaultRecvFee, defaultAckFee, defaultTimeoutFee)
+	msgPayPacketFee := types.NewMsgPayPacketFee(expectedFee, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, suite.chainA.SenderAccount.GetAddress().String(), nil)
+
+	// fetch the account balance before fees are escrowed and assert the difference below
+	preEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
+
+	res, err := suite.chainA.SendMsgs(msgPayPacketFee)
+	suite.Require().NotNil(res)
+	suite.Require().NoError(err)
+
+	postEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
+	suite.Require().Equal(postEscrowBalance.AddAmount(expectedFee.Total().AmountOf(sdk.DefaultBondDenom)), preEscrowBalance)
+
+	packetID := channeltypes.NewPacketId(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, 1)
+	packetFees, found := suite.chainA.GetSimApp().IBCFeeKeeper.GetFeesInEscrow(suite.chainA.GetContext(), packetID)
+	suite.Require().True(found)
+	suite.Require().Equal(expectedFee, packetFees.PacketFees[0].Fee)
+
+	interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID)
+	suite.Require().True(found)
+
+	// fund the interchain account on chainB
+	coins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000)))
+	msgBankSend := &banktypes.MsgSend{
+		FromAddress: suite.chainB.SenderAccount.GetAddress().String(),
+		ToAddress:   interchainAccountAddr,
+		Amount:      coins,
+	}
+
+	res, err = suite.chainB.SendMsgs(msgBankSend)
+	suite.Require().NotEmpty(res)
+	suite.Require().NoError(err)
+
+	// prepare a simple stakingtypes.MsgDelegate to be used as the interchain account msg executed on chainB
+	validatorAddr := (sdk.ValAddress)(suite.chainB.Vals.Validators[0].Address)
+	msgDelegate := &stakingtypes.MsgDelegate{
+		DelegatorAddress: interchainAccountAddr,
+		ValidatorAddress: validatorAddr.String(),
+		Amount:           sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(5000)),
+	}
+
+	data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []sdk.Msg{msgDelegate})
+	suite.Require().NoError(err)
+
+	icaPacketData := icatypes.InterchainAccountPacketData{
+		Type: icatypes.EXECUTE_TX,
+		Data: data,
+	}
+
+	// ensure chainB is allowed to execute stakingtypes.MsgDelegate
+	params := icahosttypes.NewParams(true, []string{sdk.MsgTypeURL(msgDelegate)})
+	suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
+
+	// build the interchain accounts packet
+	packet := buildInterchainAccountsPacket(path, icaPacketData.GetBytes(), 1)
+
+	// write packet commitment to state on chainA and commit state
+	commitment := channeltypes.CommitPacket(suite.chainA.GetSimApp().AppCodec(), packet)
+	suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetPacketCommitment(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, 1, commitment)
+	suite.chainA.NextBlock()
+
+	err = path.RelayPacket(packet)
+	suite.Require().NoError(err)
+
+	// ensure escrowed fees are cleaned up
+	packetFees, found = suite.chainA.GetSimApp().IBCFeeKeeper.GetFeesInEscrow(suite.chainA.GetContext(), packetID)
+	suite.Require().False(found)
+	suite.Require().Empty(packetFees)
+
+	// assert the value of the account balance after fee distribution
+	// NOTE: the balance after fee distribution should be equal to the pre-escrow balance minus the recv fee
+	// as chainA.SenderAccount is used as the msg signer and refund address for msgPayPacketFee above as well as the relyer account for acknowledgements in path.RelayPacket()
+	postDistBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
+	suite.Require().Equal(preEscrowBalance.SubAmount(defaultRecvFee.AmountOf(sdk.DefaultBondDenom)), postDistBalance)
+}
+
+func buildInterchainAccountsPacket(path *ibctesting.Path, data []byte, seq uint64) channeltypes.Packet {
+	packet := channeltypes.NewPacket(
+		data,
+		1,
+		path.EndpointA.ChannelConfig.PortID,
+		path.EndpointA.ChannelID,
+		path.EndpointB.ChannelConfig.PortID,
+		path.EndpointB.ChannelID,
+		clienttypes.NewHeight(0, 100),
+		0,
+	)
+
+	return packet
+}