From aad58a5aef33569d2afe9276b7b5aa313b8c83cd Mon Sep 17 00:00:00 2001
From: violet <158512193+fastfadingviolets@users.noreply.github.com>
Date: Thu, 24 Oct 2024 02:42:59 -0400
Subject: [PATCH] ci: Add interchaintests for user stories & v21 (#3400)

* ci: Add a test for LSM happy path ops

* ci: Add a test for LSM via ICA

* ci: Add a test for the ICA controller module

* ci: Add a PFM test

* ci: Test that rewards work with sovereign -> consumer changeovers
---
 .github/workflows/interchain-test.yml    |   2 +-
 tests/interchain/chainsuite/chain.go     | 150 ++++++-
 tests/interchain/chainsuite/chain_ics.go | 145 ++-----
 tests/interchain/chainsuite/config.go    | 103 +++--
 tests/interchain/chainsuite/relayer.go   | 158 ++++++--
 tests/interchain/consensus_test.go       |  34 +-
 tests/interchain/consumer_launch_test.go |  87 ++--
 tests/interchain/feemarket_test.go       | 221 ++++++++++
 tests/interchain/go.mod                  |   4 +-
 tests/interchain/go.sum                  |   4 +-
 tests/interchain/ica_controller_test.go  | 148 +++++++
 tests/interchain/lsm_test.go             | 493 +++++++++++++++++++++++
 tests/interchain/matrix_tool/main.go     |   4 +
 tests/interchain/permissionless_test.go  | 222 +++++++++-
 tests/interchain/pfm_test.go             | 133 ++++++
 tests/interchain/unbonding_test.go       |  23 +-
 16 files changed, 1703 insertions(+), 228 deletions(-)
 create mode 100644 tests/interchain/feemarket_test.go
 create mode 100644 tests/interchain/ica_controller_test.go
 create mode 100644 tests/interchain/lsm_test.go
 create mode 100644 tests/interchain/pfm_test.go

diff --git a/.github/workflows/interchain-test.yml b/.github/workflows/interchain-test.yml
index 8de73e3264e..38b16906b1c 100644
--- a/.github/workflows/interchain-test.yml
+++ b/.github/workflows/interchain-test.yml
@@ -42,7 +42,7 @@ jobs:
             matrix:
                 ${{fromJson(needs.prepare-matrix.outputs.matrix)}}
             fail-fast: false
-            max-parallel: 10
+            max-parallel: 3
         steps:
             - name: Check out repository code
               uses: actions/checkout@v4
diff --git a/tests/interchain/chainsuite/chain.go b/tests/interchain/chainsuite/chain.go
index 22741701e09..cd1e6b5808f 100644
--- a/tests/interchain/chainsuite/chain.go
+++ b/tests/interchain/chainsuite/chain.go
@@ -237,10 +237,10 @@ func (c *Chain) GetValidatorHex(ctx context.Context, val int) (string, error) {
 }
 
 func getValidatorWallets(ctx context.Context, chain *Chain) ([]ValidatorWallet, error) {
-	wallets := make([]ValidatorWallet, ValidatorCount)
+	wallets := make([]ValidatorWallet, len(chain.Validators))
 	lock := new(sync.Mutex)
 	eg := new(errgroup.Group)
-	for i := 0; i < ValidatorCount; i++ {
+	for i := range chain.Validators {
 		i := i
 		eg.Go(func() error {
 			// This moniker is hardcoded into the chain's genesis process.
@@ -309,3 +309,149 @@ func (c *Chain) GetProposalID(ctx context.Context, txhash string) (string, error
 	}
 	return "", fmt.Errorf("proposal ID not found in tx %s", txhash)
 }
+
+func (c *Chain) hasOrderingFlag(ctx context.Context) (bool, error) {
+	cmd := c.GetNode().BinCommand("tx", "interchain-accounts", "controller", "register", "--help")
+	stdout, _, err := c.GetNode().Exec(ctx, cmd, nil)
+	if err != nil {
+		return false, err
+	}
+	return strings.Contains(string(stdout), "ordering"), nil
+}
+
+func (c *Chain) GetICAAddress(ctx context.Context, srcAddress string, srcConnection string) string {
+	var icaAddress string
+
+	// it takes a moment for it to be created
+	timeoutCtx, timeoutCancel := context.WithTimeout(ctx, 90*time.Second)
+	defer timeoutCancel()
+	for timeoutCtx.Err() == nil {
+		time.Sleep(5 * time.Second)
+		stdout, _, err := c.GetNode().ExecQuery(timeoutCtx,
+			"interchain-accounts", "controller", "interchain-account",
+			srcAddress, srcConnection,
+		)
+		if err != nil {
+			GetLogger(ctx).Sugar().Warnf("error querying interchain account: %s", err)
+			continue
+		}
+		result := map[string]interface{}{}
+		err = json.Unmarshal(stdout, &result)
+		if err != nil {
+			GetLogger(ctx).Sugar().Warnf("error unmarshalling interchain account: %s", err)
+			continue
+		}
+		icaAddress = result["address"].(string)
+		if icaAddress != "" {
+			break
+		}
+	}
+	return icaAddress
+}
+
+func (c *Chain) SetupICAAccount(ctx context.Context, host *Chain, relayer *Relayer, srcAddress string, valIdx int, initialFunds int64) (string, error) {
+	srcChannel, err := relayer.GetTransferChannel(ctx, c, host)
+	if err != nil {
+		return "", err
+	}
+	srcConnection := srcChannel.ConnectionHops[0]
+
+	hasOrdering, err := c.hasOrderingFlag(ctx)
+	if err != nil {
+		return "", err
+	}
+
+	if hasOrdering {
+		_, err = c.Validators[valIdx].ExecTx(ctx, srcAddress,
+			"interchain-accounts", "controller", "register",
+			"--ordering", "ORDER_ORDERED", "--version", "",
+			srcConnection,
+		)
+	} else {
+		_, err = c.Validators[valIdx].ExecTx(ctx, srcAddress,
+			"interchain-accounts", "controller", "register",
+			srcConnection,
+		)
+	}
+	if err != nil {
+		return "", err
+	}
+
+	icaAddress := c.GetICAAddress(ctx, srcAddress, srcConnection)
+	if icaAddress == "" {
+		return "", fmt.Errorf("ICA address not found")
+	}
+
+	err = host.SendFunds(ctx, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{
+		Denom:   host.Config().Denom,
+		Amount:  sdkmath.NewInt(initialFunds),
+		Address: icaAddress,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return icaAddress, nil
+}
+
+func (c *Chain) AddLinkedChain(ctx context.Context, testName interchaintest.TestName, relayer *Relayer, spec *interchaintest.ChainSpec) (*Chain, error) {
+	dockerClient, dockerNetwork := GetDockerContext(ctx)
+
+	cf := interchaintest.NewBuiltinChainFactory(
+		GetLogger(ctx),
+		[]*interchaintest.ChainSpec{spec},
+	)
+
+	chains, err := cf.Chains(testName.Name())
+	if err != nil {
+		return nil, err
+	}
+	cosmosChainB := chains[0].(*cosmos.CosmosChain)
+	relayerWallet, err := cosmosChainB.BuildRelayerWallet(ctx, "relayer-"+cosmosChainB.Config().ChainID)
+	if err != nil {
+		return nil, err
+	}
+
+	ic := interchaintest.NewInterchain().AddChain(cosmosChainB, ibc.WalletAmount{
+		Address: relayerWallet.FormattedAddress(),
+		Denom:   cosmosChainB.Config().Denom,
+		Amount:  sdkmath.NewInt(ValidatorFunds),
+	})
+
+	if err := ic.Build(ctx, GetRelayerExecReporter(ctx), interchaintest.InterchainBuildOptions{
+		Client:    dockerClient,
+		NetworkID: dockerNetwork,
+		TestName:  testName.Name(),
+	}); err != nil {
+		return nil, err
+	}
+
+	chainB, err := chainFromCosmosChain(cosmosChainB, relayerWallet)
+	if err != nil {
+		return nil, err
+	}
+	rep := GetRelayerExecReporter(ctx)
+	if err := relayer.SetupChainKeys(ctx, chainB); err != nil {
+		return nil, err
+	}
+	if err := relayer.StopRelayer(ctx, rep); err != nil {
+		return nil, err
+	}
+	if err := relayer.StartRelayer(ctx, rep); err != nil {
+		return nil, err
+	}
+
+	if err := relayer.GeneratePath(ctx, rep, c.Config().ChainID, chainB.Config().ChainID, relayerTransferPathFor(c, chainB)); err != nil {
+		return nil, err
+	}
+
+	if err := relayer.LinkPath(ctx, rep, relayerTransferPathFor(c, chainB), ibc.CreateChannelOptions{
+		DestPortName:   TransferPortID,
+		SourcePortName: TransferPortID,
+		Order:          ibc.Unordered,
+	}, ibc.DefaultClientOpts()); err != nil {
+		return nil, err
+	}
+
+	return chainB, nil
+}
diff --git a/tests/interchain/chainsuite/chain_ics.go b/tests/interchain/chainsuite/chain_ics.go
index 0124af7773b..03f0652ffa0 100644
--- a/tests/interchain/chainsuite/chain_ics.go
+++ b/tests/interchain/chainsuite/chain_ics.go
@@ -15,7 +15,6 @@ import (
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
 	"go.uber.org/multierr"
-	"golang.org/x/mod/semver"
 
 	clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
 	ccvclient "github.com/cosmos/interchain-security/v5/x/ccv/provider/client"
@@ -29,18 +28,20 @@ import (
 type ConsumerBootstrapCb func(ctx context.Context, consumer *cosmos.CosmosChain)
 
 type ConsumerConfig struct {
-	ChainName             string
-	Version               string
-	Denom                 string
-	ShouldCopyProviderKey [ValidatorCount]bool
-	TopN                  int
-	ValidatorSetCap       int
-	ValidatorPowerCap     int
-	AllowInactiveVals     bool
-	MinStake              uint64
-	Allowlist             []string
-	Denylist              []string
-	spec                  *interchaintest.ChainSpec
+	ChainName                       string
+	Version                         string
+	Denom                           string
+	ShouldCopyProviderKey           [ValidatorCount]bool
+	TopN                            int
+	ValidatorSetCap                 int
+	ValidatorPowerCap               int
+	AllowInactiveVals               bool
+	MinStake                        uint64
+	Allowlist                       []string
+	Denylist                        []string
+	InitialHeight                   uint64
+	DistributionTransmissionChannel string
+	Spec                            *interchaintest.ChainSpec
 
 	DuringDepositPeriod ConsumerBootstrapCb
 	DuringVotingPeriod  ConsumerBootstrapCb
@@ -104,7 +105,8 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C
 	}
 
 	spawnTime := time.Now().Add(ChainSpawnWait)
-	chainID := fmt.Sprintf("%s-%d", config.ChainName, len(p.Consumers)+1)
+	// We need -test- in there because certain consumer IDs are hardcoded into the binary and we can't re-launch them
+	chainID := fmt.Sprintf("%s-test-%d", config.ChainName, len(p.Consumers)+1)
 
 	var proposalWaiter *proposalWaiter
 	var errCh chan error
@@ -123,15 +125,16 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C
 		}
 	}
 
-	if config.spec == nil {
-		config.spec = p.DefaultConsumerChainSpec(ctx, chainID, config, spawnTime, proposalWaiter)
-	}
-	if semver.Compare(p.GetNode().ICSVersion(ctx), "v4.1.0") > 0 && config.spec.InterchainSecurityConfig.ProviderVerOverride == "" {
-		config.spec.InterchainSecurityConfig.ProviderVerOverride = "v4.1.0"
+	defaultSpec := p.DefaultConsumerChainSpec(ctx, chainID, config, spawnTime, proposalWaiter)
+	config.Spec = MergeChainSpecs(defaultSpec, config.Spec)
+	providerICS := p.GetNode().ICSVersion(ctx)
+	if config.Spec.InterchainSecurityConfig.ConsumerVerOverride == "" {
+		// This will disable the genesis transform
+		config.Spec.InterchainSecurityConfig.ConsumerVerOverride = providerICS
 	}
 	cf := interchaintest.NewBuiltinChainFactory(
 		GetLogger(ctx),
-		[]*interchaintest.ChainSpec{config.spec},
+		[]*interchaintest.ChainSpec{config.Spec},
 	)
 	chains, err := cf.Chains(p.GetNode().TestName)
 	if err != nil {
@@ -202,7 +205,7 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C
 	if err := relayer.StartRelayer(ctx, rep); err != nil {
 		return nil, err
 	}
-	err = connectProviderConsumer(ctx, p, consumer, relayer)
+	err = relayer.ConnectProviderConsumer(ctx, p, consumer)
 	if err != nil {
 		return nil, err
 	}
@@ -211,10 +214,14 @@ func (p *Chain) AddConsumerChain(ctx context.Context, relayer *Relayer, config C
 }
 
 func (p *Chain) CreateConsumerPermissionless(ctx context.Context, chainID string, config ConsumerConfig, spawnTime time.Time) error {
+	revisionHeight := config.InitialHeight
+	if revisionHeight == 0 {
+		revisionHeight = 1
+	}
 	initParams := &providertypes.ConsumerInitializationParameters{
-		InitialHeight:                     clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(chainID), RevisionHeight: 1},
+		InitialHeight:                     clienttypes.Height{RevisionNumber: clienttypes.ParseChainID(chainID), RevisionHeight: revisionHeight},
 		SpawnTime:                         spawnTime,
-		BlocksPerDistributionTransmission: 1000,
+		BlocksPerDistributionTransmission: BlocksPerDistribution,
 		CcvTimeoutPeriod:                  2419200000000000,
 		TransferTimeoutPeriod:             3600000000000,
 		ConsumerRedistributionFraction:    "0.75",
@@ -222,6 +229,7 @@ func (p *Chain) CreateConsumerPermissionless(ctx context.Context, chainID string
 		UnbondingPeriod:                   1728000000000000,
 		GenesisHash:                       []byte("Z2VuX2hhc2g="),
 		BinaryHash:                        []byte("YmluX2hhc2g="),
+		DistributionTransmissionChannel:   config.DistributionTransmissionChannel,
 	}
 	powerShapingParams := &providertypes.PowerShapingParameters{
 		Top_N:              0,
@@ -402,6 +410,7 @@ func (p *Chain) DefaultConsumerChainSpec(ctx context.Context, chainID string, co
 			Denom:         denom,
 			GasPrices:     "0.005" + denom,
 			GasAdjustment: 2.0,
+			Gas:           "auto",
 			ChainID:       chainID,
 			ConfigFileOverrides: map[string]any{
 				"config/config.toml": DefaultConfigToml(),
@@ -451,83 +460,6 @@ func (p *Chain) DefaultConsumerChainSpec(ctx context.Context, chainID string, co
 	}
 }
 
-func connectProviderConsumer(ctx context.Context, provider *Chain, consumer *Chain, relayer *Relayer) error {
-	icsPath := relayerICSPathFor(provider, consumer)
-	rep := GetRelayerExecReporter(ctx)
-	if err := relayer.GeneratePath(ctx, rep, consumer.Config().ChainID, provider.Config().ChainID, icsPath); err != nil {
-		return err
-	}
-
-	consumerClients, err := relayer.GetClients(ctx, rep, consumer.Config().ChainID)
-	if err != nil {
-		return err
-	}
-
-	var consumerClient *ibc.ClientOutput
-	for _, client := range consumerClients {
-		if client.ClientState.ChainID == provider.Config().ChainID {
-			consumerClient = client
-			break
-		}
-	}
-	if consumerClient == nil {
-		return fmt.Errorf("consumer chain %s does not have a client tracking the provider chain %s", consumer.Config().ChainID, provider.Config().ChainID)
-	}
-	consumerClientID := consumerClient.ClientID
-
-	providerClients, err := relayer.GetClients(ctx, rep, provider.Config().ChainID)
-	if err != nil {
-		return err
-	}
-
-	var providerClient *ibc.ClientOutput
-	for _, client := range providerClients {
-		if client.ClientState.ChainID == consumer.Config().ChainID {
-			providerClient = client
-			break
-		}
-	}
-	if providerClient == nil {
-		return fmt.Errorf("provider chain %s does not have a client tracking the consumer chain %s for path %s on relayer %s",
-			provider.Config().ChainID, consumer.Config().ChainID, icsPath, relayer)
-	}
-	providerClientID := providerClient.ClientID
-
-	if err := relayer.UpdatePath(ctx, rep, icsPath, ibc.PathUpdateOptions{
-		SrcClientID: &consumerClientID,
-		DstClientID: &providerClientID,
-	}); err != nil {
-		return err
-	}
-
-	if err := relayer.CreateConnections(ctx, rep, icsPath); err != nil {
-		return err
-	}
-
-	if err := relayer.CreateChannel(ctx, rep, icsPath, ibc.CreateChannelOptions{
-		SourcePortName: "consumer",
-		DestPortName:   "provider",
-		Order:          ibc.Ordered,
-		Version:        "1",
-	}); err != nil {
-		return err
-	}
-
-	tCtx, tCancel := context.WithTimeout(ctx, 30*CommitTimeout)
-	defer tCancel()
-	for tCtx.Err() == nil {
-		var ch *ibc.ChannelOutput
-		ch, err = relayer.GetTransferChannel(ctx, provider, consumer)
-		if err == nil && ch != nil {
-			break
-		} else if err == nil {
-			err = fmt.Errorf("channel not found")
-		}
-		time.Sleep(CommitTimeout)
-	}
-	return err
-}
-
 func (p *Chain) SubmitConsumerAdditionProposal(ctx context.Context, chainID string, config ConsumerConfig, spawnTime time.Time) (*proposalWaiter, chan error, error) {
 	propWaiter := newProposalWaiter()
 	prop := p.buildConsumerAdditionJSON(chainID, config, spawnTime)
@@ -575,7 +507,7 @@ func (p *Chain) buildConsumerAdditionJSON(chainID string, config ConsumerConfig,
 		BinaryHash:    []byte("bin_hash"),
 		SpawnTime:     spawnTime,
 
-		BlocksPerDistributionTransmission: 1000,
+		BlocksPerDistributionTransmission: BlocksPerDistribution,
 		CcvTimeoutPeriod:                  2419200000000000,
 		TransferTimeoutPeriod:             3600000000000,
 		ConsumerRedistributionFraction:    "0.75",
@@ -644,6 +576,13 @@ func (p *Chain) CheckCCV(ctx context.Context, consumer *Chain, relayer *Relayer,
 		}
 	}
 
+	if err := relayer.ClearCCVChannel(ctx, p, consumer); err != nil {
+		return err
+	}
+	if err := testutil.WaitForBlocks(ctx, 2, p, consumer); err != nil {
+		return err
+	}
+
 	tCtx, tCancel := context.WithTimeout(ctx, 15*time.Minute)
 	defer tCancel()
 	var retErr error
@@ -670,10 +609,6 @@ func (p *Chain) CheckCCV(ctx context.Context, consumer *Chain, relayer *Relayer,
 	return retErr
 }
 
-func relayerICSPathFor(chainA, chainB *Chain) string {
-	return fmt.Sprintf("ics-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID)
-}
-
 func (p *Chain) IsValoperJailed(ctx context.Context, valoper string) (bool, error) {
 	out, _, err := p.Validators[0].ExecQuery(ctx, "staking", "validator", valoper)
 	if err != nil {
diff --git a/tests/interchain/chainsuite/config.go b/tests/interchain/chainsuite/config.go
index f67bd2a272f..0ff1ef2f222 100644
--- a/tests/interchain/chainsuite/config.go
+++ b/tests/interchain/chainsuite/config.go
@@ -49,32 +49,45 @@ const (
 	ChainSpawnWait         = 155 * time.Second
 	SlashingWindowConsumer = 20
 	BlocksPerDistribution  = 10
+	StrideVersion          = "v22.0.0"
+	NeutronVersion         = "v3.0.2"
+	TransferPortID         = "transfer"
+	// This is needed because not every ics image is in the default heighliner registry
+	HyphaICSRepo = "ghcr.io/hyphacoop/ics"
+	ICSUidGuid   = "1025:1025"
 )
 
-func (c SuiteConfig) Merge(other SuiteConfig) SuiteConfig {
-	if c.ChainSpec == nil {
-		c.ChainSpec = other.ChainSpec
-	} else if other.ChainSpec != nil {
-		c.ChainSpec.ChainConfig = c.ChainSpec.MergeChainSpecConfig(other.ChainSpec.ChainConfig)
-		if other.ChainSpec.Name != "" {
-			c.ChainSpec.Name = other.ChainSpec.Name
-		}
-		if other.ChainSpec.ChainName != "" {
-			c.ChainSpec.ChainName = other.ChainSpec.ChainName
-		}
-		if other.ChainSpec.Version != "" {
-			c.ChainSpec.Version = other.ChainSpec.Version
-		}
-		if other.ChainSpec.NoHostMount != nil {
-			c.ChainSpec.NoHostMount = other.ChainSpec.NoHostMount
-		}
-		if other.ChainSpec.NumValidators != nil {
-			c.ChainSpec.NumValidators = other.ChainSpec.NumValidators
-		}
-		if other.ChainSpec.NumFullNodes != nil {
-			c.ChainSpec.NumFullNodes = other.ChainSpec.NumFullNodes
-		}
+func MergeChainSpecs(spec, other *interchaintest.ChainSpec) *interchaintest.ChainSpec {
+	if spec == nil {
+		return other
+	}
+	if other == nil {
+		return spec
+	}
+	spec.ChainConfig = spec.MergeChainSpecConfig(other.ChainConfig)
+	if other.Name != "" {
+		spec.Name = other.Name
+	}
+	if other.ChainName != "" {
+		spec.ChainName = other.ChainName
+	}
+	if other.Version != "" {
+		spec.Version = other.Version
 	}
+	if other.NoHostMount != nil {
+		spec.NoHostMount = other.NoHostMount
+	}
+	if other.NumValidators != nil {
+		spec.NumValidators = other.NumValidators
+	}
+	if other.NumFullNodes != nil {
+		spec.NumFullNodes = other.NumFullNodes
+	}
+	return spec
+}
+
+func (c SuiteConfig) Merge(other SuiteConfig) SuiteConfig {
+	c.ChainSpec = MergeChainSpecs(c.ChainSpec, other.ChainSpec)
 	c.UpgradeOnSetup = other.UpgradeOnSetup
 	c.CreateRelayer = other.CreateRelayer
 	c.Scope = other.Scope
@@ -100,7 +113,7 @@ func DefaultGenesisAmounts(denom string) func(i int) (types.Coin, types.Coin) {
 	}
 }
 
-func DefaultSuiteConfig(env Environment) SuiteConfig {
+func DefaultChainSpec(env Environment) *interchaintest.ChainSpec {
 	fullNodes := 0
 	validators := ValidatorCount
 	var repository string
@@ -109,30 +122,34 @@ func DefaultSuiteConfig(env Environment) SuiteConfig {
 	} else {
 		repository = fmt.Sprintf("%s/%s", env.DockerRegistry, env.GaiaImageName)
 	}
-	return SuiteConfig{
-		ChainSpec: &interchaintest.ChainSpec{
-			Name:          "gaia",
-			NumFullNodes:  &fullNodes,
-			NumValidators: &validators,
-			Version:       env.OldGaiaImageVersion,
-			ChainConfig: ibc.ChainConfig{
-				Denom:         Uatom,
-				GasPrices:     GasPrices,
-				GasAdjustment: 2.0,
-				ConfigFileOverrides: map[string]any{
-					"config/config.toml": DefaultConfigToml(),
-				},
-				Images: []ibc.DockerImage{{
-					Repository: repository,
-					UidGid:     "1025:1025", // this is the user in heighliner docker images
-				}},
-				ModifyGenesis:        cosmos.ModifyGenesis(DefaultGenesis()),
-				ModifyGenesisAmounts: DefaultGenesisAmounts(Uatom),
+	return &interchaintest.ChainSpec{
+		Name:          "gaia",
+		NumFullNodes:  &fullNodes,
+		NumValidators: &validators,
+		Version:       env.OldGaiaImageVersion,
+		ChainConfig: ibc.ChainConfig{
+			Denom:         Uatom,
+			GasPrices:     GasPrices,
+			GasAdjustment: 2.0,
+			ConfigFileOverrides: map[string]any{
+				"config/config.toml": DefaultConfigToml(),
 			},
+			Images: []ibc.DockerImage{{
+				Repository: repository,
+				UidGid:     "1025:1025", // this is the user in heighliner docker images
+			}},
+			ModifyGenesis:        cosmos.ModifyGenesis(DefaultGenesis()),
+			ModifyGenesisAmounts: DefaultGenesisAmounts(Uatom),
 		},
 	}
 }
 
+func DefaultSuiteConfig(env Environment) SuiteConfig {
+	return SuiteConfig{
+		ChainSpec: DefaultChainSpec(env),
+	}
+}
+
 func DefaultConfigToml() testutil.Toml {
 	configToml := make(testutil.Toml)
 	consensusToml := make(testutil.Toml)
diff --git a/tests/interchain/chainsuite/relayer.go b/tests/interchain/chainsuite/relayer.go
index 0b5c308603a..26a64d48891 100644
--- a/tests/interchain/chainsuite/relayer.go
+++ b/tests/interchain/chainsuite/relayer.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sort"
+	"time"
 
 	"github.com/strangelove-ventures/interchaintest/v8"
 	"github.com/strangelove-ventures/interchaintest/v8/ibc"
@@ -41,7 +43,7 @@ func (r *Relayer) SetupChainKeys(ctx context.Context, chain *Chain) error {
 }
 
 func (r *Relayer) GetTransferChannel(ctx context.Context, chain, counterparty *Chain) (*ibc.ChannelOutput, error) {
-	return r.GetChannelWithPort(ctx, chain, counterparty, "transfer")
+	return r.GetChannelWithPort(ctx, chain, counterparty, TransferPortID)
 }
 
 func (r *Relayer) GetChannelWithPort(ctx context.Context, chain, counterparty *Chain, portID string) (*ibc.ChannelOutput, error) {
@@ -49,37 +51,30 @@ func (r *Relayer) GetChannelWithPort(ctx context.Context, chain, counterparty *C
 	if err != nil {
 		return nil, err
 	}
-	var client *ibc.ClientOutput
 	for _, c := range clients {
 		if c.ClientState.ChainID == counterparty.Config().ChainID {
-			client = c
-			break
-		}
-	}
-	if client == nil {
-		return nil, fmt.Errorf("no client found for chain %s", counterparty.Config().ChainID)
-	}
-
-	stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "connection", "connections")
-	if err != nil {
-		return nil, fmt.Errorf("error querying connections: %w", err)
-	}
-	connections := gjson.GetBytes(stdout, fmt.Sprintf("connections.#(client_id==\"%s\")#.id", client.ClientID)).Array()
-	if len(connections) == 0 {
-		return nil, fmt.Errorf("no connections found for client %s", client.ClientID)
-	}
-	for _, connID := range connections {
-		stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "channel", "connections", connID.String())
-		if err != nil {
-			return nil, err
-		}
-		channelJSON := gjson.GetBytes(stdout, fmt.Sprintf("channels.#(port_id==\"%s\")", portID)).String()
-		if channelJSON != "" {
-			channelOutput := &ibc.ChannelOutput{}
-			if err := json.Unmarshal([]byte(channelJSON), channelOutput); err != nil {
-				return nil, fmt.Errorf("error unmarshalling channel output %s: %w", channelJSON, err)
+			stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "connection", "connections")
+			if err != nil {
+				return nil, fmt.Errorf("error querying connections: %w", err)
+			}
+			connections := gjson.GetBytes(stdout, fmt.Sprintf("connections.#(client_id==\"%s\")#.id", c.ClientID)).Array()
+			if len(connections) == 0 {
+				continue
+			}
+			for _, connID := range connections {
+				stdout, _, err := chain.GetNode().ExecQuery(ctx, "ibc", "channel", "connections", connID.String())
+				if err != nil {
+					return nil, err
+				}
+				channelJSON := gjson.GetBytes(stdout, fmt.Sprintf("channels.#(port_id==\"%s\")", portID)).String()
+				if channelJSON != "" {
+					channelOutput := &ibc.ChannelOutput{}
+					if err := json.Unmarshal([]byte(channelJSON), channelOutput); err != nil {
+						return nil, fmt.Errorf("error unmarshalling channel output %s: %w", channelJSON, err)
+					}
+					return channelOutput, nil
+				}
 			}
-			return channelOutput, nil
 		}
 	}
 	return nil, fmt.Errorf("no channel found for port %s", portID)
@@ -100,3 +95,108 @@ func (r *Relayer) ClearCCVChannel(ctx context.Context, provider, consumer *Chain
 	}
 	return nil
 }
+
+func (r *Relayer) ClearTransferChannel(ctx context.Context, chainA, chainB *Chain) error {
+	channel, err := r.GetTransferChannel(ctx, chainA, chainB)
+	if err != nil {
+		return err
+	}
+	rs := r.Exec(ctx, GetRelayerExecReporter(ctx), []string{
+		"hermes", "clear", "packets", "--port", channel.PortID, "--channel", channel.ChannelID,
+		"--chain", chainA.Config().ChainID,
+	}, nil)
+	if rs.Err != nil {
+		return fmt.Errorf("error clearing packets: %w", rs.Err)
+	}
+	return nil
+}
+
+func (r *Relayer) ConnectProviderConsumer(ctx context.Context, provider *Chain, consumer *Chain) error {
+	icsPath := relayerICSPathFor(provider, consumer)
+	rep := GetRelayerExecReporter(ctx)
+	if err := r.GeneratePath(ctx, rep, consumer.Config().ChainID, provider.Config().ChainID, icsPath); err != nil {
+		return err
+	}
+
+	consumerClients, err := r.GetClients(ctx, rep, consumer.Config().ChainID)
+	if err != nil {
+		return err
+	}
+	sort.Slice(consumerClients, func(i, j int) bool {
+		return consumerClients[i].ClientID > consumerClients[j].ClientID
+	})
+	var consumerClient *ibc.ClientOutput
+	for _, client := range consumerClients {
+		if client.ClientState.ChainID == provider.Config().ChainID {
+			consumerClient = client
+			break
+		}
+	}
+	if consumerClient == nil {
+		return fmt.Errorf("consumer chain %s does not have a client tracking the provider chain %s", consumer.Config().ChainID, provider.Config().ChainID)
+	}
+	consumerClientID := consumerClient.ClientID
+
+	providerClients, err := r.GetClients(ctx, rep, provider.Config().ChainID)
+	if err != nil {
+		return err
+	}
+	sort.Slice(providerClients, func(i, j int) bool {
+		return providerClients[i].ClientID > providerClients[j].ClientID
+	})
+
+	var providerClient *ibc.ClientOutput
+	for _, client := range providerClients {
+		if client.ClientState.ChainID == consumer.Config().ChainID {
+			providerClient = client
+			break
+		}
+	}
+	if providerClient == nil {
+		return fmt.Errorf("provider chain %s does not have a client tracking the consumer chain %s for path %s on relayer %s",
+			provider.Config().ChainID, consumer.Config().ChainID, icsPath, r)
+	}
+	providerClientID := providerClient.ClientID
+
+	if err := r.UpdatePath(ctx, rep, icsPath, ibc.PathUpdateOptions{
+		SrcClientID: &consumerClientID,
+		DstClientID: &providerClientID,
+	}); err != nil {
+		return err
+	}
+
+	if err := r.CreateConnections(ctx, rep, icsPath); err != nil {
+		return err
+	}
+
+	if err := r.CreateChannel(ctx, rep, icsPath, ibc.CreateChannelOptions{
+		SourcePortName: "consumer",
+		DestPortName:   "provider",
+		Order:          ibc.Ordered,
+		Version:        "1",
+	}); err != nil {
+		return err
+	}
+
+	tCtx, tCancel := context.WithTimeout(ctx, 30*CommitTimeout)
+	defer tCancel()
+	for tCtx.Err() == nil {
+		var ch *ibc.ChannelOutput
+		ch, err = r.GetTransferChannel(ctx, provider, consumer)
+		if err == nil && ch != nil {
+			break
+		} else if err == nil {
+			err = fmt.Errorf("channel not found")
+		}
+		time.Sleep(CommitTimeout)
+	}
+	return err
+}
+
+func relayerICSPathFor(chainA, chainB *Chain) string {
+	return fmt.Sprintf("ics-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID)
+}
+
+func relayerTransferPathFor(chainA, chainB *Chain) string {
+	return fmt.Sprintf("transfer-%s-%s", chainA.Config().ChainID, chainB.Config().ChainID)
+}
diff --git a/tests/interchain/consensus_test.go b/tests/interchain/consensus_test.go
index 8f09f9f09e4..a256731e4b9 100644
--- a/tests/interchain/consensus_test.go
+++ b/tests/interchain/consensus_test.go
@@ -6,10 +6,13 @@ import (
 	"testing"
 
 	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
+	"github.com/strangelove-ventures/interchaintest/v8"
+	"github.com/strangelove-ventures/interchaintest/v8/ibc"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
+	"golang.org/x/mod/semver"
 )
 
 const (
@@ -49,15 +52,21 @@ func (s *ConsensusSuite) SetupSuite() {
 	result, err := s.Chain.SubmitProposal(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, prop)
 	s.Require().NoError(err)
 	s.Require().NoError(s.Chain.PassProposal(s.GetContext(), result.ProposalID))
+
 	s.UpgradeChain()
 
 	stakingParams, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "staking", "params")
 	s.Require().NoError(err)
-	s.Require().Equal(uint64(200), gjson.GetBytes(stakingParams, "params.max_validators").Uint(), string(stakingParams))
 
 	providerParams, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "params")
 	s.Require().NoError(err)
-	s.Require().Equal(uint64(180), gjson.GetBytes(providerParams, "max_provider_consensus_validators").Uint(), string(providerParams))
+
+	if semver.Compare(s.Env.OldGaiaImageVersion, "v20.0.0") < 0 {
+		// These are set by the v20 upgrade handler
+		s.Require().Equal(uint64(200), gjson.GetBytes(stakingParams, "params.max_validators").Uint(), string(stakingParams))
+		s.Require().Equal(uint64(180), gjson.GetBytes(providerParams, "max_provider_consensus_validators").Uint(), string(providerParams))
+	}
+
 	providerParams, err = sjson.SetBytes(providerParams, "max_provider_consensus_validators", maxProviderConsensusValidators)
 	s.Require().NoError(err)
 	providerProposal, err := sjson.SetRaw(fmt.Sprintf(`{
@@ -84,12 +93,23 @@ func (s *ConsensusSuite) SetupSuite() {
 
 	cfg := chainsuite.ConsumerConfig{
 		ChainName:             "ics-consumer",
-		Version:               "v5.0.0",
+		Version:               "v6.2.1",
 		ShouldCopyProviderKey: allProviderKeysCopied(),
 		Denom:                 chainsuite.Ucon,
 		TopN:                  100,
 		AllowInactiveVals:     true,
 		MinStake:              1_000_000,
+		Spec: &interchaintest.ChainSpec{
+			ChainConfig: ibc.ChainConfig{
+				Images: []ibc.DockerImage{
+					{
+						Repository: chainsuite.HyphaICSRepo,
+						Version:    "v6.2.1",
+						UidGid:     chainsuite.ICSUidGuid,
+					},
+				},
+			},
+		},
 	}
 	consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
@@ -109,7 +129,7 @@ func (s *ConsensusSuite) Test0ValidatorSets() {
 		s.Require().Equal(s.Chain.ValidatorWallets[i].ValConsAddress, valCons)
 	}
 
-	vals, err = s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set")
+	vals, err = s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set")
 	s.Require().NoError(err)
 	s.Require().Equal(maxProviderConsensusValidators, len(vals.Array()), vals)
 	for i := 0; i < maxProviderConsensusValidators; i++ {
@@ -161,7 +181,7 @@ func (s *ConsensusSuite) TestOptInInactive() {
 		s.Require().NoError(err)
 		s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer)
 		s.Require().EventuallyWithT(func(c *assert.CollectT) {
-			vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set")
+			vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set")
 			assert.NoError(c, err)
 			assert.Equal(c, maxProviderConsensusValidators, len(vals.Array()), vals)
 		}, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout)
@@ -171,7 +191,7 @@ func (s *ConsensusSuite) TestOptInInactive() {
 	}()
 	s.Require().NoError(s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer))
 	s.Require().EventuallyWithT(func(c *assert.CollectT) {
-		vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set")
+		vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set")
 		assert.NoError(c, err)
 		assert.Equal(c, maxProviderConsensusValidators+1, len(vals.Array()), vals)
 	}, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout)
@@ -182,7 +202,7 @@ func (s *ConsensusSuite) TestOptInInactive() {
 	_, err = s.Chain.Validators[5].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[5].Moniker, "provider", "opt-in", consumerID)
 	s.Require().NoError(err)
 	s.Require().NoError(s.Relayer.ClearCCVChannel(s.GetContext(), s.Chain, s.Consumer))
-	vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set")
+	vals, err := s.Consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set")
 	s.Require().NoError(err)
 	s.Require().Equal(maxProviderConsensusValidators+1, len(vals.Array()), vals)
 	jailed, err = s.Chain.IsValidatorJailedForConsumerDowntime(s.GetContext(), s.Relayer, s.Consumer, 5)
diff --git a/tests/interchain/consumer_launch_test.go b/tests/interchain/consumer_launch_test.go
index 8cf9204f9ab..ae4c343adc8 100644
--- a/tests/interchain/consumer_launch_test.go
+++ b/tests/interchain/consumer_launch_test.go
@@ -3,16 +3,20 @@ package interchain_test
 import (
 	"testing"
 
+	"github.com/strangelove-ventures/interchaintest/v8"
+	"github.com/strangelove-ventures/interchaintest/v8/ibc"
 	"github.com/stretchr/testify/suite"
+	"golang.org/x/mod/semver"
 
 	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
 )
 
 type ConsumerLaunchSuite struct {
 	*chainsuite.Suite
-	OtherChain            string
-	OtherChainVersion     string
-	ShouldCopyProviderKey [chainsuite.ValidatorCount]bool
+	OtherChain                   string
+	OtherChainVersionPreUpgrade  string
+	OtherChainVersionPostUpgrade string
+	ShouldCopyProviderKey        [chainsuite.ValidatorCount]bool
 }
 
 func noProviderKeysCopied() [chainsuite.ValidatorCount]bool {
@@ -30,10 +34,21 @@ func someProviderKeysCopied() [chainsuite.ValidatorCount]bool {
 func (s *ConsumerLaunchSuite) TestChainLaunch() {
 	cfg := chainsuite.ConsumerConfig{
 		ChainName:             s.OtherChain,
-		Version:               s.OtherChainVersion,
+		Version:               s.OtherChainVersionPreUpgrade,
 		ShouldCopyProviderKey: s.ShouldCopyProviderKey,
 		Denom:                 chainsuite.Ucon,
 		TopN:                  94,
+		Spec: &interchaintest.ChainSpec{
+			ChainConfig: ibc.ChainConfig{
+				Images: []ibc.DockerImage{
+					{
+						Repository: chainsuite.HyphaICSRepo,
+						Version:    s.OtherChainVersionPreUpgrade,
+						UidGid:     chainsuite.ICSUidGuid,
+					},
+				},
+			},
+		},
 	}
 	consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
@@ -53,6 +68,8 @@ func (s *ConsumerLaunchSuite) TestChainLaunch() {
 	s.Require().NoError(err)
 	s.Require().False(jailed, "validator 5 should not be jailed for downtime")
 
+	cfg.Version = s.OtherChainVersionPostUpgrade
+	cfg.Spec.ChainConfig.Images[0].Version = s.OtherChainVersionPostUpgrade
 	consumer2, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
 	err = s.Chain.CheckCCV(s.GetContext(), consumer2, s.Relayer, 1_000_000, 0, 1)
@@ -67,42 +84,53 @@ func (s *ConsumerLaunchSuite) TestChainLaunch() {
 	s.Require().False(jailed, "validator 5 should not be jailed for downtime")
 }
 
-func TestICS40ChainLaunch(t *testing.T) {
+func selectConsumerVersion(preV21, postV21 string) string {
+	if semver.Compare(semver.Major(chainsuite.GetEnvironment().OldGaiaImageVersion), "v21") >= 0 {
+		return postV21
+	}
+	return preV21
+}
+
+func TestICS4ChainLaunch(t *testing.T) {
 	s := &ConsumerLaunchSuite{
-		Suite:                 chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
-		OtherChain:            "ics-consumer",
-		OtherChainVersion:     "v4.0.0",
-		ShouldCopyProviderKey: noProviderKeysCopied(),
+		Suite:                        chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
+		OtherChain:                   "ics-consumer",
+		OtherChainVersionPreUpgrade:  selectConsumerVersion("v4.4.1", "v4.5.0"),
+		OtherChainVersionPostUpgrade: "v4.5.0",
+		ShouldCopyProviderKey:        noProviderKeysCopied(),
 	}
 	suite.Run(t, s)
 }
 
-func TestICS33ConsumerAllKeysChainLaunch(t *testing.T) {
+func TestICS6ConsumerAllKeysChainLaunch(t *testing.T) {
 	s := &ConsumerLaunchSuite{
-		Suite:                 chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
-		OtherChain:            "ics-consumer",
-		OtherChainVersion:     "v3.3.0",
-		ShouldCopyProviderKey: allProviderKeysCopied(),
+		Suite:                        chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
+		OtherChain:                   "ics-consumer",
+		OtherChainVersionPreUpgrade:  selectConsumerVersion("v6.0.0", "v6.2.1"),
+		OtherChainVersionPostUpgrade: "v6.2.1",
+		ShouldCopyProviderKey:        allProviderKeysCopied(),
 	}
 	suite.Run(t, s)
 }
 
-func TestICS33ConsumerSomeKeysChainLaunch(t *testing.T) {
+func TestICS6ConsumerSomeKeysChainLaunch(t *testing.T) {
 	s := &ConsumerLaunchSuite{
-		Suite:                 chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
-		OtherChain:            "ics-consumer",
-		OtherChainVersion:     "v3.3.0",
-		ShouldCopyProviderKey: someProviderKeysCopied(),
+		Suite:                        chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
+		OtherChain:                   "ics-consumer",
+		OtherChainVersionPreUpgrade:  selectConsumerVersion("v6.0.0", "v6.2.1"),
+		OtherChainVersionPostUpgrade: "v6.2.1",
+		ShouldCopyProviderKey:        someProviderKeysCopied(),
 	}
 	suite.Run(t, s)
 }
 
-func TestICS33ConsumerNoKeysChainLaunch(t *testing.T) {
+func TestICS6ConsumerNoKeysChainLaunch(t *testing.T) {
 	s := &ConsumerLaunchSuite{
-		Suite:                 chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
-		OtherChain:            "ics-consumer",
-		OtherChainVersion:     "v3.3.0",
-		ShouldCopyProviderKey: noProviderKeysCopied(),
+		Suite:                        chainsuite.NewSuite(chainsuite.SuiteConfig{CreateRelayer: true}),
+		OtherChain:                   "ics-consumer",
+		OtherChainVersionPreUpgrade:  selectConsumerVersion("v6.0.0", "v6.2.1"),
+		OtherChainVersionPostUpgrade: "v6.2.1",
+		ShouldCopyProviderKey:        noProviderKeysCopied(),
 	}
 	suite.Run(t, s)
 }
@@ -112,12 +140,13 @@ type MainnetConsumerChainsSuite struct {
 }
 
 func (s *MainnetConsumerChainsSuite) TestMainnetConsumerChainsAfterUpgrade() {
-	const neutronVersion = "v3.0.2"
-	const strideVersion = "v22.0.0"
-
+	// We can't do these consumer launches yet because the chains aren't compatible with launching on v21 yet
+	if semver.Major(s.Env.OldGaiaImageVersion) == s.Env.UpgradeName && s.Env.UpgradeName == "v21" {
+		s.T().Skip("Skipping Consumer Launch tests when going from v21 -> v21")
+	}
 	neutron, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{
 		ChainName:             "neutron",
-		Version:               neutronVersion,
+		Version:               chainsuite.NeutronVersion,
 		ShouldCopyProviderKey: allProviderKeysCopied(),
 		Denom:                 chainsuite.NeutronDenom,
 		TopN:                  95,
@@ -125,7 +154,7 @@ func (s *MainnetConsumerChainsSuite) TestMainnetConsumerChainsAfterUpgrade() {
 	s.Require().NoError(err)
 	stride, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{
 		ChainName:             "stride",
-		Version:               strideVersion,
+		Version:               chainsuite.StrideVersion,
 		ShouldCopyProviderKey: allProviderKeysCopied(),
 		Denom:                 chainsuite.StrideDenom,
 		TopN:                  95,
diff --git a/tests/interchain/feemarket_test.go b/tests/interchain/feemarket_test.go
new file mode 100644
index 00000000000..473915832ac
--- /dev/null
+++ b/tests/interchain/feemarket_test.go
@@ -0,0 +1,221 @@
+package interchain_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
+	"github.com/strangelove-ventures/interchaintest/v8"
+	"github.com/strangelove-ventures/interchaintest/v8/chain/cosmos"
+	"github.com/strangelove-ventures/interchaintest/v8/testutil"
+	"github.com/stretchr/testify/suite"
+	"github.com/tidwall/sjson"
+	"golang.org/x/sync/errgroup"
+)
+
+type FeemarketSuite struct {
+	*chainsuite.Suite
+}
+
+func (s *FeemarketSuite) TestGasGoesUp() {
+	const (
+		txsPerBlock         = 600
+		blocksToPack        = 5
+		maxBlockUtilization = 1000000
+	)
+
+	s.setMaxBlockUtilization(maxBlockUtilization)
+
+	s.setCommitTimeout(150 * time.Second)
+
+	s.packBlocks(txsPerBlock, blocksToPack)
+}
+
+func (s *FeemarketSuite) setMaxBlockUtilization(utilization int) {
+	params, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "feemarket", "params")
+	s.Require().NoError(err)
+
+	params, err = sjson.SetBytes(params, "max_block_utilization", fmt.Sprint(utilization))
+	s.Require().NoError(err)
+
+	govAuthority, err := s.Chain.GetGovernanceAddress(s.GetContext())
+	s.Require().NoError(err)
+
+	proposalJson := fmt.Sprintf(`{
+		"@type": "/feemarket.feemarket.v1.MsgParams",
+		"authority": "%s"
+}`, govAuthority)
+	proposalJson, err = sjson.SetRaw(proposalJson, "params", string(params))
+	s.Require().NoError(err)
+
+	txhash, err := s.Chain.GetNode().SubmitProposal(s.GetContext(), interchaintest.FaucetAccountKeyName,
+		cosmos.TxProposalv1{
+			Title:    "Set Block Params",
+			Deposit:  chainsuite.GovDepositAmount,
+			Messages: []json.RawMessage{json.RawMessage(proposalJson)},
+			Summary:  "Set Block Params",
+			Metadata: "ipfs://CID",
+		})
+	s.Require().NoError(err)
+
+	propId, err := s.Chain.GetProposalID(s.GetContext(), txhash)
+	s.Require().NoError(err)
+	s.Require().NoError(s.Chain.PassProposal(s.GetContext(), propId))
+	maxBlockResult, err := s.Chain.QueryJSON(s.GetContext(), "max_block_utilization", "feemarket", "params")
+	s.Require().NoError(err)
+	maxBlock := maxBlockResult.String()
+	s.Require().Equal(fmt.Sprint(utilization), maxBlock)
+
+}
+
+func (s *FeemarketSuite) packBlocks(txsPerBlock, blocksToPack int) {
+	script := `
+#!/bin/sh
+
+set -ue
+set -o pipefail
+
+TX_COUNT=$1
+CHAIN_BINARY=$2
+FROM=$3
+TO=$4
+DENOM=$5
+GAS_PRICES=$6
+CHAIN_ID=$7
+VAL_HOME=$8
+NODE=$9
+
+i=0
+
+
+cd $HOME
+
+SEQUENCE=$($CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json | jq -r .account.value.sequence)
+ACCOUNT=$($CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json | jq -r .account.value.account_number)
+
+if [ $SEQUENCE == "null" ]; then
+	$CHAIN_BINARY query auth account $FROM --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME -o json >&2
+	exit 1
+fi
+
+if [ $ACCOUNT == "null" ]; then
+	ACCOUNT=0
+fi
+
+$CHAIN_BINARY tx bank send $FROM $TO 1$DENOM --keyring-backend test --generate-only --account-number $ACCOUNT --from $FROM --chain-id $CHAIN_ID --gas 500000 --gas-adjustment 2.0 --gas-prices $GAS_PRICES$DENOM --home $VAL_HOME --node $NODE -o json > tx.json
+
+while [ $i -lt $TX_COUNT ]; do
+	$CHAIN_BINARY tx sign tx.json --from $FROM --chain-id $CHAIN_ID --sequence $SEQUENCE --keyring-backend test --account-number $ACCOUNT --offline --home $VAL_HOME > tx.json.signed
+	tx=$($CHAIN_BINARY tx broadcast tx.json.signed --node $NODE --chain-id $CHAIN_ID --home $VAL_HOME -o json)
+	if [ $(echo $tx | jq -r .code) -ne 0 ]; then
+		echo "$tx" >&2
+		$CHAIN_BINARY query tx $(echo $tx | jq -r .txhash) --chain-id $CHAIN_ID --node $NODE --home $VAL_HOME >&2
+		exit 1
+	else
+		echo $(echo $tx | jq -r .txhash)
+	fi
+	SEQUENCE=$((SEQUENCE+1))
+	i=$((i+1))
+done
+`
+	for _, val := range s.Chain.Validators {
+		err := val.WriteFile(s.GetContext(), []byte(script), "pack.sh")
+		s.Require().NoError(err)
+	}
+	wallets := s.Chain.ValidatorWallets
+
+	gasResult, err := s.Chain.QueryJSON(s.GetContext(), "price.amount", "feemarket", "gas-price", s.Chain.Config().Denom)
+	s.Require().NoError(err)
+	gasStr := gasResult.String()
+	gasBefore, err := strconv.ParseFloat(gasStr, 64)
+	s.Require().NoError(err)
+	gasNow := gasBefore
+
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain))
+
+	prevBlock, err := s.Chain.Height(s.GetContext())
+	s.Require().NoError(err)
+
+	for i := 0; i < blocksToPack; i++ {
+		eg := errgroup.Group{}
+		for v, val := range s.Chain.Validators {
+			val := val
+			v := v
+			eg.Go(func() error {
+				_, stderr, err := val.Exec(s.GetContext(), []string{
+					"sh", path.Join(val.HomeDir(), "pack.sh"),
+					strconv.Itoa(txsPerBlock / len(s.Chain.Validators)),
+					s.Chain.Config().Bin,
+					wallets[v].Address,
+					wallets[(v+1)%len(s.Chain.Validators)].Address,
+					s.Chain.Config().Denom,
+					fmt.Sprint(gasNow),
+					s.Chain.Config().ChainID,
+					val.HomeDir(),
+					fmt.Sprintf("tcp://%s:26657", val.HostName()),
+				}, nil)
+
+				if err != nil {
+					return fmt.Errorf("validator %d, err %w, stderr: %s", v, err, stderr)
+				} else if len(stderr) > 0 {
+					return fmt.Errorf("stderr: %s", stderr)
+				}
+				return nil
+			})
+		}
+		s.Require().NoError(eg.Wait())
+		s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain))
+		time.Sleep(5 * time.Second) // ensure the feemarket has time to update
+		currentBlock, err := s.Chain.Height(s.GetContext())
+		s.Require().NoError(err)
+		s.Require().Equal(prevBlock+1, currentBlock)
+		prevBlock = currentBlock
+
+		gasResult, err := s.Chain.QueryJSON(s.GetContext(), "price.amount", "feemarket", "gas-price", s.Chain.Config().Denom)
+		s.Require().NoError(err)
+		gasStr = gasResult.String()
+		gasNow, err = strconv.ParseFloat(gasStr, 64)
+		s.Require().NoError(err)
+		s.Require().Greater(gasNow, gasBefore)
+		gasBefore = gasNow
+	}
+}
+
+func (s *FeemarketSuite) setCommitTimeout(timeout time.Duration) {
+	eg := errgroup.Group{}
+	for _, val := range s.Chain.Validators {
+		val := val
+		eg.Go(func() error {
+			configToml := make(testutil.Toml)
+			consensusToml := make(testutil.Toml)
+			consensusToml["timeout_commit"] = timeout.String()
+			configToml["consensus"] = consensusToml
+			if err := testutil.ModifyTomlConfigFile(
+				s.GetContext(), chainsuite.GetLogger(s.GetContext()),
+				val.DockerClient, s.T().Name(), val.VolumeName,
+				"config/config.toml", configToml,
+			); err != nil {
+				return err
+			}
+			if err := val.StopContainer(s.GetContext()); err != nil {
+				return err
+			}
+			return val.StartContainer(s.GetContext())
+		})
+	}
+	s.Require().NoError(eg.Wait())
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 1, s.Chain))
+}
+
+func TestFeemarket(t *testing.T) {
+	s := &FeemarketSuite{
+		Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{
+			UpgradeOnSetup: true,
+		}),
+	}
+	suite.Run(t, s)
+}
diff --git a/tests/interchain/go.mod b/tests/interchain/go.mod
index b5355dc503d..d116fea5d8c 100644
--- a/tests/interchain/go.mod
+++ b/tests/interchain/go.mod
@@ -23,6 +23,7 @@ require (
 	cosmossdk.io/math v1.3.0
 	github.com/cometbft/cometbft v0.38.11
 	github.com/cosmos/cosmos-sdk v0.50.9
+	github.com/cosmos/gogoproto v1.7.0
 	github.com/cosmos/ibc-go/v8 v8.5.0
 	github.com/cosmos/interchain-security/v5 v5.1.1
 	github.com/docker/docker v27.1.2+incompatible
@@ -95,7 +96,6 @@ require (
 	github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
 	github.com/cosmos/go-bip39 v1.0.0 // indirect
 	github.com/cosmos/gogogateway v1.2.0 // indirect
-	github.com/cosmos/gogoproto v1.7.0 // indirect
 	github.com/cosmos/iavl v1.3.0 // indirect
 	github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect
 	github.com/cosmos/ics23/go v0.11.0 // indirect
@@ -292,4 +292,4 @@ require (
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
 
-replace github.com/strangelove-ventures/interchaintest/v8 => github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d
+replace github.com/strangelove-ventures/interchaintest/v8 => github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c
diff --git a/tests/interchain/go.sum b/tests/interchain/go.sum
index 827bcf254c0..b2d2be54de8 100644
--- a/tests/interchain/go.sum
+++ b/tests/interchain/go.sum
@@ -773,8 +773,8 @@ github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXM
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
 github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
 github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
-github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d h1:3LXY5EWY78Qxh2t4h2rtcm/XpJdryN7bML2Nb0VfUjc=
-github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20240904201357-3a54d751e08d/go.mod h1:/4eZW5g+Gm5E7fCJvNVyjSlEyFkAfMzap4i8E6iqyyU=
+github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c h1:zTsxQIsnbocbpjqA6yEpM7nYYrqlF7EjWWFgHUEQtCE=
+github.com/hyphacoop/interchaintest/v8 v8.2.1-0.20241007153747-ed0a63d6cc1c/go.mod h1:/4eZW5g+Gm5E7fCJvNVyjSlEyFkAfMzap4i8E6iqyyU=
 github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
 github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
diff --git a/tests/interchain/ica_controller_test.go b/tests/interchain/ica_controller_test.go
new file mode 100644
index 00000000000..3e972bfcd4e
--- /dev/null
+++ b/tests/interchain/ica_controller_test.go
@@ -0,0 +1,148 @@
+package interchain_test
+
+import (
+	"context"
+	"encoding/json"
+	"strings"
+	"testing"
+
+	sdkmath "cosmossdk.io/math"
+	"github.com/cosmos/cosmos-sdk/codec"
+	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
+	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
+	"github.com/cosmos/gogoproto/proto"
+	icatypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/types"
+	"github.com/strangelove-ventures/interchaintest/v8"
+	"github.com/strangelove-ventures/interchaintest/v8/ibc"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+)
+
+type ICAControllerSuite struct {
+	*chainsuite.Suite
+	Host *chainsuite.Chain
+}
+
+func (s *ICAControllerSuite) SetupSuite() {
+	s.Suite.SetupSuite()
+	host, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env))
+	s.Require().NoError(err)
+	s.Host = host
+}
+
+func (s *ICAControllerSuite) TestICAController() {
+	const amountToSend = int64(3_300_000_000)
+	wallets := s.Chain.ValidatorWallets
+	valIdx := 0
+
+	var icaAddress, srcAddress string
+	var err error
+	for ; valIdx < len(wallets); valIdx++ {
+		srcAddress = wallets[valIdx].Address
+		icaAddress, err = s.Chain.SetupICAAccount(s.GetContext(), s.Host, s.Relayer, srcAddress, valIdx, amountToSend)
+		if err == nil {
+			break
+		} else if strings.Contains(err.Error(), "active channel already set for this owner") {
+			chainsuite.GetLogger(s.GetContext()).Sugar().Warnf("error setting up ICA account: %s", err)
+			valIdx++
+			continue
+		}
+		// if we get here, fail the test. Unexpected error.
+		s.Require().NoError(err)
+	}
+	if icaAddress == "" {
+		// this'll happen if every validator has an ICA account already
+		s.Require().Fail("unable to create ICA account")
+	}
+
+	srcChannel, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, s.Host)
+	s.Require().NoError(err)
+
+	_, err = s.Chain.SendIBCTransfer(s.GetContext(), srcChannel.ChannelID, interchaintest.FaucetAccountKeyName, ibc.WalletAmount{
+		Address: icaAddress,
+		Amount:  sdkmath.NewInt(amountToSend),
+		Denom:   s.Chain.Config().Denom,
+	}, ibc.TransferOptions{})
+	s.Require().NoError(err)
+
+	wallets = s.Host.ValidatorWallets
+	s.Require().NoError(err)
+	dstAddress := wallets[0].Address
+
+	var ibcStakeDenom string
+	s.Require().EventuallyWithT(func(c *assert.CollectT) {
+		balances, err := s.Host.BankQueryAllBalances(s.GetContext(), icaAddress)
+		s.Require().NoError(err)
+		s.Require().NotEmpty(balances)
+		for _, c := range balances {
+			if strings.Contains(c.Denom, "ibc") {
+				ibcStakeDenom = c.Denom
+				break
+			}
+		}
+		assert.NotEmpty(c, ibcStakeDenom)
+	}, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout)
+
+	recipientBalanceBefore, err := s.Host.GetBalance(s.GetContext(), dstAddress, ibcStakeDenom)
+	s.Require().NoError(err)
+
+	icaAmount := int64(amountToSend / 3)
+
+	srcConnection := srcChannel.ConnectionHops[0]
+
+	s.Require().NoError(s.sendICATx(s.GetContext(), valIdx, srcAddress, dstAddress, icaAddress, srcConnection, icaAmount, ibcStakeDenom))
+
+	s.Require().EventuallyWithT(func(c *assert.CollectT) {
+		recipientBalanceAfter, err := s.Host.GetBalance(s.GetContext(), dstAddress, ibcStakeDenom)
+		assert.NoError(c, err)
+
+		assert.Equal(c, recipientBalanceBefore.Add(sdkmath.NewInt(icaAmount)), recipientBalanceAfter)
+	}, 10*chainsuite.CommitTimeout, chainsuite.CommitTimeout)
+
+}
+
+func TestICAController(t *testing.T) {
+	s := &ICAControllerSuite{Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{
+		UpgradeOnSetup: true,
+		CreateRelayer:  true,
+	})}
+	suite.Run(t, s)
+}
+
+func (s *ICAControllerSuite) sendICATx(ctx context.Context, valIdx int, srcAddress string, dstAddress string, icaAddress string, srcConnection string, amount int64, denom string) error {
+	interfaceRegistry := codectypes.NewInterfaceRegistry()
+	cdc := codec.NewProtoCodec(interfaceRegistry)
+
+	bankSendMsg := banktypes.NewMsgSend(
+		sdk.MustAccAddressFromBech32(icaAddress),
+		sdk.MustAccAddressFromBech32(dstAddress),
+		sdk.NewCoins(sdk.NewCoin(denom, sdkmath.NewInt(amount))),
+	)
+	data, err := icatypes.SerializeCosmosTx(cdc, []proto.Message{bankSendMsg}, icatypes.EncodingProtobuf)
+	if err != nil {
+		return err
+	}
+
+	msg, err := json.Marshal(icatypes.InterchainAccountPacketData{
+		Type: icatypes.EXECUTE_TX,
+		Data: data,
+	})
+	if err != nil {
+		return err
+	}
+	msgPath := "msg.json"
+	if err := s.Chain.Validators[valIdx].WriteFile(ctx, msg, msgPath); err != nil {
+		return err
+	}
+	msgPath = s.Chain.Validators[valIdx].HomeDir() + "/" + msgPath
+	_, err = s.Chain.Validators[valIdx].ExecTx(ctx, srcAddress,
+		"interchain-accounts", "controller", "send-tx",
+		srcConnection, msgPath,
+	)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/tests/interchain/lsm_test.go b/tests/interchain/lsm_test.go
new file mode 100644
index 00000000000..d99d5d3e482
--- /dev/null
+++ b/tests/interchain/lsm_test.go
@@ -0,0 +1,493 @@
+package interchain_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+	"testing"
+	"time"
+
+	sdkmath "cosmossdk.io/math"
+	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
+	"github.com/strangelove-ventures/interchaintest/v8"
+	"github.com/strangelove-ventures/interchaintest/v8/ibc"
+	"github.com/strangelove-ventures/interchaintest/v8/testutil"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+	"golang.org/x/mod/semver"
+	"golang.org/x/sync/errgroup"
+)
+
+const (
+	lsmBondingMoniker = "bonding"
+	lsmLiquid1Moniker = "liquid_1"
+	lsmLiquid2Moniker = "liquid_2"
+	lsmLiquid3Moniker = "liquid_3"
+	lsmOwnerMoniker   = "owner"
+)
+
+type LSMSuite struct {
+	*chainsuite.Suite
+	Stride      *chainsuite.Chain
+	ICAAddr     string
+	LSMWallets  map[string]ibc.Wallet
+	ShareFactor sdkmath.Int
+}
+
+func (s *LSMSuite) checkAMinusBEqualsX(a, b string, x sdkmath.Int) {
+	intA, err := chainsuite.StrToSDKInt(a)
+	s.Require().NoError(err)
+	intB, err := chainsuite.StrToSDKInt(b)
+	s.Require().NoError(err)
+	s.Require().True(intA.Sub(intB).Equal(x), "a - b = %s, expected %s", intA.Sub(intB).String(), x.String())
+}
+
+func (s *LSMSuite) TestLSMHappyPath() {
+	const (
+		delegation    = 100000000
+		tokenize      = 50000000
+		bankSend      = 20000000
+		ibcTransfer   = 10000000
+		liquid1Redeem = 20000000
+	)
+	providerWallet := s.Chain.ValidatorWallets[0]
+
+	strideWallet := s.Stride.ValidatorWallets[0]
+
+	s.Run("Validator Bond", func() {
+		delegatorShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		validatorBondShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(),
+			"staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+		delegatorShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		s.checkAMinusBEqualsX(delegatorShares2.String(), delegatorShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor))
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(),
+			"staking", "validator-bond", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		validatorBondShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		s.checkAMinusBEqualsX(validatorBondShares2.String(), validatorBondShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor))
+	})
+
+	var tokenizedDenom string
+	s.Run("Tokenize", func() {
+		delegatorShares1, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(),
+			"staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+		delegatorShares2, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		s.checkAMinusBEqualsX(delegatorShares2.String(), delegatorShares1.String(), sdkmath.NewInt(delegation).Mul(s.ShareFactor))
+
+		sharesPreTokenize, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(),
+			"staking", "tokenize-share",
+			providerWallet.ValoperAddress, fmt.Sprintf("%d%s", tokenize, s.Chain.Config().Denom), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(),
+			"--gas", "auto")
+		s.Require().NoError(err)
+		sharesPostTokenize, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		s.checkAMinusBEqualsX(sharesPostTokenize.String(), sharesPreTokenize.String(), sdkmath.NewInt(tokenize).Mul(s.ShareFactor))
+
+		balances, err := s.Chain.BankQueryAllBalances(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		for _, balance := range balances {
+			if balance.Amount.Int64() == tokenize {
+				tokenizedDenom = balance.Denom
+			}
+		}
+		s.Require().NotEmpty(tokenizedDenom)
+	})
+
+	s.Run("Transfer Ownership", func() {
+		recordIDResult, err := s.Chain.QueryJSON(s.GetContext(), "record.id", "staking", "tokenize-share-record-by-denom", tokenizedDenom)
+		s.Require().NoError(err)
+		recordID := recordIDResult.String()
+
+		ownerResult, err := s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom)
+		s.Require().NoError(err)
+		owner := ownerResult.String()
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), owner,
+			"staking", "transfer-tokenize-share-record", recordID, s.LSMWallets[lsmOwnerMoniker].FormattedAddress())
+		s.Require().NoError(err)
+
+		ownerResult, err = s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom)
+		s.Require().NoError(err)
+		owner = ownerResult.String()
+		s.Require().Equal(s.LSMWallets[lsmOwnerMoniker].FormattedAddress(), owner)
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), owner,
+			"staking", "transfer-tokenize-share-record", recordID, s.LSMWallets[lsmLiquid1Moniker].FormattedAddress())
+		s.Require().NoError(err)
+
+		ownerResult, err = s.Chain.QueryJSON(s.GetContext(), "record.owner", "staking", "tokenize-share-record-by-denom", tokenizedDenom)
+		s.Require().NoError(err)
+		owner = ownerResult.String()
+		s.Require().Equal(s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), owner)
+	})
+
+	var happyLiquid1Delegations1 string
+	var ibcDenom string
+
+	ibcChannelProvider, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, s.Stride)
+	s.Require().NoError(err)
+	ibcChannelStride, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Stride, s.Chain)
+	s.Require().NoError(err)
+
+	s.Run("Transfer Tokens", func() {
+		happyLiquid1Delegations1Result, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid1Delegations1 = happyLiquid1Delegations1Result.String()
+
+		err = s.Chain.SendFunds(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), ibc.WalletAmount{
+			Amount:  sdkmath.NewInt(bankSend),
+			Denom:   tokenizedDenom,
+			Address: s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(),
+		})
+		s.Require().NoError(err)
+
+		_, err = s.Chain.SendIBCTransfer(s.GetContext(), ibcChannelProvider.ChannelID, s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(), ibc.WalletAmount{
+			Amount:  sdkmath.NewInt(ibcTransfer),
+			Denom:   tokenizedDenom,
+			Address: strideWallet.Address,
+		}, ibc.TransferOptions{})
+		s.Require().NoError(err)
+		s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 5, s.Stride))
+		balances, err := s.Stride.BankQueryAllBalances(s.GetContext(), strideWallet.Address)
+		s.Require().NoError(err)
+		for _, balance := range balances {
+			if balance.Amount.Int64() == ibcTransfer {
+				ibcDenom = balance.Denom
+			}
+		}
+		s.Require().NotEmpty(ibcDenom)
+	})
+
+	var happyLiquid1DelegationBalance string
+	s.Run("Redeem Tokens", func() {
+		_, err := s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(),
+			"staking", "redeem-tokens", fmt.Sprintf("%d%s", liquid1Redeem, tokenizedDenom),
+			"--gas", "auto")
+		s.Require().NoError(err)
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(),
+			"staking", "redeem-tokens", fmt.Sprintf("%d%s", bankSend, tokenizedDenom),
+			"--gas", "auto")
+		s.Require().NoError(err)
+
+		_, err = s.Stride.SendIBCTransfer(s.GetContext(), ibcChannelStride.ChannelID, strideWallet.Address, ibc.WalletAmount{
+			Amount:  sdkmath.NewInt(ibcTransfer),
+			Denom:   ibcDenom,
+			Address: s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(),
+		}, ibc.TransferOptions{})
+		s.Require().NoError(err)
+		// wait for the transfer to be reflected
+		s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 5, s.Chain))
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(),
+			"staking", "redeem-tokens", fmt.Sprintf("%d%s", ibcTransfer, tokenizedDenom),
+			"--gas", "auto")
+		s.Require().NoError(err)
+
+		happyLiquid1Delegations2Result, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid1Delegations2 := happyLiquid1Delegations2Result.String()
+		s.checkAMinusBEqualsX(happyLiquid1Delegations2, happyLiquid1Delegations1, sdkmath.NewInt(liquid1Redeem).Mul(s.ShareFactor))
+
+		happyLiquid2DelegationsResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid2Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid2Delegations := happyLiquid2DelegationsResult.String()
+		// LOL there are better ways of doing this
+		s.checkAMinusBEqualsX(happyLiquid2Delegations, "0", sdkmath.NewInt(bankSend).Mul(s.ShareFactor))
+
+		happyLiquid3DelegationsResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").delegation.shares", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid3Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid3Delegations := happyLiquid3DelegationsResult.String()
+		s.checkAMinusBEqualsX(happyLiquid3Delegations, "0", sdkmath.NewInt(ibcTransfer).Mul(s.ShareFactor))
+
+		happyLiquid1DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid1Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid1DelegationBalance = happyLiquid1DelegationBalanceResult.String()
+
+		happyLiquid2DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid2Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid2DelegationBalance := happyLiquid2DelegationBalanceResult.String()
+
+		happyLiquid3DelegationBalanceResult, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("delegation_responses.#(delegation.validator_address==\"%s\").balance.amount", providerWallet.ValoperAddress), "staking", "delegations", s.LSMWallets[lsmLiquid3Moniker].FormattedAddress())
+		s.Require().NoError(err)
+		happyLiquid3DelegationBalance := happyLiquid3DelegationBalanceResult.String()
+
+		s.checkAMinusBEqualsX(happyLiquid1DelegationBalance, "0", sdkmath.NewInt(70000000))
+		s.checkAMinusBEqualsX(happyLiquid2DelegationBalance, "0", sdkmath.NewInt(bankSend))
+		s.checkAMinusBEqualsX(happyLiquid3DelegationBalance, "0", sdkmath.NewInt(ibcTransfer))
+	})
+	s.Run("Cleanup", func() {
+		_, err := s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmBondingMoniker].FormattedAddress(),
+			"staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", delegation, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+
+		validatorBondSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		validatorBondShares := validatorBondSharesResult.String()
+		s.checkAMinusBEqualsX(validatorBondShares, "0", sdkmath.NewInt(0).Mul(s.ShareFactor))
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid1Moniker].FormattedAddress(),
+			"staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%s%s", happyLiquid1DelegationBalance, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid2Moniker].FormattedAddress(),
+			"staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", bankSend, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), s.LSMWallets[lsmLiquid3Moniker].FormattedAddress(),
+			"staking", "unbond", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", ibcTransfer, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+	})
+}
+
+func (s *LSMSuite) TestICADelegate() {
+	const (
+		delegate       = 20000000
+		bondDelegation = 20000000
+	)
+	bondingWallet, err := s.Chain.BuildWallet(s.GetContext(), fmt.Sprintf("lsm_happy_bonding_%d", time.Now().Unix()), "")
+	s.Require().NoError(err)
+
+	err = s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{
+		Amount:  sdkmath.NewInt(50_000_000),
+		Denom:   s.Chain.Config().Denom,
+		Address: bondingWallet.FormattedAddress(),
+	})
+	s.Require().NoError(err)
+
+	providerWallet := s.Chain.ValidatorWallets[1]
+
+	strideWallet := s.Stride.ValidatorWallets[0]
+
+	s.Run("Delegate and Bond", func() {
+		shares1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		shares1 := shares1Result.String()
+
+		tokens1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		tokens1 := tokens1Result.String()
+
+		bondShares1Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		bondShares1 := bondShares1Result.String()
+
+		shares1Int, err := chainsuite.StrToSDKInt(shares1)
+		s.Require().NoError(err)
+		tokens1Int, err := chainsuite.StrToSDKInt(tokens1)
+		s.Require().NoError(err)
+		bondShares1Int, err := chainsuite.StrToSDKInt(bondShares1)
+		s.Require().NoError(err)
+
+		exchangeRate1 := shares1Int.Quo(tokens1Int)
+		expectedSharesIncrease := exchangeRate1.MulRaw(bondDelegation)
+		expectedShares := expectedSharesIncrease.Add(bondShares1Int)
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), bondingWallet.FormattedAddress(),
+			"staking", "delegate", providerWallet.ValoperAddress, fmt.Sprintf("%d%s", bondDelegation, s.Chain.Config().Denom))
+		s.Require().NoError(err)
+
+		_, err = s.Chain.GetNode().ExecTx(s.GetContext(), bondingWallet.FormattedAddress(),
+			"staking", "validator-bond", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+
+		bondShares2Result, err := s.Chain.QueryJSON(s.GetContext(), "validator.validator_bond_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		bondShares2 := bondShares2Result.String()
+
+		bondShares2Int, err := chainsuite.StrToSDKInt(bondShares2)
+		s.Require().NoError(err)
+		s.Require().Truef(bondShares2Int.Sub(expectedShares).Abs().LTE(sdkmath.NewInt(1)), "bondShares2: %s, expectedShares: %s", bondShares2, expectedShares)
+	})
+
+	s.Run("Delegate via ICA", func() {
+		preDelegationTokensResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		preDelegationTokens := preDelegationTokensResult.String()
+
+		preDelegationSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.delegator_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		preDelegationShares := preDelegationSharesResult.String()
+
+		preDelegationLiquidSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		preDelegationLiquidShares := preDelegationLiquidSharesResult.String()
+
+		preDelegationTokensInt, err := chainsuite.StrToSDKInt(preDelegationTokens)
+		s.Require().NoError(err)
+		preDelegationSharesInt, err := chainsuite.StrToSDKInt(preDelegationShares)
+		s.Require().NoError(err)
+		exchangeRate := preDelegationSharesInt.Quo(preDelegationTokensInt)
+		expectedLiquidIncrease := exchangeRate.MulRaw(delegate)
+
+		delegateHappy := map[string]interface{}{
+			"@type":             "/cosmos.staking.v1beta1.MsgDelegate",
+			"delegator_address": s.ICAAddr,
+			"validator_address": providerWallet.ValoperAddress,
+			"amount": map[string]interface{}{
+				"denom":  s.Chain.Config().Denom,
+				"amount": fmt.Sprint(delegate),
+			},
+		}
+		delegateHappyJSON, err := json.Marshal(delegateHappy)
+		s.Require().NoError(err)
+		jsonPath := "delegate-happy.json"
+		fullJsonPath := path.Join(s.Stride.Validators[0].HomeDir(), jsonPath)
+		stdout, _, err := s.Stride.GetNode().ExecBin(s.GetContext(), "tx", "interchain-accounts", "host", "generate-packet-data", string(delegateHappyJSON), "--encoding", "proto3")
+		s.Require().NoError(err)
+		s.Require().NoError(s.Stride.Validators[0].WriteFile(s.GetContext(), []byte(stdout), jsonPath))
+		ibcChannelStride, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Stride, s.Chain)
+		s.Require().NoError(err)
+
+		_, err = s.Stride.GetNode().ExecTx(s.GetContext(), strideWallet.Address,
+			"interchain-accounts", "controller", "send-tx", ibcChannelStride.ConnectionHops[0], fullJsonPath)
+		s.Require().NoError(err)
+
+		var tokensDelta sdkmath.Int
+		s.Require().EventuallyWithT(func(c *assert.CollectT) {
+			postDelegationTokensResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.tokens", "staking", "validator", providerWallet.ValoperAddress)
+			s.Require().NoError(err)
+			postDelegationTokens, err := chainsuite.StrToSDKInt(postDelegationTokensResult.String())
+			s.Require().NoError(err)
+			tokensDelta = postDelegationTokens.Sub(preDelegationTokensInt)
+			assert.Truef(c, tokensDelta.Sub(sdkmath.NewInt(delegate)).Abs().LTE(sdkmath.NewInt(1)), "tokensDelta: %s, delegate: %d", tokensDelta, delegate)
+		}, 20*time.Second, time.Second)
+
+		postDelegationLiquidSharesResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", providerWallet.ValoperAddress)
+		s.Require().NoError(err)
+		postDelegationLiquidShares, err := chainsuite.StrToSDKInt(postDelegationLiquidSharesResult.String())
+		s.Require().NoError(err)
+		preDelegationLiquidSharesInt, err := chainsuite.StrToSDKInt(preDelegationLiquidShares)
+		s.Require().NoError(err)
+		liquidSharesDelta := postDelegationLiquidShares.Sub(preDelegationLiquidSharesInt)
+		s.Require().Truef(liquidSharesDelta.Sub(expectedLiquidIncrease).Abs().LTE(sdkmath.NewInt(1)), "liquidSharesDelta: %s, expectedLiquidIncrease: %d", liquidSharesDelta, expectedLiquidIncrease)
+	})
+}
+
+func (s *LSMSuite) TestTokenizeVested() {
+	const amount = 100_000_000_000
+	const vestingPeriod = 100 * time.Second
+	vestedByTimestamp := time.Now().Add(vestingPeriod).Unix()
+	vestingAccount, err := s.Chain.BuildWallet(s.GetContext(), fmt.Sprintf("vesting-%d", vestedByTimestamp), "")
+	s.Require().NoError(err)
+	validatorWallet := s.Chain.ValidatorWallets[0]
+
+	_, err = s.Chain.GetNode().ExecTx(s.GetContext(), interchaintest.FaucetAccountKeyName,
+		"vesting", "create-vesting-account", vestingAccount.FormattedAddress(),
+		fmt.Sprintf("%d%s", amount, s.Chain.Config().Denom),
+		fmt.Sprintf("%d", vestedByTimestamp))
+	s.Require().NoError(err)
+
+	// give the vesting account a little cash for gas fees
+	err = s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{
+		Amount:  sdkmath.NewInt(5_000),
+		Denom:   s.Chain.Config().Denom,
+		Address: vestingAccount.FormattedAddress(),
+	})
+	s.Require().NoError(err)
+
+	vestingAmount := int64(amount - 5000)
+	// delegate the vesting account to the validator
+	_, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(),
+		"staking", "delegate", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", vestingAmount, s.Chain.Config().Denom))
+	s.Require().NoError(err)
+
+	// wait for half the vesting period
+	time.Sleep(vestingPeriod / 2)
+
+	// try to tokenize full amount. Should fail.
+	_, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(),
+		"staking", "tokenize-share", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", vestingAmount, s.Chain.Config().Denom), vestingAccount.FormattedAddress(),
+		"--gas", "auto")
+	s.Require().Error(err)
+
+	sharesPreTokenizeResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", validatorWallet.ValoperAddress)
+	s.Require().NoError(err)
+	sharesPreTokenize := sharesPreTokenizeResult.String()
+
+	// try to tokenize vested amount (i.e. half) should succeed if upgraded
+	tokenizeAmount := vestingAmount / 2
+	_, err = s.Chain.GetNode().ExecTx(s.GetContext(), vestingAccount.FormattedAddress(),
+		"staking", "tokenize-share", validatorWallet.ValoperAddress, fmt.Sprintf("%d%s", tokenizeAmount, s.Chain.Config().Denom), vestingAccount.FormattedAddress(),
+		"--gas", "auto")
+	s.Require().NoError(err)
+	sharesPostTokenizeResult, err := s.Chain.QueryJSON(s.GetContext(), "validator.liquid_shares", "staking", "validator", validatorWallet.ValoperAddress)
+	s.Require().NoError(err)
+	sharesPostTokenize := sharesPostTokenizeResult.String()
+	s.checkAMinusBEqualsX(sharesPostTokenize, sharesPreTokenize, sdkmath.NewInt(tokenizeAmount).Mul(s.ShareFactor))
+}
+
+func (s *LSMSuite) setupLSMWallets() {
+	names := []string{lsmBondingMoniker, lsmLiquid1Moniker, lsmLiquid2Moniker, lsmLiquid3Moniker, lsmOwnerMoniker}
+	wallets := make(map[string]ibc.Wallet)
+	eg := new(errgroup.Group)
+	for _, name := range names {
+		keyName := "happy_" + name
+		wallet, err := s.Chain.BuildWallet(s.GetContext(), keyName, "")
+		s.Require().NoError(err)
+		wallets[name] = wallet
+		amount := 500_000_000
+		if name == "owner" {
+			amount = 10_000_000
+		}
+		eg.Go(func() error {
+			return s.Chain.SendFunds(s.GetContext(), interchaintest.FaucetAccountKeyName, ibc.WalletAmount{
+				Amount:  sdkmath.NewInt(int64(amount)),
+				Denom:   s.Chain.Config().Denom,
+				Address: wallet.FormattedAddress(),
+			})
+		})
+	}
+	s.Require().NoError(eg.Wait())
+	s.LSMWallets = wallets
+}
+
+func (s *LSMSuite) SetupSuite() {
+	s.Suite.SetupSuite()
+	// This is slightly broken while stride is still in the process of being upgraded, so skip if
+	// going from v21 -> v21
+	if semver.Major(s.Env.OldGaiaImageVersion) == s.Env.UpgradeName && s.Env.UpgradeName == "v21" {
+		s.T().Skip("Skipping LSM when going from v21 -> v21")
+	}
+	stride, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, chainsuite.ConsumerConfig{
+		ChainName: "stride",
+		Version:   chainsuite.StrideVersion,
+		Denom:     chainsuite.StrideDenom,
+		TopN:      100,
+	})
+	s.Require().NoError(err)
+	s.Stride = stride
+	err = s.Chain.CheckCCV(s.GetContext(), stride, s.Relayer, 1_000_000, 0, 1)
+	s.Require().NoError(err)
+
+	icaAddr, err := stride.SetupICAAccount(s.GetContext(), s.Chain, s.Relayer, stride.ValidatorWallets[0].Address, 0, 1_000_000_000)
+	s.Require().NoError(err)
+	s.ICAAddr = icaAddr
+	shareFactor, ok := sdkmath.NewIntFromString("1000000000000000000")
+	s.Require().True(ok)
+	s.ShareFactor = shareFactor
+
+	s.setupLSMWallets()
+	s.UpgradeChain()
+}
+
+func TestLSM(t *testing.T) {
+	s := &LSMSuite{
+		Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{
+			CreateRelayer: true,
+		}),
+	}
+	suite.Run(t, s)
+}
diff --git a/tests/interchain/matrix_tool/main.go b/tests/interchain/matrix_tool/main.go
index 244a691c396..92172ba3f50 100644
--- a/tests/interchain/matrix_tool/main.go
+++ b/tests/interchain/matrix_tool/main.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"math/rand/v2"
 	"os"
 	"os/exec"
 	"path"
@@ -105,6 +106,9 @@ func GetTestList() ([]string, error) {
 			retval = append(retval, line)
 		}
 	}
+	rand.Shuffle(len(retval), func(i, j int) {
+		retval[i], retval[j] = retval[j], retval[i]
+	})
 	return retval, nil
 }
 
diff --git a/tests/interchain/permissionless_test.go b/tests/interchain/permissionless_test.go
index 948177436d2..18ed527cab6 100644
--- a/tests/interchain/permissionless_test.go
+++ b/tests/interchain/permissionless_test.go
@@ -7,11 +7,13 @@ import (
 	"path"
 	"path/filepath"
 	"strconv"
+	"strings"
 	"testing"
 	"time"
 
 	sdkmath "cosmossdk.io/math"
-	govtypes "github.cogaia/v21/cosmos-sdk/x/gov/types/v1"
+	"github.com/cosmos/cosmos-sdk/types"
+	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
 	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
 	transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
 	ccvclient "github.com/cosmos/interchain-security/v5/x/ccv/provider/client"
@@ -21,6 +23,7 @@ import (
 	"github.com/strangelove-ventures/interchaintest/v8/ibc"
 	"github.com/strangelove-ventures/interchaintest/v8/testutil"
 	"github.com/stretchr/testify/suite"
+	"github.com/tidwall/gjson"
 	"github.com/tidwall/sjson"
 	"golang.org/x/mod/semver"
 	"golang.org/x/sync/errgroup"
@@ -352,7 +355,7 @@ func (s *PermissionlessConsumersSuite) TestChangePowerShaping() {
 
 	s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer, s.Relayer, 1_000_000, 0, 1))
 
-	vals, err := consumer.QueryJSON(s.GetContext(), "validators", "comet-validator-set")
+	vals, err := consumer.QueryJSON(s.GetContext(), "validators", "tendermint-validator-set")
 	s.Require().NoError(err)
 	s.Require().Equal(newValidatorCount, len(vals.Array()), vals)
 	for i := 0; i < newValidatorCount; i++ {
@@ -373,10 +376,28 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() {
 		_, err = s.Chain.Validators[0].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, "provider", "opt-in", consumerID)
 		s.Require().NoError(err)
 	}
+
+	images := []ibc.DockerImage{
+		{
+			Repository: "ghcr.io/hyphacoop/ics",
+			Version:    "v4.5.0",
+			UidGid:     "1025:1025",
+		},
+	}
+	chainID := fmt.Sprintf("%s-test-%d", cfg.ChainName, len(s.Chain.Consumers)+1)
+	spawnTime := time.Now().Add(chainsuite.ChainSpawnWait)
+	cfg.Spec = s.Chain.DefaultConsumerChainSpec(s.GetContext(), chainID, cfg, spawnTime, nil)
+	cfg.Spec.Version = "v4.5.0"
+	cfg.Spec.Images = images
 	consumer1, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
 	s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer1, s.Relayer, 1_000_000, 0, 1))
 
+	chainID = fmt.Sprintf("%s-test-%d", cfg.ChainName, len(s.Chain.Consumers)+1)
+	spawnTime = time.Now().Add(chainsuite.ChainSpawnWait)
+	cfg.Spec = s.Chain.DefaultConsumerChainSpec(s.GetContext(), chainID, cfg, spawnTime, nil)
+	cfg.Spec.Version = "v4.5.0"
+	cfg.Spec.Images = images
 	consumer2, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
 	s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer2, s.Relayer, 1_000_000, 0, 1))
@@ -448,7 +469,9 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() {
 	})
 	s.Require().NoError(eg.Wait())
 
-	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+3, s.Chain, consumer1, consumer2))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer1, consumer2))
+	s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer1))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer1, consumer2))
 
 	rewardStr, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", denom1), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address)
 	s.Require().NoError(err)
@@ -491,7 +514,9 @@ func (s *PermissionlessConsumersSuite) TestConsumerCommissionRate() {
 	})
 	s.Require().NoError(eg.Wait())
 
-	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+3, s.Chain, consumer1, consumer2))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer1, consumer2))
+	s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer1))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer1, consumer2))
 
 	rewardStr, err = s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", denom1), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address)
 	s.Require().NoError(err)
@@ -570,6 +595,87 @@ func (s *PermissionlessConsumersSuite) TestLaunchWithAllowListThenModify() {
 	s.Require().Equal(4, len(validators.Array()))
 }
 
+func (s *PermissionlessConsumersSuite) TestRewardsWithChangeover() {
+	validators := 1
+	fullNodes := 0
+	genesisChanges := []cosmos.GenesisKV{
+		cosmos.NewGenesisKV("app_state.gov.params.voting_period", chainsuite.GovVotingPeriod.String()),
+		cosmos.NewGenesisKV("app_state.gov.params.max_deposit_period", chainsuite.GovDepositPeriod.String()),
+		cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.denom", chainsuite.Ucon),
+		cosmos.NewGenesisKV("app_state.gov.params.min_deposit.0.amount", strconv.Itoa(chainsuite.GovMinDepositAmount)),
+	}
+	spec := &interchaintest.ChainSpec{
+		Name:      "ics-consumer",
+		ChainName: "ics-consumer",
+		// Unfortunately, this rc is a bit of a bespoke version; it corresponds to an rc
+		// in hypha's fork that has a fix for sovereign -> consumer changeovers
+		Version:       "v6.2.0-rc1",
+		NumValidators: &validators,
+		NumFullNodes:  &fullNodes,
+		ChainConfig: ibc.ChainConfig{
+			Denom:         chainsuite.Ucon,
+			GasPrices:     "0.025" + chainsuite.Ucon,
+			GasAdjustment: 2.0,
+			Gas:           "auto",
+			ConfigFileOverrides: map[string]any{
+				"config/config.toml": chainsuite.DefaultConfigToml(),
+			},
+			ModifyGenesisAmounts: chainsuite.DefaultGenesisAmounts(chainsuite.Ucon),
+			ModifyGenesis:        cosmos.ModifyGenesis(genesisChanges),
+			Bin:                  "interchain-security-sd",
+			Images: []ibc.DockerImage{
+				{
+					Repository: chainsuite.HyphaICSRepo,
+					Version:    "v6.2.0-rc1",
+					UidGid:     chainsuite.ICSUidGuid,
+				},
+			},
+			Bech32Prefix: "consumer",
+		},
+	}
+	consumer, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, spec)
+	s.Require().NoError(err)
+
+	transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, consumer)
+	s.Require().NoError(err)
+	rewardDenom := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", transferCh.ChannelID, consumer.Config().Denom)).IBCDenom()
+
+	s.UpgradeChain()
+
+	s.changeSovereignToConsumer(consumer, transferCh)
+
+	govAuthority, err := s.Chain.GetGovernanceAddress(s.GetContext())
+	s.Require().NoError(err)
+	rewardDenomsProp := providertypes.MsgChangeRewardDenoms{
+		DenomsToAdd: []string{rewardDenom},
+		Authority:   govAuthority,
+	}
+	prop, err := s.Chain.BuildProposal([]cosmos.ProtoMessage{&rewardDenomsProp},
+		"add denoms to list of registered reward denoms",
+		"add denoms to list of registered reward denoms",
+		"", chainsuite.GovDepositAmount, "", false)
+	s.Require().NoError(err)
+	propResult, err := s.Chain.SubmitProposal(s.GetContext(), s.Chain.ValidatorWallets[0].Moniker, prop)
+	s.Require().NoError(err)
+	s.Require().NoError(s.Chain.PassProposal(s.GetContext(), propResult.ProposalID))
+
+	faucetAddrBts, err := consumer.GetAddress(s.GetContext(), interchaintest.FaucetAccountKeyName)
+	s.Require().NoError(err)
+	faucetAddr := types.MustBech32ifyAddressBytes(consumer.Config().Bech32Prefix, faucetAddrBts)
+	_, err = consumer.Validators[0].ExecTx(s.GetContext(), interchaintest.FaucetAccountKeyName, "bank", "send", string(faucetAddr), consumer.ValidatorWallets[0].Address, "1"+consumer.Config().Denom, "--fees", "100000000"+consumer.Config().Denom)
+	s.Require().NoError(err)
+
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), chainsuite.BlocksPerDistribution+2, s.Chain, consumer))
+	s.Require().NoError(s.Relayer.ClearTransferChannel(s.GetContext(), s.Chain, consumer))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain, consumer))
+
+	rewardStr, err := s.Chain.QueryJSON(s.GetContext(), fmt.Sprintf("total.#(%%\"*%s\")", rewardDenom), "distribution", "rewards", s.Chain.ValidatorWallets[0].Address)
+	s.Require().NoError(err)
+	rewards, err := chainsuite.StrToSDKInt(rewardStr.String())
+	s.Require().NoError(err)
+	s.Require().True(rewards.GT(sdkmath.NewInt(0)), "rewards: %s", rewards.String())
+}
+
 func TestPermissionlessConsumers(t *testing.T) {
 	genesis := chainsuite.DefaultGenesis()
 	genesis = append(genesis,
@@ -587,17 +693,123 @@ func TestPermissionlessConsumers(t *testing.T) {
 		}),
 		consumerCfg: chainsuite.ConsumerConfig{
 			ChainName:             "ics-consumer",
-			Version:               "v5.0.0",
+			Version:               "v4.5.0",
 			ShouldCopyProviderKey: allProviderKeysCopied(),
 			Denom:                 chainsuite.Ucon,
 			TopN:                  100,
 			AllowInactiveVals:     true,
 			MinStake:              1_000_000,
+			Spec: &interchaintest.ChainSpec{
+				ChainConfig: ibc.ChainConfig{
+					Images: []ibc.DockerImage{
+						{
+							Repository: chainsuite.HyphaICSRepo,
+							Version:    "v4.5.0",
+							UidGid:     chainsuite.ICSUidGuid,
+						},
+					},
+				},
+			},
 		},
 	}
 	suite.Run(t, s)
 }
 
+func (s *PermissionlessConsumersSuite) changeSovereignToConsumer(consumer *chainsuite.Chain, transferCh *ibc.ChannelOutput) {
+	cfg := s.consumerCfg
+	cfg.TopN = 0
+	currentHeight, err := consumer.Height(s.GetContext())
+	s.Require().NoError(err)
+	initialHeight := uint64(currentHeight) + 60
+	cfg.InitialHeight = initialHeight
+	spawnTime := time.Now().Add(60 * time.Second)
+	cfg.DistributionTransmissionChannel = transferCh.ChannelID
+
+	err = s.Chain.CreateConsumerPermissionless(s.GetContext(), consumer.Config().ChainID, cfg, spawnTime)
+	s.Require().NoError(err)
+
+	consumerChains, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "list-consumer-chains")
+	s.Require().NoError(err)
+	consumerChain := gjson.GetBytes(consumerChains, fmt.Sprintf("chains.#(chain_id=%q)", consumer.Config().ChainID))
+	consumerID := consumerChain.Get("consumer_id").String()
+
+	eg := errgroup.Group{}
+	for i := range consumer.Validators {
+		i := i
+		eg.Go(func() error {
+			key, _, err := consumer.Validators[i].ExecBin(s.GetContext(), "tendermint", "show-validator")
+			if err != nil {
+				return err
+			}
+			keyStr := strings.TrimSpace(string(key))
+			_, err = s.Chain.Validators[i].ExecTx(s.GetContext(), s.Chain.ValidatorWallets[i].Moniker, "provider", "opt-in", consumerID, keyStr)
+			return err
+		})
+	}
+	s.Require().NoError(eg.Wait())
+
+	s.Require().NoError(err)
+	time.Sleep(time.Until(spawnTime))
+	s.Require().NoError(testutil.WaitForBlocks(s.GetContext(), 2, s.Chain))
+
+	proposal := cosmos.SoftwareUpgradeProposal{
+		Deposit:     "5000000" + chainsuite.Ucon,
+		Title:       "Changeover",
+		Name:        "sovereign-changeover",
+		Description: "Changeover",
+		Height:      int64(initialHeight) - 3,
+	}
+	upgradeTx, err := consumer.UpgradeProposal(s.GetContext(), interchaintest.FaucetAccountKeyName, proposal)
+	s.Require().NoError(err)
+	err = consumer.PassProposal(s.GetContext(), upgradeTx.ProposalID)
+	s.Require().NoError(err)
+
+	currentHeight, err = consumer.Height(s.GetContext())
+	s.Require().NoError(err)
+
+	timeoutCtx, timeoutCtxCancel := context.WithTimeout(s.GetContext(), (time.Duration(int64(initialHeight)-currentHeight)+10)*chainsuite.CommitTimeout)
+	defer timeoutCtxCancel()
+	err = testutil.WaitForBlocks(timeoutCtx, int(int64(initialHeight)-currentHeight)+3, consumer)
+	s.Require().Error(err)
+
+	s.Require().NoError(consumer.StopAllNodes(s.GetContext()))
+
+	genesis, err := consumer.GetNode().GenesisFileContent(s.GetContext())
+	s.Require().NoError(err)
+
+	ccvState, _, err := s.Chain.GetNode().ExecQuery(s.GetContext(), "provider", "consumer-genesis", consumerID)
+	s.Require().NoError(err)
+	genesis, err = sjson.SetRawBytes(genesis, "app_state.ccvconsumer", ccvState)
+	s.Require().NoError(err)
+
+	genesis, err = sjson.SetBytes(genesis, "app_state.slashing.params.signed_blocks_window", strconv.Itoa(chainsuite.SlashingWindowConsumer))
+	s.Require().NoError(err)
+	genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.reward_denoms", []string{chainsuite.Ucon})
+	s.Require().NoError(err)
+	genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.provider_reward_denoms", []string{s.Chain.Config().Denom})
+	s.Require().NoError(err)
+	genesis, err = sjson.SetBytes(genesis, "app_state.ccvconsumer.params.blocks_per_distribution_transmission", chainsuite.BlocksPerDistribution)
+	s.Require().NoError(err)
+
+	for _, val := range consumer.Validators {
+		val := val
+		eg.Go(func() error {
+			if err := val.OverwriteGenesisFile(s.GetContext(), []byte(genesis)); err != nil {
+				return err
+			}
+			return val.WriteFile(s.GetContext(), []byte(genesis), ".sovereign/config/genesis.json")
+		})
+	}
+	s.Require().NoError(eg.Wait())
+
+	consumer.ChangeBinary(s.GetContext(), "interchain-security-cdd")
+	s.Require().NoError(consumer.StartAllNodes(s.GetContext()))
+	s.Require().NoError(s.Relayer.ConnectProviderConsumer(s.GetContext(), s.Chain, consumer))
+	s.Require().NoError(s.Relayer.StopRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext())))
+	s.Require().NoError(s.Relayer.StartRelayer(s.GetContext(), chainsuite.GetRelayerExecReporter(s.GetContext())))
+	s.Require().NoError(s.Chain.CheckCCV(s.GetContext(), consumer, s.Relayer, 1_000_000, 0, 1))
+}
+
 func (s *PermissionlessConsumersSuite) submitChangeRewardDenoms(consumer *chainsuite.Chain) (string, string) {
 	consumerCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chain, consumer)
 	s.Require().NoError(err)
diff --git a/tests/interchain/pfm_test.go b/tests/interchain/pfm_test.go
new file mode 100644
index 00000000000..867102bb0b0
--- /dev/null
+++ b/tests/interchain/pfm_test.go
@@ -0,0 +1,133 @@
+package interchain_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/cosmos/gaia/v21/tests/interchain/chainsuite"
+	transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
+	"github.com/strangelove-ventures/interchaintest/v8/ibc"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/suite"
+)
+
+type PFMSuite struct {
+	*chainsuite.Suite
+	Chains []*chainsuite.Chain
+}
+
+func (s *PFMSuite) SetupSuite() {
+	s.Suite.SetupSuite()
+	chainB, err := s.Chain.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env))
+	s.Require().NoError(err)
+	chainC, err := chainB.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env))
+	s.Require().NoError(err)
+	chainD, err := chainC.AddLinkedChain(s.GetContext(), s.T(), s.Relayer, chainsuite.DefaultChainSpec(s.Env))
+	s.Require().NoError(err)
+
+	s.Chains = []*chainsuite.Chain{s.Chain, chainB, chainC, chainD}
+}
+
+func (s *PFMSuite) TestPFMHappyPath() {
+	var forwardChannels []*ibc.ChannelOutput
+	targetDenomAD := s.Chains[0].Config().Denom
+	for i := 0; i < len(s.Chains)-1; i++ {
+		transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chains[i], s.Chains[i+1])
+		s.Require().NoError(err)
+		forwardChannels = append(forwardChannels, transferCh)
+		targetDenomAD = transfertypes.GetPrefixedDenom(transferCh.PortID, transferCh.Counterparty.ChannelID, targetDenomAD)
+	}
+	targetDenomAD = transfertypes.ParseDenomTrace(targetDenomAD).IBCDenom()
+
+	// backwardChannels[2] = chain3 -> chain2, backwardChannels[1] = chain2 -> chain1, backwardChannels[0] = chain1 -> chain0
+	backwardChannels := make([]*ibc.ChannelOutput, len(forwardChannels))
+	targetDenomDA := s.Chains[3].Config().Denom
+	for i := len(s.Chains) - 1; i > 0; i-- {
+		transferCh, err := s.Relayer.GetTransferChannel(s.GetContext(), s.Chains[i], s.Chains[i-1])
+		s.Require().NoError(err)
+		backwardChannels[i-1] = transferCh
+		targetDenomDA = transfertypes.GetPrefixedDenom(transferCh.PortID, transferCh.Counterparty.ChannelID, targetDenomDA)
+	}
+	targetDenomDA = transfertypes.ParseDenomTrace(targetDenomDA).IBCDenom()
+
+	dWallet1 := s.Chains[3].ValidatorWallets[0]
+
+	aWallet1 := s.Chains[0].ValidatorWallets[0]
+
+	dStartBalance, err := s.Chains[3].GetBalance(s.GetContext(), dWallet1.Address, targetDenomAD)
+	s.Require().NoError(err)
+
+	timeout := "10m"
+	memo := map[string]interface{}{
+		"forward": map[string]interface{}{
+			"receiver": "pfm",
+			"port":     "transfer",
+			"channel":  forwardChannels[1].ChannelID,
+			"timeout":  timeout,
+			"next": map[string]interface{}{
+				"forward": map[string]interface{}{
+					"receiver": dWallet1.Address,
+					"port":     "transfer",
+					"channel":  forwardChannels[2].ChannelID,
+					"timeout":  timeout,
+				},
+			},
+		},
+	}
+	memoBytes, err := json.Marshal(memo)
+	s.Require().NoError(err)
+	_, err = s.Chains[0].GetNode().ExecTx(s.GetContext(), aWallet1.Address,
+		"ibc-transfer", "transfer", "transfer", forwardChannels[0].ChannelID, "pfm", "1000000"+s.Chains[0].Config().Denom,
+		"--memo", string(memoBytes))
+	s.Require().NoError(err)
+
+	s.Require().EventuallyWithT(func(c *assert.CollectT) {
+		dEndBalance, err := s.Chains[3].GetBalance(s.GetContext(), dWallet1.Address, targetDenomAD)
+		assert.NoError(c, err)
+		assert.Truef(c, dEndBalance.Sub(dStartBalance).IsPositive(), "expected %d - %d > 0 (it was %d) in %s",
+			dEndBalance, dStartBalance, dEndBalance.Sub(dStartBalance), targetDenomAD)
+	}, 30*chainsuite.CommitTimeout, chainsuite.CommitTimeout, "chain D balance has not increased")
+
+	aStartBalance, err := s.Chains[0].GetBalance(s.GetContext(), aWallet1.Address, targetDenomDA)
+	s.Require().NoError(err)
+
+	memo = map[string]interface{}{
+		"forward": map[string]interface{}{
+			"receiver": "pfm",
+			"port":     "transfer",
+			"channel":  backwardChannels[1].ChannelID,
+			"timeout":  timeout,
+			"next": map[string]interface{}{
+				"forward": map[string]interface{}{
+					"receiver": aWallet1.Address,
+					"port":     "transfer",
+					"channel":  backwardChannels[0].ChannelID,
+					"timeout":  timeout,
+				},
+			},
+		},
+	}
+	memoBytes, err = json.Marshal(memo)
+	s.Require().NoError(err)
+	_, err = s.Chains[3].GetNode().ExecTx(s.GetContext(), dWallet1.Address,
+		"ibc-transfer", "transfer", "transfer", backwardChannels[2].ChannelID, "pfm", "1000000"+s.Chains[3].Config().Denom,
+		"--memo", string(memoBytes))
+	s.Require().NoError(err)
+
+	s.Require().EventuallyWithT(func(c *assert.CollectT) {
+		aEndBalance, err := s.Chains[0].GetBalance(s.GetContext(), aWallet1.Address, targetDenomDA)
+		assert.NoError(c, err)
+		assert.Truef(c, aEndBalance.Sub(aStartBalance).IsPositive(), "expected %d - %d > 0 (it was %d) in %s",
+			aEndBalance, aStartBalance, aEndBalance.Sub(aStartBalance), targetDenomDA)
+	}, 30*chainsuite.CommitTimeout, chainsuite.CommitTimeout, "chain A balance has not increased")
+
+}
+
+func TestPFM(t *testing.T) {
+	s := &PFMSuite{
+		Suite: chainsuite.NewSuite(chainsuite.SuiteConfig{
+			UpgradeOnSetup: true,
+			CreateRelayer:  true,
+		})}
+	suite.Run(t, s)
+}
diff --git a/tests/interchain/unbonding_test.go b/tests/interchain/unbonding_test.go
index 86c7656b333..105d5dbbb57 100644
--- a/tests/interchain/unbonding_test.go
+++ b/tests/interchain/unbonding_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/strangelove-ventures/interchaintest/v8/ibc"
 	"github.com/strangelove-ventures/interchaintest/v8/testutil"
 	"github.com/stretchr/testify/suite"
+	"golang.org/x/mod/semver"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 )
@@ -31,10 +32,21 @@ func (s *UnbondingSuite) SetupSuite() {
 	s.Suite.SetupSuite()
 	cfg := chainsuite.ConsumerConfig{
 		ChainName:             "ics-consumer",
-		Version:               "v5.0.0",
+		Version:               selectConsumerVersion("v6.0.0", "v6.2.1"),
 		ShouldCopyProviderKey: allProviderKeysCopied(),
 		Denom:                 chainsuite.Ucon,
 		TopN:                  100,
+		Spec: &interchaintest.ChainSpec{
+			ChainConfig: ibc.ChainConfig{
+				Images: []ibc.DockerImage{
+					{
+						Repository: chainsuite.HyphaICSRepo,
+						Version:    selectConsumerVersion("v6.0.0", "v6.2.1"),
+						UidGid:     chainsuite.ICSUidGuid,
+					},
+				},
+			},
+		},
 	}
 	consumer, err := s.Chain.AddConsumerChain(s.GetContext(), s.Relayer, cfg)
 	s.Require().NoError(err)
@@ -107,9 +119,14 @@ func (s *UnbondingSuite) TestCanLaunchAfterInitTimeout() {
 
 func TestUnbonding(t *testing.T) {
 	genesis := chainsuite.DefaultGenesis()
+	env := chainsuite.GetEnvironment()
+	if semver.Compare(env.OldGaiaImageVersion, "v20.0.0") < 0 {
+		genesis = append(genesis,
+			cosmos.NewGenesisKV("app_state.provider.params.vsc_timeout_period", vscTimeoutPeriod.String()),
+			cosmos.NewGenesisKV("app_state.provider.params.init_timeout_period", initTimeoutPeriod.String()),
+		)
+	}
 	genesis = append(genesis,
-		cosmos.NewGenesisKV("app_state.provider.params.vsc_timeout_period", vscTimeoutPeriod.String()),
-		cosmos.NewGenesisKV("app_state.provider.params.init_timeout_period", initTimeoutPeriod.String()),
 		cosmos.NewGenesisKV("app_state.staking.params.unbonding_time", unbondingTime.String()),
 	)
 	s := &UnbondingSuite{