diff --git a/.github/compatibility-test-matrices/main/transfer-v2-chain-a.json b/.github/compatibility-test-matrices/main/transfer-v2-chain-a.json new file mode 100644 index 00000000000..95ca1fcfe0f --- /dev/null +++ b/.github/compatibility-test-matrices/main/transfer-v2-chain-a.json @@ -0,0 +1,18 @@ +{ + "chain-a": [ + "main" + ], + "chain-b": [ + "main" + ], + "entrypoint": [ + "TestTransferTestSuite" + ], + "test": [ + "TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom", + "TestMsgTransfer_Fails_InvalidAddress_MultiDenom" + ], + "relayer-type": [ + "hermes" + ] +} \ No newline at end of file diff --git a/.github/compatibility-test-matrices/main/transfer-v2-channel-upgrade-chain-a.json b/.github/compatibility-test-matrices/main/transfer-v2-channel-upgrade-chain-a.json new file mode 100644 index 00000000000..9703bd94cef --- /dev/null +++ b/.github/compatibility-test-matrices/main/transfer-v2-channel-upgrade-chain-a.json @@ -0,0 +1,19 @@ +{ + "chain-a": [ + "main" + ], + "chain-b": [ + "main" + ], + "entrypoint": [ + "TestTransferChannelUpgradesTestSuite" + ], + "test": [ + "TestChannelUpgrade_WithICS20v2_Succeeds", + "TestChannelUpgrade_WithFeeMiddlewareAndICS20v2_Succeeds", + "TestChannelDowngrade_WithICS20v1_Succeeds" + ], + "relayer-type": [ + "hermes" + ] +} \ No newline at end of file diff --git a/.github/compatibility-test-matrices/unreleased/transfer-v2-channel-upgrade.json b/.github/compatibility-test-matrices/unreleased/transfer-v2-channel-upgrade.json new file mode 100644 index 00000000000..1d13a54e695 --- /dev/null +++ b/.github/compatibility-test-matrices/unreleased/transfer-v2-channel-upgrade.json @@ -0,0 +1,19 @@ +{ + "chain-a": [ + "release-v9.0.x" + ], + "chain-b": [ + "release-v9.0.x" + ], + "entrypoint": [ + "TestTransferChannelUpgradesTestSuite" + ], + "test": [ + "TestChannelUpgrade_WithICS20v2_Succeeds", + "TestChannelUpgrade_WithFeeMiddlewareAndICS20v2_Succeeds", + "TestChannelDowngrade_WithICS20v1_Succeeds" + ], + "relayer-type": [ + "hermes" + ] +} \ No newline at end of file diff --git a/.github/compatibility-test-matrices/unreleased/transfer-v2.json b/.github/compatibility-test-matrices/unreleased/transfer-v2.json new file mode 100644 index 00000000000..23484ea3cd0 --- /dev/null +++ b/.github/compatibility-test-matrices/unreleased/transfer-v2.json @@ -0,0 +1,18 @@ +{ + "chain-a": [ + "release-v9.0.x" + ], + "chain-b": [ + "release-v9.0.x" + ], + "entrypoint": [ + "TestTransferTestSuite" + ], + "test": [ + "TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom", + "TestMsgTransfer_Fails_InvalidAddress_MultiDenom" + ], + "relayer-type": [ + "hermes" + ] +} diff --git a/e2e/tests/transfer/base_test.go b/e2e/tests/transfer/base_test.go index fffe14d9228..d11c45c5798 100644 --- a/e2e/tests/transfer/base_test.go +++ b/e2e/tests/transfer/base_test.go @@ -150,6 +150,225 @@ func (s *TransferTestSuite) TestMsgTransfer_Succeeds_Nonincentivized() { } } +// TestMsgTransfer_Succeeds_MultiDenom will test sending successful IBC transfers from chainA to chainB. +// A multidenom transfer with native chainB tokens and IBC tokens from chainA is executed from chainB to chainA. +func (s *TransferTestSuite) TestMsgTransfer_Succeeds_Nonincentivized_MultiDenom() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBDenom := chainB.Config().Denom + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + chainAIBCToken := testsuite.GetIBCToken(chainBDenom, channelA.PortID, channelA.ChannelID) + + t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("native chainA tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + + expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) + s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("metadata for IBC denomination exists on chainB", func(t *testing.T) { + s.AssertHumanReadableDenom(ctx, chainB, chainADenom, channelA) + }) + + // send the native chainB denom and also the ibc token from chainA + transferCoins := []sdk.Coin{ + testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), + testvalues.DefaultTransferAmount(chainBDenom), + } + + t.Run("native token from chain B and non-native IBC token from chainA, both to chainA", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainA), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) + + t.Run("chain A native denom", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("chain B IBC denom", func(t *testing.T) { + actualBalance, err := query.Balance(ctx, chainA, chainAAddress, chainAIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + }) + + t.Run("native chainA tokens are un-escrowed", func(t *testing.T) { + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + s.Require().Equal(sdk.NewCoin(chainADenom, sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because tokens have come back + }) +} + +// TestMsgTransfer_Fails_InvalidAddress_MultiDenom attempts to send a multidenom IBC transfer +// to an invalid address and ensures that the tokens on the sending chain are returned to the sender. +func (s *TransferTestSuite) TestMsgTransfer_Fails_InvalidAddress_MultiDenom() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, nil) + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBDenom := chainB.Config().Denom + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + + t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, sdk.NewCoins(testvalues.DefaultTransferAmount(chainADenom)), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("native chainA tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + + expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) + s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("metadata for IBC denomination exists on chainB", func(t *testing.T) { + s.AssertHumanReadableDenom(ctx, chainB, chainADenom, channelA) + }) + + // send the native chainB denom and also the ibc token from chainA + transferCoins := []sdk.Coin{ + testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), + testvalues.DefaultTransferAmount(chainBDenom), + } + + t.Run("stop relayer", func(t *testing.T) { + s.StopRelayer(ctx, relayer) + }) + + t.Run("native token from chain B and non-native IBC token from chainA, both to chainA", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, testvalues.InvalidAddress, s.GetTimeoutHeight(ctx, chainA), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("tokens are sent from chain B", func(t *testing.T) { + t.Run("native chainB tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainBNativeBalance(ctx, chainBWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("non-native chainA IBC denom are burned", func(t *testing.T) { + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(int64(0), actualBalance.Int64()) + }) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) + }) + + t.Run("tokens are returned to sender on chainB", func(t *testing.T) { + t.Run("native chainB denom", func(t *testing.T) { + actualBalance, err := s.GetChainBNativeBalance(ctx, chainBWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("non-native chainA IBC denom", func(t *testing.T) { + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + }) +} + // TestMsgTransfer_Fails_InvalidAddress attempts to send an IBC transfer to an invalid address and ensures // that the tokens on the sending chain are unescrowed. func (s *TransferTestSuite) TestMsgTransfer_Fails_InvalidAddress() { diff --git a/e2e/tests/transfer/upgrades_test.go b/e2e/tests/transfer/upgrades_test.go index 10ea8b3c839..c458e71cf96 100644 --- a/e2e/tests/transfer/upgrades_test.go +++ b/e2e/tests/transfer/upgrades_test.go @@ -19,6 +19,7 @@ import ( "github.com/cosmos/ibc-go/e2e/testsuite/query" "github.com/cosmos/ibc-go/e2e/testvalues" feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" ) @@ -247,6 +248,423 @@ func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddleware_ }) } +// TestChannelUpgrade_WithICS20v2_Succeeds tests upgrading a transfer channel to ICS20 v2. +func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithICS20v2_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, func(opts *ibc.CreateChannelOptions) { + opts.Version = transfertypes.V1 + }) + + channelB := channelA.Counterparty + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBDenom := chainB.Config().Denom + chainAIBCToken := testsuite.GetIBCToken(chainBDenom, channelA.PortID, channelA.ChannelID) + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelB.PortID, channelB.ChannelID) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.FormattedAddress() + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("verify transfer version of channel A is ics20-1", func(t *testing.T) { + channel, err := query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V1, channel.Version, "the channel version is not ics20-1") + }) + + t.Run("native token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + chainBWalletAmount := ibc.WalletAmount{ + Address: chainBWallet.FormattedAddress(), // destination address + Denom: chainA.Config().Denom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + transferTxResp, err := chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(transferTxResp.Validate(), "chain-a ibc transfer tx is invalid") + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + + expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) + s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + chA, err := query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + upgradeFields := channeltypes.NewUpgradeFields(chA.Ordering, chA.ConnectionHops, transfertypes.V2) + s.InitiateChannelUpgrade(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, upgradeFields) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("verify channel A upgraded and transfer version is ics20-2", func(t *testing.T) { + channel, err := query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V2, channel.Version, "the channel version is not ics20-2") + }) + + t.Run("verify channel B upgraded and transfer version is ics20-2", func(t *testing.T) { + channel, err := query.Channel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V2, channel.Version, "the channel version is not ics20-2") + }) + + // send the native chainB denom and also the ibc token from chainA + transferCoins := []sdk.Coin{ + testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), + testvalues.DefaultTransferAmount(chainBDenom), + } + + t.Run("native token from chain B and non-native IBC token from chainA, both to chainA", func(t *testing.T) { + transferTxResp := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, transferCoins, chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainA), 0, "") + s.AssertTxSuccess(transferTxResp) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) + + t.Run("chain A native denom", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("chain B IBC denom", func(t *testing.T) { + actualBalance, err := query.Balance(ctx, chainA, chainAAddress, chainAIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + }) + + t.Run("tokens are un-escrowed", func(t *testing.T) { + t.Run("chain A escrow", func(t *testing.T) { + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + s.Require().Equal(sdk.NewCoin(chainADenom, sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because tokens have come back + }) + + t.Run("chain B escrow", func(t *testing.T) { + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainB, chainBDenom) + s.Require().NoError(err) + s.Require().Equal(sdk.NewCoin(chainBDenom, sdkmath.NewInt(testvalues.IBCTransferAmount)), actualTotalEscrow) + }) + }) +} + +// TestChannelUpgrade_WithFeeMiddlewareAndICS20v2_Succeeds tests upgrading a transfer channel to wire up fee middleware and upgrade to ICS20 v2. +func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddlewareAndICS20v2_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, func(opts *ibc.CreateChannelOptions) { + opts.Version = transfertypes.V1 + opts.SourcePortName = transfertypes.PortID + opts.DestPortName = transfertypes.PortID + }) + + channelB := channelA.Counterparty + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBDenom := chainB.Config().Denom + chainAIBCToken := testsuite.GetIBCToken(chainBDenom, channelA.PortID, channelA.ChannelID) + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelB.PortID, channelB.ChannelID) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + var ( + err error + channel channeltypes.Channel + ) + + t.Run("verify transfer version of channel A is ics20-1", func(t *testing.T) { + channel, err = query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V1, channel.Version, "the channel version is not ics20-1") + }) + + t.Run("native token transfer from chainB to chainA, sender is source of tokens", func(t *testing.T) { + chainAwalletAmount := ibc.WalletAmount{ + Address: chainAWallet.FormattedAddress(), // destination address + Denom: chainBDenom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + transferTxResp, err := chainB.SendIBCTransfer(ctx, channelB.ChannelID, chainBWallet.KeyName(), chainAwalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(transferTxResp.Validate(), "chain-b ibc transfer tx is invalid") + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + channel.Version = transfertypes.V2 // change version to ics20-2 + s.InitiateChannelUpgrade(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, s.CreateUpgradeFields(channel)) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("verify channel A upgraded and channel version is {ics29-1,ics20-2}", func(t *testing.T) { + channel, err = query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + + // check the channel version include the fee version + version, err := feetypes.MetadataFromVersion(channel.Version) + s.Require().NoError(err) + s.Require().Equal(feetypes.Version, version.FeeVersion, "the channel version did not include ics29") + s.Require().Equal(transfertypes.V2, version.AppVersion, "the channel version is not ics20-2") + }) + + t.Run("verify channel B upgraded and channel version is {ics29-1,ics20-2}", func(t *testing.T) { + channel, err = query.Channel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + + // check the channel version include the fee version + version, err := feetypes.MetadataFromVersion(channel.Version) + s.Require().NoError(err) + s.Require().Equal(feetypes.Version, version.FeeVersion, "the channel version did not include ics29") + s.Require().Equal(transfertypes.V2, version.AppVersion, "the channel version is not ics20-2") + }) + + var ( + chainARelayerWallet, chainBRelayerWallet ibc.Wallet + relayerAStartingBalance int64 + testFee = testvalues.DefaultFee(chainADenom) + ) + + t.Run("recover relayer wallets", func(t *testing.T) { + err := s.RecoverRelayerWallets(ctx, relayer) + s.Require().NoError(err) + + chainARelayerWallet, chainBRelayerWallet, err = s.GetRelayerWallets(relayer) + s.Require().NoError(err) + + relayerAStartingBalance, err = s.GetChainANativeBalance(ctx, chainARelayerWallet) + s.Require().NoError(err) + t.Logf("relayer A user starting with balance: %d", relayerAStartingBalance) + }) + + t.Run("register and verify counterparty payee", func(t *testing.T) { + _, chainBRelayerUser := s.GetRelayerUsers(ctx) + resp := s.RegisterCounterPartyPayee(ctx, chainB, chainBRelayerUser, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, chainBRelayerWallet.FormattedAddress(), chainARelayerWallet.FormattedAddress()) + s.AssertTxSuccess(resp) + + address, err := query.CounterPartyPayee(ctx, chainB, chainBRelayerWallet.FormattedAddress(), channelA.Counterparty.ChannelID) + s.Require().NoError(err) + s.Require().Equal(chainARelayerWallet.FormattedAddress(), address) + }) + + // send the native chainA denom and also the ibc token from chainB + denoms := []string{chainAIBCToken.IBCDenom(), chainADenom} + var transferCoins []sdk.Coin + for _, denom := range denoms { + transferCoins = append(transferCoins, testvalues.DefaultTransferAmount(denom)) + } + + t.Run("send incentivized transfer packet to chain B with native token from chain A and non-native IBC token from chainB", func(t *testing.T) { + // before adding fees for the packet, there should not be incentivized packets + packets, err := query.IncentivizedPacketsForChannel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Empty(packets) + + msgPayPacketFee := feetypes.NewMsgPayPacketFee(testFee, channelA.PortID, channelA.ChannelID, chainAWallet.FormattedAddress(), nil) + msgTransfer := testsuite.GetMsgTransfer( + channelA.PortID, + channelA.ChannelID, + transfertypes.V2, + transferCoins, + chainAWallet.FormattedAddress(), + chainBWallet.FormattedAddress(), + s.GetTimeoutHeight(ctx, chainB), + 0, + "", + ) + resp := s.BroadcastMessages(ctx, chainA, chainAWallet, msgPayPacketFee, msgTransfer) + s.AssertTxSuccess(resp) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + t.Run("chain B native denom", func(t *testing.T) { + actualBalance, err := s.GetChainBNativeBalance(ctx, chainBWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("chain A IBC denom", func(t *testing.T) { + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) + }) + + t.Run("timeout fee is refunded", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + // once the relayer has relayed the packets, the timeout fee should be refunded. + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount - testFee.AckFee.AmountOf(chainADenom).Int64() - testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("relayerA is paid ack and recv fee", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainARelayerWallet) + s.Require().NoError(err) + + expected := relayerAStartingBalance + testFee.AckFee.AmountOf(chainADenom).Int64() + testFee.RecvFee.AmountOf(chainADenom).Int64() + s.Require().Equal(expected, actualBalance) + }) + + t.Run("tokens are un-escrowed", func(t *testing.T) { + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainB, chainADenom) + s.Require().NoError(err) + s.Require().Equal(sdk.NewCoin(chainADenom, sdkmath.NewInt(0)), actualTotalEscrow) // total escrow is zero because tokens have come back + }) +} + +// TestChannelDowngrade_WithICS20v1_Succeeds tests downgrading a transfer channel from ICS20 v2 to ICS20 v1. +func (s *TransferChannelUpgradesTestSuite) TestChannelDowngrade_WithICS20v1_Succeeds() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, s.TransferChannelOptions()) + + channelB := channelA.Counterparty + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelB.PortID, channelB.ChannelID) + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.FormattedAddress() + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + var ( + err error + channel channeltypes.Channel + ) + + t.Run("verify transfer version of channel A is ics20-2", func(t *testing.T) { + channel, err = query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V2, channel.Version, "the channel version is not ics20-2") + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + t.Run("execute gov proposal to initiate channel upgrade", func(t *testing.T) { + upgradeFields := channeltypes.NewUpgradeFields(channel.Ordering, channel.ConnectionHops, transfertypes.V1) + s.InitiateChannelUpgrade(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, upgradeFields) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") + + t.Run("verify channel A downgraded and transfer version is ics20-1", func(t *testing.T) { + channel, err = query.Channel(ctx, chainA, channelA.PortID, channelA.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V1, channel.Version, "the channel version is not ics20-1") + }) + + t.Run("verify channel B downgraded and transfer version is ics20-1", func(t *testing.T) { + channel, err = query.Channel(ctx, chainB, channelB.PortID, channelB.ChannelID) + s.Require().NoError(err) + s.Require().Equal(transfertypes.V1, channel.Version, "the channel version is not ics20-1") + }) + + t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + chainBWalletAmount := ibc.WalletAmount{ + Address: chainBWallet.FormattedAddress(), // destination address + Denom: chainA.Config().Denom, + Amount: sdkmath.NewInt(testvalues.IBCTransferAmount), + } + + transferTxResp, err := chainA.SendIBCTransfer(ctx, channelA.ChannelID, chainAWallet.KeyName(), chainBWalletAmount, ibc.TransferOptions{}) + s.Require().NoError(err) + s.Require().NoError(transferTxResp.Validate(), "chain-a ibc transfer tx is invalid") + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + + actualTotalEscrow, err := query.TotalEscrowForDenom(ctx, chainA, chainADenom) + s.Require().NoError(err) + + expectedTotalEscrow := sdk.NewCoin(chainADenom, sdkmath.NewInt(testvalues.IBCTransferAmount)) + s.Require().Equal(expectedTotalEscrow, actualTotalEscrow) + }) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := query.Balance(ctx, chainB, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance.Int64()) + }) +} + // TestChannelUpgrade_WithFeeMiddleware_CrossingHello_Succeeds tests upgrading a transfer channel to wire up fee middleware under crossing hello func (s *TransferChannelUpgradesTestSuite) TestChannelUpgrade_WithFeeMiddleware_CrossingHello_Succeeds() { t := s.T()