diff --git a/app/ibctesting/chain.go b/app/ibctesting/chain.go index 8c6a6ecc5..00dab1490 100644 --- a/app/ibctesting/chain.go +++ b/app/ibctesting/chain.go @@ -2,7 +2,7 @@ package ibctesting import ( "context" - errorsmod "cosmossdk.io/errors" + "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" "fmt" @@ -50,16 +50,16 @@ import ( "github.com/cosmos/ibc-go/v8/modules/core/types" ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v8/testing" - "github.com/cosmos/ibc-go/v8/testing/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" cmtprotoversion "github.com/cometbft/cometbft/proto/tendermint/version" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" "github.com/notional-labs/composable/v6/app" ) +var MaxAccounts = 10 + type SenderAccount struct { SenderPrivKey cryptotypes.PrivKey SenderAccount sdk.AccountI @@ -141,29 +141,55 @@ type ChainAppFactory func(t *testing.T, valSet *cmttypes.ValidatorSet, genAccs [ // Each update of any chain increments the block header time for all chains by 5 seconds. func NewTestChain(t *testing.T, coord *Coordinator, appFactory ChainAppFactory, chainID string) *TestChain { t.Helper() - // generate validator private/public key - privVal := mock.NewPV() - pubKey, err := privVal.GetPubKey() - require.NoError(t, err) + genAccs := []authtypes.GenesisAccount{} + genBals := []banktypes.Balance{} + senderAccs := []SenderAccount{} + + // generate validators private/public key + var ( + validatorsPerChain = 4 + validators []*cmttypes.Validator + signersByAddress = make(map[string]cmttypes.PrivValidator, validatorsPerChain) + ) - // create validator set with single validator - validator := cmttypes.NewValidator(pubKey, 1) - valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{validator}) - signers := make(map[string]cmttypes.PrivValidator, 1) - signers[pubKey.Address().String()] = privVal - - // generate genesis account - senderPrivKey := secp256k1.GenPrivKey() - acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) - amount, ok := sdkmath.NewIntFromString("10000000000000000000000") - require.True(t, ok) - - balance := banktypes.Balance{ - Address: acc.GetAddress().String(), - Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)), + for i := 0; i < validatorsPerChain; i++ { + _, privVal := cmttypes.RandValidator(false, 100) + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + validators = append(validators, cmttypes.NewValidator(pubKey, 1)) + signersByAddress[pubKey.Address().String()] = privVal } - app := appFactory(t, valSet, []authtypes.GenesisAccount{acc}, chainID, nil, balance) + // construct validator set; + // Note that the validators are sorted by voting power + // or, if equal, by address lexical order + valSet := cmttypes.NewValidatorSet(validators) + + // generate genesis accounts + for i := 0; i < MaxAccounts; i++ { + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), uint64(i), 0) + amount, ok := sdkmath.NewIntFromString("10000000000000000000") + require.True(t, ok) + + // add sender account + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, amount)), + } + + genAccs = append(genAccs, acc) + genBals = append(genBals, balance) + + senderAcc := SenderAccount{ + SenderAccount: acc, + SenderPrivKey: senderPrivKey, + } + + senderAccs = append(senderAccs, senderAcc) + } + + app := appFactory(t, valSet, genAccs, chainID, nil, genBals...) // app := NewTestingAppDecorator(t, app.SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, "", nil, balance)) @@ -187,9 +213,10 @@ func NewTestChain(t *testing.T, coord *Coordinator, appFactory ChainAppFactory, Codec: app.AppCodec(), Vals: valSet, NextVals: valSet, - Signers: signers, - SenderPrivKey: senderPrivKey, - SenderAccount: acc, + Signers: signersByAddress, + SenderPrivKey: senderAccs[0].SenderPrivKey, + SenderAccount: senderAccs[0].SenderAccount, + SenderAccounts: senderAccs, DefaultMsgFees: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.ZeroInt())), } @@ -327,7 +354,7 @@ func (chain *TestChain) commitBlock(res *abci.ResponseFinalizeBlock) { // CurrentCmtClientHeader creates a CMT header using the current header parameters // on the chain. The trusted fields in the header are set to nil. -func (chain *TestChain) CurrentCmtClientHeader() *ibctm.Header { +func (chain *TestChain) CurrentCmtClientHeader() *ibctmtypes.Header { return chain.CreateCmtClientHeader( chain.ChainID, chain.CurrentHeader.Height, @@ -342,7 +369,7 @@ func (chain *TestChain) CurrentCmtClientHeader() *ibctm.Header { // CreateCmtClientHeader creates a CMT header to update the CMT client. Args are passed in to allow // caller flexibility to use params that differ from the chain. -func (chain *TestChain) CreateCmtClientHeader(chainID string, blockHeight int64, trustedHeight clienttypes.Height, timestamp time.Time, cmtValSet, nextVals, cmtTrustedVals *cmttypes.ValidatorSet, signers map[string]cmttypes.PrivValidator) *ibctm.Header { +func (chain *TestChain) CreateCmtClientHeader(chainID string, blockHeight int64, trustedHeight clienttypes.Height, timestamp time.Time, cmtValSet, nextVals, cmtTrustedVals *cmttypes.ValidatorSet, signers map[string]cmttypes.PrivValidator) *ibctmtypes.Header { var ( valSet *cmtproto.ValidatorSet trustedVals *cmtproto.ValidatorSet @@ -399,7 +426,7 @@ func (chain *TestChain) CreateCmtClientHeader(chainID string, blockHeight int64, // The trusted fields may be nil. They may be filled before relaying messages to a client. // The relayer is responsible for querying client and injecting appropriate trusted fields. - return &ibctm.Header{ + return &ibctmtypes.Header{ SignedHeader: signedHeader, ValidatorSet: valSet, TrustedHeight: trustedHeight, @@ -429,7 +456,7 @@ func (chain *TestChain) sendMsgs(msgs ...sdk.Msg) error { // occurred. func (chain *TestChain) SendMsgs(msgs ...sdk.Msg) (*abci.ExecTxResult, error) { rsp, gotErr := chain.sendWithSigner(chain.SenderPrivKey, chain.SenderAccount, msgs...) - require.NoError(chain.t, chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence()+1)) + //require.NoError(chain.t, chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence()+1)) return rsp, gotErr } @@ -452,31 +479,40 @@ func (chain *TestChain) sendWithSigner( // ensure the chain has the latest time chain.Coordinator.UpdateTimeForChain(chain) - blockResp, gotErr := app.SignAndDeliverWithoutCommit( + // increment acc sequence regardless of success or failure tx execution + defer func() { + err := chain.SenderAccount.SetSequence(chain.SenderAccount.GetSequence() + 1) + if err != nil { + panic(err) + } + }() + + blockResp, gotErr := app.SignAndDeliver( chain.t, chain.TxConfig, chain.App.GetBaseApp(), msgs, - chain.DefaultMsgFees, chain.ChainID, []uint64{senderAccount.GetAccountNumber()}, []uint64{senderAccount.GetSequence()}, + true, chain.CurrentHeader.GetTime(), + chain.NextVals.Hash(), senderPrivKey, ) if gotErr != nil { return nil, gotErr } - chain.commitBlock(blockResp) - chain.Coordinator.IncrementTime() require.Len(chain.t, blockResp.TxResults, 1) txResult := blockResp.TxResults[0] + if txResult.Code != 0 { return txResult, fmt.Errorf("%s/%d: %q", txResult.Codespace, txResult.Code, txResult.Log) } + chain.Coordinator.IncrementTime() chain.CaptureIBCEvents(txResult) return txResult, nil } @@ -591,13 +627,13 @@ func (chain *TestChain) GetPrefix() commitmenttypes.MerklePrefix { // ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the // light client on the source chain. -func (chain *TestChain) ConstructUpdateTMClientHeader(counterparty *TestChain, clientID string) (*ibctm.Header, error) { +func (chain *TestChain) ConstructUpdateTMClientHeader(counterparty *TestChain, clientID string) (*ibctmtypes.Header, error) { return chain.ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty, clientID, clienttypes.ZeroHeight()) } -// ConstructUpdateTMClientHeaderWithTrustedHeight will construct a valid 07-tendermint Header to update the +// ConstructUpdateTMClientHeader will construct a valid 07-tendermint Header to update the // light client on the source chain. -func (chain *TestChain) ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty *TestChain, clientID string, trustedHeight clienttypes.Height) (*ibctm.Header, error) { +func (chain *TestChain) ConstructUpdateTMClientHeaderWithTrustedHeight(counterparty *TestChain, clientID string, trustedHeight clienttypes.Height) (*ibctmtypes.Header, error) { header := counterparty.LastHeader // Relayer must query for LatestHeight on client to get TrustedHeight if the trusted height is not set if trustedHeight.IsZero() { @@ -607,12 +643,21 @@ func (chain *TestChain) ConstructUpdateTMClientHeaderWithTrustedHeight(counterpa tmTrustedVals *cmttypes.ValidatorSet ok bool ) - - tmTrustedVals, ok = counterparty.GetValsAtHeight(int64(trustedHeight.RevisionHeight)) - if !ok { - return nil, errorsmod.Wrapf(ibctm.ErrInvalidHeaderHeight, "could not retrieve trusted validators at trustedHeight: %d", trustedHeight) + // Once we get TrustedHeight from client, we must query the validators from the counterparty chain + // If the LatestHeight == LastHeader.Height, then TrustedValidators are current validators + // If LatestHeight < LastHeader.Height, we can query the historical validator set from HistoricalInfo + if trustedHeight == counterparty.LastHeader.GetHeight() { + tmTrustedVals = counterparty.Vals + } else { + // NOTE: We need to get validators from counterparty at height: trustedHeight+1 + // since the last trusted validators for a header at height h + // is the NextValidators at h+1 committed to in header h by + // NextValidatorsHash + tmTrustedVals, ok = counterparty.GetValsAtHeight(int64(trustedHeight.RevisionHeight + 1)) + if !ok { + return nil, errors.Wrapf(ibctmtypes.ErrInvalidHeaderHeight, "could not retrieve trusted validators at trustedHeight: %d", trustedHeight) + } } - // inject trusted fields into last header // for now assume revision number is 0 header.TrustedHeight = trustedHeight diff --git a/app/ibctesting/chain_test.go b/app/ibctesting/chain_test.go new file mode 100644 index 000000000..419cc0bb2 --- /dev/null +++ b/app/ibctesting/chain_test.go @@ -0,0 +1,53 @@ +package ibctesting_test + +import ( + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + "github.com/notional-labs/composable/v6/app/ibctesting" + "testing" + + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func NewTransferPath(chainA, chainB *ibctesting.TestChain) *ibctesting.Path { + path := ibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointA.ChannelConfig.Version = transfertypes.Version + path.EndpointB.ChannelConfig.Version = transfertypes.Version + + return path +} + +func TestChangeValSet(t *testing.T) { + coord := ibctesting.NewCoordinator(t, 2) + chainA := coord.GetChain(ibctesting.GetChainID(1)) + chainB := coord.GetChain(ibctesting.GetChainID(2)) + + path := NewTransferPath(chainA, chainB) + coord.Setup(path) + + amount, ok := sdkmath.NewIntFromString("10000000000000000000") + require.True(t, ok) + amount2, ok := sdkmath.NewIntFromString("30000000000000000000") + require.True(t, ok) + + val, err := chainA.App.GetStakingKeeper().GetValidators(chainA.GetContext(), 4) + require.NoError(t, err) + + chainA.App.GetStakingKeeper().Delegate(chainA.GetContext(), chainA.SenderAccounts[1].SenderAccount.GetAddress(), //nolint:errcheck // ignore error for test + amount, types.Unbonded, val[1], true) + chainA.App.GetStakingKeeper().Delegate(chainA.GetContext(), chainA.SenderAccounts[3].SenderAccount.GetAddress(), //nolint:errcheck // ignore error for test + amount2, types.Unbonded, val[3], true) + + coord.CommitBlock(chainA) + + // verify that update clients works even after validator update goes into effect + err = path.EndpointB.UpdateClient() + require.NoError(t, err) + err = path.EndpointB.UpdateClient() + require.NoError(t, err) +} diff --git a/app/ibctesting/coordinator.go b/app/ibctesting/coordinator.go index 6b5672498..fc91cad95 100644 --- a/app/ibctesting/coordinator.go +++ b/app/ibctesting/coordinator.go @@ -38,7 +38,7 @@ func NewCoordinator(t *testing.T, n int) *Coordinator { CurrentTime: globalStartTime, } - for i := 0; i < n; i++ { + for i := 1; i <= n; i++ { chainID := GetChainID(i) chains[chainID] = NewTestChain(t, coord, DefaultComposableAppFactory, chainID) } diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 38acee0ec..a2b3c1942 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -340,12 +340,12 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.PfmKeeper = pfmkeeper.NewKeeper( appCodec, appKeepers.keys[pfmtypes.StoreKey], - nil, + appKeepers.TransferKeeper, appKeepers.IBCKeeper.ChannelKeeper, &appKeepers.DistrKeeper, appKeepers.BankKeeper, - &appKeepers.TransferMiddlewareKeeper, - appKeepers.HooksICS4Wrapper, + appKeepers.TransferMiddlewareKeeper, + appKeepers.IBCKeeper.ChannelKeeper, govModAddress, ) @@ -373,7 +373,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( // TODO: Implement ICS4Wrapper in Records and pass records keeper here &appKeepers.HooksICS4Wrapper, // ICS4Wrapper appKeepers.TransferMiddlewareKeeper, - authtypes.NewModuleAddress(govtypes.ModuleName).String(), + govModAddress, ) scopedICQKeeper := appKeepers.CapabilityKeeper.ScopeToModule(icqtypes.ModuleName) @@ -381,7 +381,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.ICQKeeper = icqkeeper.NewKeeper( appCodec, appKeepers.keys[icqtypes.StoreKey], - &appKeepers.TransferMiddlewareKeeper, + appKeepers.IBCKeeper.ChannelKeeper, appKeepers.IBCKeeper.ChannelKeeper, appKeepers.IBCKeeper.PortKeeper, scopedICQKeeper, bApp.GRPCQueryRouter(), govModAddress, @@ -396,24 +396,22 @@ func (appKeepers *AppKeepers) InitNormalKeepers( // channel.RecvPacket -> ibc_hooks.OnRecvPacket -> ibc_rate_limit.OnRecvPacket -> forward.OnRecvPacket -> transfermiddleware_OnRecvPacket -> transfer.OnRecvPacket // - var transferStack porttypes.IBCModule - - transferStack = transfer.NewIBCModule(appKeepers.TransferKeeper.Keeper) + transferIBCModule := transfer.NewIBCModule(appKeepers.TransferKeeper.Keeper) - transferStack = transfermiddleware.NewIBCMiddleware( - transferStack, + transfermiddlewareStack := transfermiddleware.NewIBCMiddleware( + transferIBCModule, appKeepers.TransferMiddlewareKeeper, ) - transferStack = pfm.NewIBCMiddleware( - transferStack, + ibcMiddlewareStack := pfm.NewIBCMiddleware( + transfermiddlewareStack, appKeepers.PfmKeeper, 0, pfmkeeper.DefaultForwardTransferPacketTimeoutTimestamp, pfmkeeper.DefaultRefundTransferPacketTimeoutTimestamp, ) - transferStack = ratelimitmodule.NewIBCMiddleware(appKeepers.RatelimitKeeper, transferStack) - transferStack = ibc_hooks.NewIBCMiddleware(transferStack, &appKeepers.HooksICS4Wrapper) + ratelimitMiddlewareStack := ratelimitmodule.NewIBCMiddleware(appKeepers.RatelimitKeeper, ibcMiddlewareStack) + hooksTransferMiddleware := ibc_hooks.NewIBCMiddleware(ratelimitMiddlewareStack, &appKeepers.HooksICS4Wrapper) // Create evidence Keeper for to register the IBC light client misbehaviour evidence route evidenceKeeper := evidencekeeper.NewKeeper( @@ -462,7 +460,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.IBCKeeper.ChannelKeeper, appKeepers.IBCKeeper.PortKeeper, appKeepers.ScopedWasmKeeper, - appKeepers.TransferKeeper.Keeper, + appKeepers.TransferKeeper, bApp.MsgServiceRouter(), bApp.GRPCQueryRouter(), wasmDir, @@ -471,6 +469,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( govModAddress, wasmOpts..., ) + appKeepers.Ics20WasmHooks.ContractKeeper = &appKeepers.WasmKeeper wasmDataDir := filepath.Join(homePath, "wasm_client_data") wasmSupportedFeatures := strings.Join([]string{"storage", "iterator"}, ",") @@ -494,8 +493,6 @@ func (appKeepers *AppKeepers) InitNormalKeepers( bApp.GRPCQueryRouter(), ) - appKeepers.Ics20WasmHooks.ContractKeeper = &appKeepers.WasmKeeper - // Register Gov (must be registered after stakeibc) govRouter := govtypesv1beta1.NewRouter() // Register the proposal types @@ -520,7 +517,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( ) ibcRouter := porttypes.NewRouter() - ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) + ibcRouter.AddRoute(ibctransfertypes.ModuleName, hooksTransferMiddleware) ibcRouter.AddRoute(icqtypes.ModuleName, icqIBCModule) ibcRouter.AddRoute(wasmtypes.ModuleName, wasm.NewIBCHandler(appKeepers.WasmKeeper, appKeepers.IBCKeeper.ChannelKeeper, appKeepers.IBCKeeper.ChannelKeeper)) ibcRouter.AddRoute(icahosttypes.SubModuleName, icaHostStack) diff --git a/app/test_helpers.go b/app/test_helpers.go index 60e9d9552..f77a7c844 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -388,42 +388,33 @@ func SignCheckDeliver( return gInfo, res, err } -// SignAndDeliver signs and delivers a transaction. No simulation occurs as the -// ibc testing package causes checkState and deliverState to diverge in block time. func SignAndDeliver( - t *testing.T, txCfg client.TxConfig, app *baseapp.BaseApp, header tmproto.Header, msgs []sdk.Msg, - chainID string, accNums, accSeqs []uint64, _, expPass bool, priv ...cryptotypes.PrivKey, -) (sdk.GasInfo, *sdk.Result, error) { - t.Helper() - tx, err := helpers.GenSignedMockTx( + tb testing.TB, txCfg client.TxConfig, app *baseapp.BaseApp, msgs []sdk.Msg, + chainID string, accNums, accSeqs []uint64, expPass bool, blockTime time.Time, nextValHash []byte, priv ...cryptotypes.PrivKey, +) (*abci.ResponseFinalizeBlock, error) { + tb.Helper() + tx, err := simtestutil.GenSignedMockTx( rand.New(rand.NewSource(time.Now().UnixNano())), txCfg, msgs, sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, - 2*DefaultGas, + simtestutil.DefaultGenTxGas, chainID, accNums, accSeqs, priv..., ) - require.NoError(t, err) - - // Simulate a sending a transaction and committing a block - //app.BeginBlock(abci.RequestBeginBlock{Header: header}) - gInfo, res, err := app.SimDeliver(txCfg.TxEncoder(), tx) - - if expPass { - require.NoError(t, err) - require.NotNil(t, res) - } else { - require.Error(t, err) - require.Nil(t, res) - } + require.NoError(tb, err) - //app.EndBlock(abci.RequestEndBlock{}) - app.Commit() + txBytes, err := txCfg.TxEncoder()(tx) + require.NoError(tb, err) - return gInfo, res, err + return app.FinalizeBlock(&abci.RequestFinalizeBlock{ + Height: app.LastBlockHeight() + 1, + Time: blockTime, + NextValidatorsHash: nextValHash, + Txs: [][]byte{txBytes}, + }) } // GenSequenceOfTxs generates a set of signed transactions of messages, such diff --git a/go.mod b/go.mod index 20df69ef0..666e57818 100644 --- a/go.mod +++ b/go.mod @@ -361,7 +361,8 @@ replace ( github.com/CosmWasm/wasmvm => github.com/CosmWasm/wasmvm v1.5.2 github.com/cosmos/cosmos-sdk => github.com/notional-labs/cosmos-sdk v0.50.5-patch-validators-trim-tag - github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 => github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240501114651-5399289a5ef2 + github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 => github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240503092956-16204721528b + // github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 => github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240501060940-654293260efb // use cosmos-compatible protobufs github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 diff --git a/go.sum b/go.sum index 60b988f6e..1436ea9c2 100644 --- a/go.sum +++ b/go.sum @@ -1387,8 +1387,8 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/notional-labs/cosmos-sdk v0.50.5-patch-validators-trim-tag h1:lnMn2O2zjUvLbWJuKY3yXtrkVzCXQ7r1/oS/tgCsyjU= github.com/notional-labs/cosmos-sdk v0.50.5-patch-validators-trim-tag/go.mod h1:oV/k6GJgXV9QPoM2fsYDPPsyPBgQbdotv532O6Mz1OQ= -github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240501114651-5399289a5ef2 h1:3O/frX6x3SMOmVtJnn1BK9IFxL4U2WTlnk2vjVTYUwM= -github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240501114651-5399289a5ef2/go.mod h1:caFEYyFRCMVQdN9UsCC17apFxd/LSntCxEFpWIaDmr4= +github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240503092956-16204721528b h1:4T8dAAY8Sou9ib7t+5NW8W+WfCz2PKTG5tubmun9Sko= +github.com/notional-labs/ibc-apps/middleware/packet-forward-middleware/v8 v8.0.0-20240503092956-16204721528b/go.mod h1:dgFaRgM0YjzSBQ8zPLmBaQzMul8eNYPHu5EN84t5rYY= github.com/nunnatsa/ginkgolinter v0.9.0 h1:Sm0zX5QfjJzkeCjEp+t6d3Ha0jwvoDjleP9XCsrEzOA= github.com/nunnatsa/ginkgolinter v0.9.0/go.mod h1:FHaMLURXP7qImeH6bvxWJUpyH+2tuqe5j4rW1gxJRmI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/tests/interchaintest/basic_cosmos_test.go b/tests/interchaintest/basic_cosmos_test.go index a40b73197..50b143190 100644 --- a/tests/interchaintest/basic_cosmos_test.go +++ b/tests/interchaintest/basic_cosmos_test.go @@ -28,7 +28,7 @@ func TestBasicCentauri(t *testing.T) { cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ { Name: "centauri", - ChainConfig: centauriConfig, + ChainConfig: CentauriConfig, NumValidators: &numVals, NumFullNodes: &numFullNodes, }, diff --git a/tests/interchaintest/chain_start_test.go b/tests/interchaintest/chain_start_test.go index f5d0a9ab6..d63943f3e 100644 --- a/tests/interchaintest/chain_start_test.go +++ b/tests/interchaintest/chain_start_test.go @@ -28,7 +28,7 @@ func TestStartCentauri(t *testing.T) { cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ { Name: "centauri", - ChainConfig: centauriConfig, + ChainConfig: CentauriConfig, NumValidators: &numVals, NumFullNodes: &numFullNodes, }, diff --git a/tests/interchaintest/forward_timeout_test.go b/tests/interchaintest/forward_timeout_test.go new file mode 100644 index 000000000..406904518 --- /dev/null +++ b/tests/interchaintest/forward_timeout_test.go @@ -0,0 +1,414 @@ +package interchaintest + +import ( + "context" + "encoding/json" + "testing" + "time" + + "cosmossdk.io/math" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + chantypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + "github.com/strangelove-ventures/interchaintest/v8" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/relayer" + "github.com/strangelove-ventures/interchaintest/v8/testreporter" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestTimeoutOnForward(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + var ( + ctx = context.Background() + client, network = interchaintest.DockerSetup(t) + rep = testreporter.NewNopReporter() + eRep = rep.RelayerExecReporter(t) + chainIdA, chainIdB, chainIdC, chainIdD = "chain-a", "chain-b", "chain-c", "chain-d" + ) + + vals := 1 + fullNodes := 0 + + baseCfg := CentauriConfig + + baseCfg.ChainID = chainIdA + configA := baseCfg + + baseCfg.ChainID = chainIdB + configB := baseCfg + + baseCfg.ChainID = chainIdC + configC := baseCfg + + baseCfg.ChainID = chainIdD + configD := baseCfg + + cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ + {Name: "pfm", ChainConfig: configA, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configB, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configC, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configD, NumFullNodes: &fullNodes, NumValidators: &vals}, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + chainA, chainB, chainC, chainD := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain), chains[2].(*cosmos.CosmosChain), chains[3].(*cosmos.CosmosChain) + + r := interchaintest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t), + relayer.DockerImage(&DefaultRelayer), + relayer.StartupFlags("--processor", "events", "--block-history", "100"), + ).Build(t, client, network) + + const pathAB = "ab" + const pathBC = "bc" + const pathCD = "cd" + + ic := interchaintest.NewInterchain(). + AddChain(chainA). + AddChain(chainB). + AddChain(chainC). + AddChain(chainD). + AddRelayer(r, "relayer"). + AddLink(interchaintest.InterchainLink{ + Chain1: chainA, + Chain2: chainB, + Relayer: r, + Path: pathAB, + }). + AddLink(interchaintest.InterchainLink{ + Chain1: chainB, + Chain2: chainC, + Relayer: r, + Path: pathBC, + }). + AddLink(interchaintest.InterchainLink{ + Chain1: chainC, + Chain2: chainD, + Relayer: r, + Path: pathCD, + }) + + require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + SkipPathCreation: false, + })) + + t.Cleanup(func() { + _ = ic.Close() + }) + + // Start the relayer on only the path between chainA<>chainB so that the initial transfer succeeds + err = r.StartRelayer(ctx, eRep, pathAB) + require.NoError(t, err) + + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("an error occured while stopping the relayer: %s", err) + } + }, + ) + + // Fund user accounts with initial balances and get the transfer channel information between each set of chains + initBal := math.NewInt(10_000_000_000) + users := interchaintest.GetAndFundTestUsers(t, ctx, t.Name(), initBal, chainA, chainB, chainC, chainD) + + abChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdA, chainIdB) + require.NoError(t, err) + + baChan := abChan.Counterparty + + cbChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdC, chainIdB) + require.NoError(t, err) + + bcChan := cbChan.Counterparty + + dcChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdD, chainIdC) + require.NoError(t, err) + + cdChan := dcChan.Counterparty + + userA, userB, userC, userD := users[0], users[1], users[2], users[3] + + // Compose the prefixed denoms and ibc denom for asserting balances + firstHopDenom := transfertypes.GetPrefixedDenom(baChan.PortID, baChan.ChannelID, chainA.Config().Denom) + secondHopDenom := transfertypes.GetPrefixedDenom(cbChan.PortID, cbChan.ChannelID, firstHopDenom) + thirdHopDenom := transfertypes.GetPrefixedDenom(dcChan.PortID, dcChan.ChannelID, secondHopDenom) + + firstHopDenomTrace := transfertypes.ParseDenomTrace(firstHopDenom) + secondHopDenomTrace := transfertypes.ParseDenomTrace(secondHopDenom) + thirdHopDenomTrace := transfertypes.ParseDenomTrace(thirdHopDenom) + + firstHopIBCDenom := firstHopDenomTrace.IBCDenom() + secondHopIBCDenom := secondHopDenomTrace.IBCDenom() + thirdHopIBCDenom := thirdHopDenomTrace.IBCDenom() + + firstHopEscrowAccount := transfertypes.GetEscrowAddress(abChan.PortID, abChan.ChannelID).String() + secondHopEscrowAccount := transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID).String() + thirdHopEscrowAccount := transfertypes.GetEscrowAddress(cdChan.PortID, abChan.ChannelID).String() + + zeroBal := math.ZeroInt() + transferAmount := math.NewInt(100_000) + + // Attempt to send packet from Chain A->Chain B->Chain C->Chain D + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + retries := uint8(0) + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userD.FormattedAddress(), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + Retries: &retries, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + Retries: &retries, + Timeout: time.Second * 10, // Set low timeout for forward from chainB<>chainC + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + opts := ibc.TransferOptions{ + Memo: string(memo), + } + + chainBHeight, err := chainB.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, opts) + require.NoError(t, err) + + // Poll for MsgRecvPacket on chainB + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, chainB, cosmos.DefaultEncoding().InterfaceRegistry, chainBHeight, chainBHeight+20, nil) + require.NoError(t, err) + + // Stop the relayer and wait for the timeout to happen on chainC + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err) + + time.Sleep(time.Second * 11) + + // Restart the relayer + err = r.StartRelayer(ctx, eRep, pathAB, pathBC, pathCD) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + chainBHeight, err = chainB.Height(ctx) + require.NoError(t, err) + + // Poll for the MsgTimeout on chainB and the MsgAck on chainA + _, err = cosmos.PollForMessage[*chantypes.MsgTimeout](ctx, chainB, chainB.Config().EncodingConfig.InterfaceRegistry, chainBHeight, chainBHeight+20, nil) + require.NoError(t, err) + + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // Assert balances to ensure that the funds are still on the original sending chain + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainDBalance, err := chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal)) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(zeroBal)) + + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(zeroBal)) + require.True(t, secondHopEscrowBalance.Equal(zeroBal)) + require.True(t, thirdHopEscrowBalance.Equal(zeroBal)) + + // Send IBC transfer from ChainA -> ChainB -> ChainC -> ChainD that will succeed + secondHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userD.FormattedAddress(), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + nextBz, err = json.Marshal(secondHopMetadata) + require.NoError(t, err) + next = string(nextBz) + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + require.NoError(t, err) + + opts = ibc.TransferOptions{ + Memo: string(memo), + } + + chainAHeight, err = chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err = chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, opts) + require.NoError(t, err) + + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + + err = testutil.WaitForBlocks(ctx, 5, chainA) + require.NoError(t, err) + + // Assert balances are updated to reflect tokens now being on ChainD + chainABalance, err = chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err = chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err = chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainDBalance, err = chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal.Sub(transferAmount))) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(transferAmount)) + + firstHopEscrowBalance, err = chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err = chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err = chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(transferAmount)) + require.True(t, secondHopEscrowBalance.Equal(transferAmount)) + require.True(t, thirdHopEscrowBalance.Equal(transferAmount)) + + // Compose IBC tx that will attempt to go from ChainD -> ChainC -> ChainB -> ChainA but timeout between ChainB->ChainA + transfer = ibc.WalletAmount{ + Address: userC.FormattedAddress(), + Denom: thirdHopDenom, + Amount: transferAmount, + } + + secondHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + Timeout: 1 * time.Second, + }, + } + nextBz, err = json.Marshal(secondHopMetadata) + require.NoError(t, err) + next = string(nextBz) + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userB.FormattedAddress(), + Channel: cbChan.ChannelID, + Port: cbChan.PortID, + Next: &next, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainDHeight, err := chainD.Height(ctx) + require.NoError(t, err) + + transferTx, err = chainD.SendIBCTransfer(ctx, dcChan.ChannelID, userD.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + + _, err = testutil.PollForAck(ctx, chainD, chainDHeight, chainDHeight+25, transferTx.Packet) + require.NoError(t, err) + + err = testutil.WaitForBlocks(ctx, 5, chainD) + require.NoError(t, err) + + // Assert balances to ensure timeout happened and user funds are still present on ChainD + chainABalance, err = chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err = chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err = chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainDBalance, err = chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal.Sub(transferAmount))) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(transferAmount)) + + firstHopEscrowBalance, err = chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err = chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err = chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(transferAmount)) + require.True(t, secondHopEscrowBalance.Equal(transferAmount)) + require.True(t, thirdHopEscrowBalance.Equal(transferAmount)) +} diff --git a/tests/interchaintest/packet_forward_test.go b/tests/interchaintest/packet_forward_test.go new file mode 100644 index 000000000..d2f66945c --- /dev/null +++ b/tests/interchaintest/packet_forward_test.go @@ -0,0 +1,682 @@ +package interchaintest + +import ( + "context" + "encoding/json" + "github.com/strangelove-ventures/interchaintest/v8" + "testing" + "time" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/relayer" + "github.com/strangelove-ventures/interchaintest/v8/testreporter" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +type PacketMetadata struct { + Forward *ForwardMetadata `json:"forward"` +} + +type ForwardMetadata struct { + Receiver string `json:"receiver"` + Port string `json:"port"` + Channel string `json:"channel"` + Timeout time.Duration `json:"timeout"` + Retries *uint8 `json:"retries,omitempty"` + Next *string `json:"next,omitempty"` + RefundSequence *uint64 `json:"refund_sequence,omitempty"` +} + +func TestPacketForwardMiddleware(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + var ( + ctx = context.Background() + client, network = interchaintest.DockerSetup(t) + rep = testreporter.NewNopReporter() + eRep = rep.RelayerExecReporter(t) + chainIdA, chainIdB, chainIdC, chainIdD = "chain-1", "chain-2", "chain-3", "chain-4" + waitBlocks = 3 + ) + + vals := 1 + fullNodes := 0 + + baseCfg := CentauriConfig + + baseCfg.ChainID = chainIdA + configA := baseCfg + + baseCfg.ChainID = chainIdB + configB := baseCfg + + baseCfg.ChainID = chainIdC + configC := baseCfg + + baseCfg.ChainID = chainIdD + configD := baseCfg + + cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ + {Name: "pfm", ChainConfig: configA, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configB, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configC, NumFullNodes: &fullNodes, NumValidators: &vals}, + {Name: "pfm", ChainConfig: configD, NumFullNodes: &fullNodes, NumValidators: &vals}, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + chainA, chainB, chainC, chainD := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain), chains[2].(*cosmos.CosmosChain), chains[3].(*cosmos.CosmosChain) + + r := interchaintest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t), + relayer.DockerImage(&DefaultRelayer), + relayer.StartupFlags("--processor", "events", "--block-history", "100"), + ).Build(t, client, network) + + const pathAB = "ab" + const pathBC = "bc" + const pathCD = "cd" + + ic := interchaintest.NewInterchain(). + AddChain(chainA). + AddChain(chainB). + AddChain(chainC). + AddChain(chainD). + AddRelayer(r, "relayer"). + AddLink(interchaintest.InterchainLink{ + Chain1: chainA, + Chain2: chainB, + Relayer: r, + Path: pathAB, + }). + AddLink(interchaintest.InterchainLink{ + Chain1: chainB, + Chain2: chainC, + Relayer: r, + Path: pathBC, + }). + AddLink(interchaintest.InterchainLink{ + Chain1: chainC, + Chain2: chainD, + Relayer: r, + Path: pathCD, + }) + + require.NoError(t, ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + + SkipPathCreation: false, + })) + t.Cleanup(func() { + _ = ic.Close() + }) + + initBal := math.NewInt(10_000_000_000) + users := interchaintest.GetAndFundTestUsers(t, ctx, t.Name(), initBal, chainA, chainB, chainC, chainD) + + abChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdA, chainIdB) + require.NoError(t, err) + + baChan := abChan.Counterparty + + cbChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdC, chainIdB) + require.NoError(t, err) + + bcChan := cbChan.Counterparty + + dcChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainIdD, chainIdC) + require.NoError(t, err) + + cdChan := dcChan.Counterparty + + // Start the relayer on both paths + err = r.StartRelayer(ctx, eRep, pathAB, pathBC, pathCD) + require.NoError(t, err) + + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("an error occured while stopping the relayer: %s", err) + } + }, + ) + + // Get original account balances + userA, userB, userC, userD := users[0], users[1], users[2], users[3] + + // Compose the prefixed denoms and ibc denom for asserting balances + firstHopDenom := transfertypes.GetPrefixedDenom(baChan.PortID, baChan.ChannelID, chainA.Config().Denom) + secondHopDenom := transfertypes.GetPrefixedDenom(cbChan.PortID, cbChan.ChannelID, firstHopDenom) + thirdHopDenom := transfertypes.GetPrefixedDenom(dcChan.PortID, dcChan.ChannelID, secondHopDenom) + + firstHopDenomTrace := transfertypes.ParseDenomTrace(firstHopDenom) + secondHopDenomTrace := transfertypes.ParseDenomTrace(secondHopDenom) + thirdHopDenomTrace := transfertypes.ParseDenomTrace(thirdHopDenom) + + firstHopIBCDenom := firstHopDenomTrace.IBCDenom() + secondHopIBCDenom := secondHopDenomTrace.IBCDenom() + thirdHopIBCDenom := thirdHopDenomTrace.IBCDenom() + + firstHopEscrowAccount := sdk.MustBech32ifyAddressBytes(chainA.Config().Bech32Prefix, transfertypes.GetEscrowAddress(abChan.PortID, abChan.ChannelID)) + secondHopEscrowAccount := sdk.MustBech32ifyAddressBytes(chainB.Config().Bech32Prefix, transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID)) + thirdHopEscrowAccount := sdk.MustBech32ifyAddressBytes(chainC.Config().Bech32Prefix, transfertypes.GetEscrowAddress(cdChan.PortID, abChan.ChannelID)) + + zeroBal := math.ZeroInt() + transferAmount := math.NewInt(100_000) + + t.Run("multi-hop a->b->c->d", func(t *testing.T) { + // Send packet from Chain A->Chain B->Chain C->Chain D + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userD.FormattedAddress(), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainDBalance, err := chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal.Sub(transferAmount))) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(transferAmount)) + + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, transferAmount, firstHopEscrowBalance) + require.Equal(t, transferAmount, secondHopEscrowBalance) + require.Equal(t, transferAmount, thirdHopEscrowBalance) + }) + + t.Run("multi-hop denom unwind d->c->b->a", func(t *testing.T) { + // Send packet back from Chain D->Chain C->Chain B->Chain A + transfer := ibc.WalletAmount{ + Address: userC.FormattedAddress(), + Denom: thirdHopIBCDenom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userB.FormattedAddress(), + Channel: cbChan.ChannelID, + Port: cbChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainDHeight, err := chainD.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainD.SendIBCTransfer(ctx, dcChan.ChannelID, userD.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainD, chainDHeight, chainDHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + require.True(t, chainDBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainABalance.Equal(initBal)) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(zeroBal)) + require.True(t, secondHopEscrowBalance.Equal(zeroBal)) + require.True(t, thirdHopEscrowBalance.Equal(zeroBal)) + }) + + t.Run("forward ack error refund", func(t *testing.T) { + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C + // This should succeed in the first hop and fail to make the second hop; funds should then be refunded to Chain A. + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + metadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on Chain C + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + }, + } + + memo, err := json.Marshal(metadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+25, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal)) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(zeroBal)) + require.True(t, secondHopEscrowBalance.Equal(zeroBal)) + }) + t.Run("forward timeout refund", func(t *testing.T) { + // Send packet from Chain A->Chain B->Chain C with the timeout so low for B->C transfer that it can not make it from B to C, which should result in a refund from B to A after two retries. + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + retries := uint8(2) + metadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Retries: &retries, + Timeout: 1 * time.Second, + }, + } + + memo, err := json.Marshal(metadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+25, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal)) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(zeroBal)) + require.True(t, secondHopEscrowBalance.Equal(zeroBal)) + }) + + t.Run("multi-hop ack error refund", func(t *testing.T) { + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C->Chain D + // This should succeed in the first hop and second hop, then fail to make the third hop. + // Funds should be refunded to Chain B and then to Chain A via acknowledgements with errors. + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on chain D + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.FormattedAddress(), thirdHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), secondHopIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(initBal)) + require.True(t, chainBBalance.Equal(zeroBal)) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(zeroBal)) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.True(t, firstHopEscrowBalance.Equal(zeroBal)) + require.True(t, secondHopEscrowBalance.Equal(zeroBal)) + require.True(t, thirdHopEscrowBalance.Equal(zeroBal)) + }) + + t.Run("multi-hop through native chain ack error refund", func(t *testing.T) { + // send normal IBC transfer from B->A to get funds in IBC denom, then do multihop A->B(native)->C->D + // this lets us test the burn from escrow account on chain C and the escrow to escrow transfer on chain B. + + // Compose the prefixed denoms and ibc denom for asserting balances + baDenom := transfertypes.GetPrefixedDenom(abChan.PortID, abChan.ChannelID, chainB.Config().Denom) + bcDenom := transfertypes.GetPrefixedDenom(cbChan.PortID, cbChan.ChannelID, chainB.Config().Denom) + cdDenom := transfertypes.GetPrefixedDenom(dcChan.PortID, dcChan.ChannelID, bcDenom) + + baDenomTrace := transfertypes.ParseDenomTrace(baDenom) + bcDenomTrace := transfertypes.ParseDenomTrace(bcDenom) + cdDenomTrace := transfertypes.ParseDenomTrace(cdDenom) + + baIBCDenom := baDenomTrace.IBCDenom() + bcIBCDenom := bcDenomTrace.IBCDenom() + cdIBCDenom := cdDenomTrace.IBCDenom() + + transfer := ibc.WalletAmount{ + Address: userA.FormattedAddress(), + Denom: chainB.Config().Denom, + Amount: transferAmount, + } + + chainBHeight, err := chainB.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainB.SendIBCTransfer(ctx, baChan.ChannelID, userB.KeyName(), transfer, ibc.TransferOptions{}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainB, chainBHeight, chainBHeight+10, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainB) + require.NoError(t, err) + + // assert balance for user controlled wallet + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), baIBCDenom) + require.NoError(t, err) + + baEscrowBalance, err := chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(baChan.PortID, baChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(transferAmount)) + require.True(t, baEscrowBalance.Equal(transferAmount)) + + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C->Chain D + // This should succeed in the first hop and second hop, then fail to make the third hop. + // Funds should be refunded to Chain B and then to Chain A via acknowledgements with errors. + transfer = ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: baIBCDenom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on chain D + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err = chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.FormattedAddress(), cdIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.FormattedAddress(), bcIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), chainB.Config().Denom) + require.NoError(t, err) + + chainABalance, err = chainA.GetBalance(ctx, userA.FormattedAddress(), baIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(transferAmount)) + require.True(t, chainBBalance.Equal(initBal.Sub(transferAmount))) + require.True(t, chainCBalance.Equal(zeroBal)) + require.True(t, chainDBalance.Equal(zeroBal)) + + // assert balances for IBC escrow accounts + cdEscrowBalance, err := chainC.GetBalance(ctx, transfertypes.GetEscrowAddress(cdChan.PortID, cdChan.ChannelID).String(), bcIBCDenom) + require.NoError(t, err) + + bcEscrowBalance, err := chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + baEscrowBalance, err = chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(baChan.PortID, baChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + require.True(t, baEscrowBalance.Equal(transferAmount)) + require.True(t, bcEscrowBalance.Equal(zeroBal)) + require.True(t, cdEscrowBalance.Equal(zeroBal)) + }) + + t.Run("forward a->b->a", func(t *testing.T) { + // Send packet from Chain A->Chain B->Chain A + userABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err, "failed to get user a balance") + + userBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopDenom) + require.NoError(t, err, "failed to get user a balance") + + transfer := ibc.WalletAmount{ + Address: userB.FormattedAddress(), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName(), transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, waitBlocks, chainA) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.FormattedAddress(), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.FormattedAddress(), firstHopIBCDenom) + require.NoError(t, err) + + require.True(t, chainABalance.Equal(userABalance)) + require.True(t, chainBBalance.Equal(userBBalance)) + }) +} diff --git a/tests/interchaintest/setup.go b/tests/interchaintest/setup.go index 0ec8640c1..58cc6f7d6 100644 --- a/tests/interchaintest/setup.go +++ b/tests/interchaintest/setup.go @@ -18,7 +18,7 @@ var ( UidGid: "1025:1025", } - centauriConfig = ibc.ChainConfig{ + CentauriConfig = ibc.ChainConfig{ Type: "cosmos", Name: "centauri", ChainID: "centauri-2", @@ -34,6 +34,12 @@ var ( ModifyGenesis: nil, ConfigFileOverrides: nil, } + + DefaultRelayer = ibc.DockerImage{ + Repository: "ghcr.io/cosmos/relayer", + Version: "main", + UidGid: "1025:1025", + } ) // GetDockerImageInfo returns the appropriate repo and branch version string for integration with the CI pipeline. diff --git a/x/ratelimit/relay_test.go b/x/ratelimit/relay_test.go index 991ced406..855125c4f 100644 --- a/x/ratelimit/relay_test.go +++ b/x/ratelimit/relay_test.go @@ -25,7 +25,7 @@ type RateLimitTestSuite struct { } func (suite *RateLimitTestSuite) SetupTest() { - suite.coordinator = customibctesting.NewCoordinator(suite.T(), 4) + suite.coordinator = customibctesting.NewCoordinator(suite.T(), 3) suite.chainA = suite.coordinator.GetChain(customibctesting.GetChainID(1)) suite.chainB = suite.coordinator.GetChain(customibctesting.GetChainID(2)) suite.chainC = suite.coordinator.GetChain(customibctesting.GetChainID(3)) diff --git a/x/transfermiddleware/keeper/ics4wrapper.go b/x/transfermiddleware/keeper/ics4wrapper.go index b71bdf73d..6487b8841 100644 --- a/x/transfermiddleware/keeper/ics4wrapper.go +++ b/x/transfermiddleware/keeper/ics4wrapper.go @@ -60,7 +60,10 @@ func (keeper Keeper) handleOverrideSendPacketTransferLogic( } // burn native token // Get Coin from excrow address - keeper.bankKeeper.BurnCoins(ctx, transfertypes.ModuleName, sdk.NewCoins(nativeTransferToken)) + err = keeper.bankKeeper.BurnCoins(ctx, transfertypes.ModuleName, sdk.NewCoins(nativeTransferToken)) + if err != nil { + panic(err) + } // release lock IBC token and send it to sender // TODO: should we use a module address for this ?