From 838a1eb968cf44a5811b443ed21f9d9ca3c49c75 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Mon, 11 May 2026 15:41:47 +0200 Subject: [PATCH 1/9] add callbacks+ and dedup middleware spikes --- app/app.go | 15 ++- x/wasm/ibc.go | 82 ++++++++++++++-- x/wasm/ibc_callbacks_middleware.go | 73 +++++++++++++++ x/wasm/ibc_dedup_middleware.go | 146 +++++++++++++++++++++++++++++ x/wasm/ibc_test.go | 2 +- 5 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 x/wasm/ibc_callbacks_middleware.go create mode 100644 x/wasm/ibc_dedup_middleware.go diff --git a/app/app.go b/app/app.go index a5f5ff78d0..e90b0787e8 100644 --- a/app/app.go +++ b/app/app.go @@ -621,8 +621,9 @@ func NewWasmApp( wasmOpts..., ) + wasmContractKeeper := wasmkeeper.NewDefaultPermissionKeeper(&app.WasmKeeper) // Create fee enabled wasm ibc Stack - wasmStackIBCHandler := wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.TransferKeeper, app.IBCKeeper.ChannelKeeper) + wasmStackIBCHandler := wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.TransferKeeper, app.IBCKeeper.ChannelKeeper, wasmContractKeeper) // Create Interchain Accounts Stack // SendPacket, since it is originating from the application to core IBC: @@ -645,9 +646,15 @@ func NewWasmApp( // Create Transfer Stack var transferStack porttypes.IBCModule - transferStack = transfer.NewIBCModule(app.TransferKeeper) - transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.IBCKeeper.ChannelKeeper, wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas) - transferICS4Wrapper := transferStack.(porttypes.ICS4Wrapper) + transferStackBuilder := porttypes.NewIBCStackBuilder(app.IBCKeeper.ChannelKeeper) + transferStackBuilder.Base( + transfer.NewIBCModule(app.TransferKeeper)).Next( + ibccallbacks.NewIBCMiddleware(wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas), + ) + + transferStack = transferStackBuilder.Build() + transferICS4Wrapper := wasm.NewIBCCallbacksICS4Middleware(transferStack.(porttypes.ICS4Wrapper)) + // Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper app.TransferKeeper.WithICS4Wrapper(transferICS4Wrapper) diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go index 2b59c8ebcd..0db5561795 100644 --- a/x/wasm/ibc.go +++ b/x/wasm/ibc.go @@ -1,9 +1,13 @@ package wasm import ( + "encoding/json" "math" + sdkmath "cosmossdk.io/math" + wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types" + callbackstypes "github.com/cosmos/ibc-go/v10/modules/apps/callbacks/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" @@ -33,14 +37,24 @@ type appVersionGetter interface { } type IBCHandler struct { - keeper types.IBCContractKeeper - channelKeeper types.ChannelKeeper - transferKeeper types.ICS20TransferPortSource - appVersionGetter appVersionGetter + keeper types.IBCContractKeeper + ics4Wrapper porttypes.ICS4Wrapper + channelKeeper types.ChannelKeeper + transferKeeper types.ICS20TransferPortSource + contractKeeper types.ContractOpsKeeper +} + +func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, _ appVersionGetter, contractKeeper types.ContractOpsKeeper) IBCHandler { + return IBCHandler{ + keeper: k, + ics4Wrapper: ck, + channelKeeper: ck, + transferKeeper: tk, + contractKeeper: contractKeeper, + } } -func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, vg appVersionGetter) IBCHandler { - return IBCHandler{keeper: k, channelKeeper: ck, transferKeeper: tk, appVersionGetter: vg} +func (i IBCHandler) SetICS4Wrapper(_ porttypes.ICS4Wrapper) { } // OnChanOpenInit implements the IBCModule interface @@ -447,13 +461,43 @@ func (i IBCHandler) IBCReceivePacketCallback( transferData.Token.Denom.Trace = append(trace, transferData.Token.Denom.Trace...) } + denom := transferData.Token.GetDenom().IBCDenom() + amount := transferData.Token.GetAmount() + + // dest_callback.calldata present: dispatch via Execute with the + // transferred funds. Otherwise fall through to ibc_destination_callback. + cbData, isCb, cbErr := callbackstypes.GetCallbackData( + transferData, version, packet.GetSourcePort(), 0, + DefaultMaxIBCCallbackGas, callbackstypes.DestinationCallbackKey, + ) + if isCb && cbErr != nil { + return errorsmod.Wrap(cbErr, "parse dest_callback") + } + if isCb && len(cbData.Calldata) != 0 { + if !contractAddr.Equals(receiverAddr) { + return errorsmod.Wrapf(types.ErrInvalid, + "dest_callback address %s must match transfer receiver %s when using calldata", + contractAddr.String(), receiverAddr.String()) + } + amountInt, ok := sdkmath.NewIntFromString(amount) + if !ok { + return errorsmod.Wrapf(types.ErrInvalid, "invalid token amount: %s", amount) + } + funds := sdk.NewCoins(sdk.NewCoin(denom, amountInt)) + _, err = i.contractKeeper.Execute(cachedCtx, contractAddr, contractAddr, cbData.Calldata, funds) + if err != nil { + return errorsmod.Wrap(err, "execute contract via calldata") + } + return nil + } + transfer = &wasmvmtypes.IBCTransferCallback{ Receiver: receiverAddr.String(), Sender: transferData.Sender, Funds: wasmvmtypes.Array[wasmvmtypes.Coin]{ { - Denom: transferData.Token.GetDenom().IBCDenom(), - Amount: transferData.Token.GetAmount(), + Denom: denom, + Amount: amount, }, }, } @@ -533,3 +577,25 @@ func ValidateChannelParams(channelID string) error { func CreateErrorAcknowledgement(err error) ibcexported.Acknowledgement { return channeltypes.NewErrorAcknowledgementWithCodespace(err) } + +// jsonStringHasKey parses the memo as a json object and checks if it contains the key. +func jsonStringHasKey(memo, key string) (found bool, jsonObject map[string]interface{}) { + jsonObject = make(map[string]interface{}) + + // If there is no memo, the packet was either sent with an earlier version of IBC, or the memo was + // intentionally left blank. Nothing to do here. Ignore the packet and pass it down the stack. + if len(memo) == 0 { + return false, jsonObject + } + + // the jsonObject must be a valid JSON object + err := json.Unmarshal([]byte(memo), &jsonObject) + if err != nil { + return false, jsonObject + } + + // If the key doesn't exist, there's nothing to do on this hook. Continue by passing the packet + // down the stack + _, ok := jsonObject[key] + return ok, jsonObject +} diff --git a/x/wasm/ibc_callbacks_middleware.go b/x/wasm/ibc_callbacks_middleware.go new file mode 100644 index 0000000000..82a11180f9 --- /dev/null +++ b/x/wasm/ibc_callbacks_middleware.go @@ -0,0 +1,73 @@ +package wasm + +import ( + "encoding/json" + + transfertypes "github.com/cosmos/ibc-go/v11/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v11/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v11/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v11/modules/core/exported" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/CosmWasm/wasmd/x/wasm/types" +) + +var _ porttypes.ICS4Wrapper = IBCCallbacksICS4Middleware{} + +type IBCCallbacksICS4Middleware struct { + ics4Wrapper porttypes.ICS4Wrapper +} + +func NewIBCCallbacksICS4Middleware(ics4Wrapper porttypes.ICS4Wrapper) IBCCallbacksICS4Middleware { + return IBCCallbacksICS4Middleware{ics4Wrapper: ics4Wrapper} +} + +func (m IBCCallbacksICS4Middleware) SendPacket( + ctx sdk.Context, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + if err := validateSendMemo(data); err != nil { + return 0, err + } + return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +func (m IBCCallbacksICS4Middleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) +} + +func (m IBCCallbacksICS4Middleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +} + +// Rejects: +// - src_callback.calldata (no source-side execute path). +// - ibc_callback + src_callback in the same memo (ambiguous source dispatch). +func validateSendMemo(data []byte) error { + var packetData transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &packetData); err != nil { + return nil + } + + _, jsonObject := jsonStringHasKey(packetData.Memo, "src_callback") + if srcObj, ok := jsonObject["src_callback"].(map[string]interface{}); ok { + if _, hasCalldata := srcObj["calldata"]; hasCalldata { + return errorsmod.Wrap(types.ErrInvalid, "src_callback must not contain a calldata field") + } + } + + _, hasHooksSrc := jsonObject["ibc_callback"] + _, hasCallbacksSrc := jsonObject["src_callback"] + if hasHooksSrc && hasCallbacksSrc { + return errorsmod.Wrap(types.ErrInvalid, "ibc_callback and src_callback must not both be present in the memo") + } + + return nil +} diff --git a/x/wasm/ibc_dedup_middleware.go b/x/wasm/ibc_dedup_middleware.go new file mode 100644 index 0000000000..83e8caec97 --- /dev/null +++ b/x/wasm/ibc_dedup_middleware.go @@ -0,0 +1,146 @@ +package wasm + +import ( + "encoding/json" + + transfertypes "github.com/cosmos/ibc-go/v11/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v11/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v11/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v11/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v11/modules/core/exported" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/CosmWasm/wasmd/x/wasm/types" +) + +var ( + _ porttypes.IBCModule = (*IBCDedupMiddleware)(nil) + _ porttypes.ICS4Wrapper = (*IBCDedupMiddleware)(nil) + _ porttypes.Middleware = (*IBCDedupMiddleware)(nil) +) + +type IBCDedupMiddleware struct { + app porttypes.IBCModule + ics4Wrapper porttypes.ICS4Wrapper +} + +func NewIBCDedupMiddleware(app porttypes.IBCModule, ics4Wrapper porttypes.ICS4Wrapper) *IBCDedupMiddleware { + return &IBCDedupMiddleware{app: app, ics4Wrapper: ics4Wrapper} +} + +func (m *IBCDedupMiddleware) OnRecvPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + if modified := stripCallbacksOnHooksCollision(packet.Data); modified != nil { + packet.Data = modified + } + return m.app.OnRecvPacket(ctx, channelVersion, packet, relayer) +} + +func (m *IBCDedupMiddleware) SendPacket( + ctx sdk.Context, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + if modified := stripCallbacksOnHooksCollision(data); modified != nil { + data = modified + } + return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +func (m *IBCDedupMiddleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) +} + +func (m *IBCDedupMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +} + +// If the memo has both a hooks key (wasm | ibc_callback) and a callbacks key +// (dest_callback | src_callback), strip both callbacks keys (hooks wins). +// Returns nil when there's nothing to change. +func stripCallbacksOnHooksCollision(data []byte) []byte { + var packetData transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &packetData); err != nil { + return nil + } + _, jsonObject := jsonStringHasKey(packetData.Memo, "wasm") + _, hasWasm := jsonObject["wasm"] + _, hasIBCCallback := jsonObject["ibc_callback"] + if !hasWasm && !hasIBCCallback { + return nil + } + _, hasDest := jsonObject["dest_callback"] + _, hasSrc := jsonObject["src_callback"] + if !hasDest && !hasSrc { + return nil + } + + delete(jsonObject, "dest_callback") + delete(jsonObject, "src_callback") + if len(jsonObject) == 0 { + packetData.Memo = "" + } else { + bz, err := json.Marshal(jsonObject) + if err != nil { + return nil + } + packetData.Memo = string(bz) + } + out, err := json.Marshal(packetData) + if err != nil { + return nil + } + return out +} + +func (m *IBCDedupMiddleware) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, counterParty channeltypes.Counterparty, version string) (string, error) { + return m.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, counterParty, version) +} + +func (m *IBCDedupMiddleware) OnChanOpenTry(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, counterParty channeltypes.Counterparty, counterpartyVersion string) (string, error) { + return m.app.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, counterParty, counterpartyVersion) +} + +func (m *IBCDedupMiddleware) OnChanOpenAck(ctx sdk.Context, portID, channelID, counterpartyChannelID, counterpartyVersion string) error { + return m.app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) +} + +func (m *IBCDedupMiddleware) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error { + return m.app.OnChanOpenConfirm(ctx, portID, channelID) +} + +func (m *IBCDedupMiddleware) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error { + return m.app.OnChanCloseInit(ctx, portID, channelID) +} + +func (m *IBCDedupMiddleware) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error { + return m.app.OnChanCloseConfirm(ctx, portID, channelID) +} + +func (m *IBCDedupMiddleware) OnAcknowledgementPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error { + return m.app.OnAcknowledgementPacket(ctx, channelVersion, packet, acknowledgement, relayer) +} + +func (m *IBCDedupMiddleware) OnTimeoutPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) error { + return m.app.OnTimeoutPacket(ctx, channelVersion, packet, relayer) +} + +func (m *IBCDedupMiddleware) SetUnderlyingApplication(app porttypes.IBCModule) { + m.app = app +} + +func (m *IBCDedupMiddleware) SetICS4Wrapper(wrapper porttypes.ICS4Wrapper) { + m.ics4Wrapper = wrapper +} + +func (m *IBCDedupMiddleware) UnmarshalPacketData(ctx sdk.Context, portID string, channelID string, bz []byte) (any, string, error) { + if unmarshaler, ok := m.app.(porttypes.PacketDataUnmarshaler); ok { + return unmarshaler.UnmarshalPacketData(ctx, portID, channelID, bz) + } + return nil, "", errorsmod.Wrap(types.ErrInvalid, "underlying app does not implement PacketDataUnmarshaler") +} diff --git a/x/wasm/ibc_test.go b/x/wasm/ibc_test.go index 66a99a48fe..9f2e081592 100644 --- a/x/wasm/ibc_test.go +++ b/x/wasm/ibc_test.go @@ -110,7 +110,7 @@ func TestOnRecvPacket(t *testing.T) { }, } channelVersion := "" - h := NewIBCHandler(&mock, nil, nil, nil) + h := NewIBCHandler(&mock, nil, nil, nil, nil) em := &sdk.EventManager{} ctx := sdk.Context{}.WithEventManager(em) if spec.expPanic { From c729aa412d3bfa3174978b530b17a375108d7107 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Tue, 12 May 2026 22:26:21 +0200 Subject: [PATCH 2/9] add ibc v2 callbacks middleware --- app/app.go | 12 ++- x/wasm/ibc_callbacks_middleware.go | 73 ------------- x/wasm/ibc_callbacks_plus_middleware.go | 135 ++++++++++++++++++++++++ x/wasm/ibc_dedup_middleware.go | 10 +- 4 files changed, 150 insertions(+), 80 deletions(-) delete mode 100644 x/wasm/ibc_callbacks_middleware.go create mode 100644 x/wasm/ibc_callbacks_plus_middleware.go diff --git a/app/app.go b/app/app.go index e90b0787e8..9d7353946c 100644 --- a/app/app.go +++ b/app/app.go @@ -653,7 +653,7 @@ func NewWasmApp( ) transferStack = transferStackBuilder.Build() - transferICS4Wrapper := wasm.NewIBCCallbacksICS4Middleware(transferStack.(porttypes.ICS4Wrapper)) + transferICS4Wrapper := wasm.NewIBCV1CallbacksPlusMiddleware(transferStack.(porttypes.ICS4Wrapper)) // Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper app.TransferKeeper.WithICS4Wrapper(transferICS4Wrapper) @@ -667,8 +667,16 @@ func NewWasmApp( app.IBCKeeper.SetRouter(ibcRouter) ibcRouterV2 := ibcapi.NewRouter() + transferV2Stack := ibcapi.IBCModule(ibccallbacksv2.NewIBCMiddleware( + transferv2.NewIBCModule(app.TransferKeeper), + app.IBCKeeper.ChannelKeeperV2, + wasmStackIBCHandler, + app.IBCKeeper.ChannelKeeperV2, + wasm.DefaultMaxIBCCallbackGas, + )) + transferV2Stack = wasm.NewIBCV2CallbacksPlusMiddleware(transferV2Stack) ibcRouterV2 = ibcRouterV2. - AddRoute(ibctransfertypes.PortID, transferv2.NewIBCModule(app.TransferKeeper)). + AddRoute(ibctransfertypes.PortID, transferV2Stack). AddPrefixRoute(wasmkeeper.PortIDPrefixV2, wasmkeeper.NewIBC2Handler(app.WasmKeeper)) app.IBCKeeper.SetRouterV2(ibcRouterV2) diff --git a/x/wasm/ibc_callbacks_middleware.go b/x/wasm/ibc_callbacks_middleware.go deleted file mode 100644 index 82a11180f9..0000000000 --- a/x/wasm/ibc_callbacks_middleware.go +++ /dev/null @@ -1,73 +0,0 @@ -package wasm - -import ( - "encoding/json" - - transfertypes "github.com/cosmos/ibc-go/v11/modules/apps/transfer/types" - clienttypes "github.com/cosmos/ibc-go/v11/modules/core/02-client/types" - porttypes "github.com/cosmos/ibc-go/v11/modules/core/05-port/types" - ibcexported "github.com/cosmos/ibc-go/v11/modules/core/exported" - - errorsmod "cosmossdk.io/errors" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/CosmWasm/wasmd/x/wasm/types" -) - -var _ porttypes.ICS4Wrapper = IBCCallbacksICS4Middleware{} - -type IBCCallbacksICS4Middleware struct { - ics4Wrapper porttypes.ICS4Wrapper -} - -func NewIBCCallbacksICS4Middleware(ics4Wrapper porttypes.ICS4Wrapper) IBCCallbacksICS4Middleware { - return IBCCallbacksICS4Middleware{ics4Wrapper: ics4Wrapper} -} - -func (m IBCCallbacksICS4Middleware) SendPacket( - ctx sdk.Context, - sourcePort string, - sourceChannel string, - timeoutHeight clienttypes.Height, - timeoutTimestamp uint64, - data []byte, -) (uint64, error) { - if err := validateSendMemo(data); err != nil { - return 0, err - } - return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) -} - -func (m IBCCallbacksICS4Middleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { - return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) -} - -func (m IBCCallbacksICS4Middleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { - return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) -} - -// Rejects: -// - src_callback.calldata (no source-side execute path). -// - ibc_callback + src_callback in the same memo (ambiguous source dispatch). -func validateSendMemo(data []byte) error { - var packetData transfertypes.FungibleTokenPacketData - if err := json.Unmarshal(data, &packetData); err != nil { - return nil - } - - _, jsonObject := jsonStringHasKey(packetData.Memo, "src_callback") - if srcObj, ok := jsonObject["src_callback"].(map[string]interface{}); ok { - if _, hasCalldata := srcObj["calldata"]; hasCalldata { - return errorsmod.Wrap(types.ErrInvalid, "src_callback must not contain a calldata field") - } - } - - _, hasHooksSrc := jsonObject["ibc_callback"] - _, hasCallbacksSrc := jsonObject["src_callback"] - if hasHooksSrc && hasCallbacksSrc { - return errorsmod.Wrap(types.ErrInvalid, "ibc_callback and src_callback must not both be present in the memo") - } - - return nil -} diff --git a/x/wasm/ibc_callbacks_plus_middleware.go b/x/wasm/ibc_callbacks_plus_middleware.go new file mode 100644 index 0000000000..70208d20a6 --- /dev/null +++ b/x/wasm/ibc_callbacks_plus_middleware.go @@ -0,0 +1,135 @@ +package wasm + +import ( + "encoding/json" + + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/CosmWasm/wasmd/x/wasm/types" +) + +var _ porttypes.ICS4Wrapper = IBCV1CallbacksPlusMiddleware{} + +type IBCV1CallbacksPlusMiddleware struct { + ics4Wrapper porttypes.ICS4Wrapper +} + +func NewIBCV1CallbacksPlusMiddleware(ics4Wrapper porttypes.ICS4Wrapper) IBCV1CallbacksPlusMiddleware { + return IBCV1CallbacksPlusMiddleware{ics4Wrapper: ics4Wrapper} +} + +func (m IBCV1CallbacksPlusMiddleware) SendPacket( + ctx sdk.Context, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + var packetData transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &packetData); err == nil { + if err := validateMemo(packetData.Memo); err != nil { + return 0, err + } + } + return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +func (m IBCV1CallbacksPlusMiddleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) +} + +func (m IBCV1CallbacksPlusMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +} + +var _ ibcapi.IBCModule = IBCV2CallbacksPlusMiddleware{} + +type IBCV2CallbacksPlusMiddleware struct { + app ibcapi.IBCModule +} + +func NewIBCV2CallbacksPlusMiddleware(app ibcapi.IBCModule) IBCV2CallbacksPlusMiddleware { + return IBCV2CallbacksPlusMiddleware{app: app} +} + +func (m IBCV2CallbacksPlusMiddleware) OnSendPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + signer sdk.AccAddress, +) error { + if payload.SourcePort == transfertypes.PortID { + if data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version, payload.Encoding); err == nil { + if err := validateMemo(data.Memo); err != nil { + return err + } + } + } + return m.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnRecvPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) channeltypesv2.RecvPacketResult { + return m.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnAcknowledgementPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + acknowledgement []byte, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + return m.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnTimeoutPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + return m.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) +} + +// Rejects: +// - src_callback.calldata (no source-side execute path). +// - ibc_callback + src_callback in the same memo (ambiguous source dispatch). +func validateMemo(memo string) error { + _, jsonObject := jsonStringHasKey(memo, "src_callback") + if srcObj, ok := jsonObject["src_callback"].(map[string]interface{}); ok { + if _, hasCalldata := srcObj["calldata"]; hasCalldata { + return errorsmod.Wrap(types.ErrInvalid, "src_callback must not contain a calldata field") + } + } + + _, hasHooksSrc := jsonObject["ibc_callback"] + _, hasCallbacksSrc := jsonObject["src_callback"] + if hasHooksSrc && hasCallbacksSrc { + return errorsmod.Wrap(types.ErrInvalid, "ibc_callback and src_callback must not both be present in the memo") + } + + return nil +} diff --git a/x/wasm/ibc_dedup_middleware.go b/x/wasm/ibc_dedup_middleware.go index 83e8caec97..11029674be 100644 --- a/x/wasm/ibc_dedup_middleware.go +++ b/x/wasm/ibc_dedup_middleware.go @@ -3,11 +3,11 @@ package wasm import ( "encoding/json" - transfertypes "github.com/cosmos/ibc-go/v11/modules/apps/transfer/types" - clienttypes "github.com/cosmos/ibc-go/v11/modules/core/02-client/types" - channeltypes "github.com/cosmos/ibc-go/v11/modules/core/04-channel/types" - porttypes "github.com/cosmos/ibc-go/v11/modules/core/05-port/types" - ibcexported "github.com/cosmos/ibc-go/v11/modules/core/exported" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" errorsmod "cosmossdk.io/errors" From 2e57876bda85ab8c30f166ee23212f6906ff5452 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Wed, 13 May 2026 09:23:01 +0200 Subject: [PATCH 3/9] fix --- app/app.go | 18 ++----- x/wasm/ibc.go | 22 ++++----- x/wasm/ibc_callbacks_plus_middleware.go | 64 ------------------------- 3 files changed, 14 insertions(+), 90 deletions(-) diff --git a/app/app.go b/app/app.go index 9d7353946c..01b67ef9ad 100644 --- a/app/app.go +++ b/app/app.go @@ -646,13 +646,8 @@ func NewWasmApp( // Create Transfer Stack var transferStack porttypes.IBCModule - transferStackBuilder := porttypes.NewIBCStackBuilder(app.IBCKeeper.ChannelKeeper) - transferStackBuilder.Base( - transfer.NewIBCModule(app.TransferKeeper)).Next( - ibccallbacks.NewIBCMiddleware(wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas), - ) - - transferStack = transferStackBuilder.Build() + transferStack = transfer.NewIBCModule(app.TransferKeeper) + transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.IBCKeeper.ChannelKeeper, wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas) transferICS4Wrapper := wasm.NewIBCV1CallbacksPlusMiddleware(transferStack.(porttypes.ICS4Wrapper)) // Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper @@ -667,14 +662,7 @@ func NewWasmApp( app.IBCKeeper.SetRouter(ibcRouter) ibcRouterV2 := ibcapi.NewRouter() - transferV2Stack := ibcapi.IBCModule(ibccallbacksv2.NewIBCMiddleware( - transferv2.NewIBCModule(app.TransferKeeper), - app.IBCKeeper.ChannelKeeperV2, - wasmStackIBCHandler, - app.IBCKeeper.ChannelKeeperV2, - wasm.DefaultMaxIBCCallbackGas, - )) - transferV2Stack = wasm.NewIBCV2CallbacksPlusMiddleware(transferV2Stack) + transferV2Stack := ibcapi.IBCModule(transferv2.NewIBCModule(app.TransferKeeper)) ibcRouterV2 = ibcRouterV2. AddRoute(ibctransfertypes.PortID, transferV2Stack). AddPrefixRoute(wasmkeeper.PortIDPrefixV2, wasmkeeper.NewIBC2Handler(app.WasmKeeper)) diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go index 0db5561795..92777051d4 100644 --- a/x/wasm/ibc.go +++ b/x/wasm/ibc.go @@ -37,20 +37,20 @@ type appVersionGetter interface { } type IBCHandler struct { - keeper types.IBCContractKeeper - ics4Wrapper porttypes.ICS4Wrapper - channelKeeper types.ChannelKeeper - transferKeeper types.ICS20TransferPortSource - contractKeeper types.ContractOpsKeeper + keeper types.IBCContractKeeper + channelKeeper types.ChannelKeeper + transferKeeper types.ICS20TransferPortSource + appVersionGetter appVersionGetter + contractKeeper types.ContractOpsKeeper } -func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, _ appVersionGetter, contractKeeper types.ContractOpsKeeper) IBCHandler { +func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, vg appVersionGetter, contractKeeper types.ContractOpsKeeper) IBCHandler { return IBCHandler{ - keeper: k, - ics4Wrapper: ck, - channelKeeper: ck, - transferKeeper: tk, - contractKeeper: contractKeeper, + keeper: k, + channelKeeper: ck, + transferKeeper: tk, + appVersionGetter: vg, + contractKeeper: contractKeeper, } } diff --git a/x/wasm/ibc_callbacks_plus_middleware.go b/x/wasm/ibc_callbacks_plus_middleware.go index 70208d20a6..812a71132c 100644 --- a/x/wasm/ibc_callbacks_plus_middleware.go +++ b/x/wasm/ibc_callbacks_plus_middleware.go @@ -5,9 +5,7 @@ import ( transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" - channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" - ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" errorsmod "cosmossdk.io/errors" @@ -52,68 +50,6 @@ func (m IBCV1CallbacksPlusMiddleware) GetAppVersion(ctx sdk.Context, portID, cha return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) } -var _ ibcapi.IBCModule = IBCV2CallbacksPlusMiddleware{} - -type IBCV2CallbacksPlusMiddleware struct { - app ibcapi.IBCModule -} - -func NewIBCV2CallbacksPlusMiddleware(app ibcapi.IBCModule) IBCV2CallbacksPlusMiddleware { - return IBCV2CallbacksPlusMiddleware{app: app} -} - -func (m IBCV2CallbacksPlusMiddleware) OnSendPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - payload channeltypesv2.Payload, - signer sdk.AccAddress, -) error { - if payload.SourcePort == transfertypes.PortID { - if data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version, payload.Encoding); err == nil { - if err := validateMemo(data.Memo); err != nil { - return err - } - } - } - return m.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer) -} - -func (m IBCV2CallbacksPlusMiddleware) OnRecvPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - payload channeltypesv2.Payload, - relayer sdk.AccAddress, -) channeltypesv2.RecvPacketResult { - return m.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) -} - -func (m IBCV2CallbacksPlusMiddleware) OnAcknowledgementPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - acknowledgement []byte, - payload channeltypesv2.Payload, - relayer sdk.AccAddress, -) error { - return m.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer) -} - -func (m IBCV2CallbacksPlusMiddleware) OnTimeoutPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - payload channeltypesv2.Payload, - relayer sdk.AccAddress, -) error { - return m.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) -} - // Rejects: // - src_callback.calldata (no source-side execute path). // - ibc_callback + src_callback in the same memo (ambiguous source dispatch). From eccaa390191e21b9451e7aab3f91c1dabd02387a Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Thu, 14 May 2026 13:15:47 +0200 Subject: [PATCH 4/9] bump ibc v10.6.0 and revert ibcv2 callbacks plus --- app/app.go | 10 +++- go.mod | 2 +- go.sum | 4 +- x/wasm/ibc_callbacks_plus_middleware.go | 64 +++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/app.go b/app/app.go index 01b67ef9ad..ab9bd064fd 100644 --- a/app/app.go +++ b/app/app.go @@ -22,6 +22,7 @@ import ( icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/types" ibccallbacks "github.com/cosmos/ibc-go/v10/modules/apps/callbacks" + ibccallbacksv2 "github.com/cosmos/ibc-go/v10/modules/apps/callbacks/v2" "github.com/cosmos/ibc-go/v10/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -662,7 +663,14 @@ func NewWasmApp( app.IBCKeeper.SetRouter(ibcRouter) ibcRouterV2 := ibcapi.NewRouter() - transferV2Stack := ibcapi.IBCModule(transferv2.NewIBCModule(app.TransferKeeper)) + transferV2Stack := ibcapi.IBCModule(ibccallbacksv2.NewIBCMiddleware( + transferv2.NewIBCModule(app.TransferKeeper), + app.IBCKeeper.ChannelKeeperV2, + wasmStackIBCHandler, + app.IBCKeeper.ChannelKeeperV2, + wasm.DefaultMaxIBCCallbackGas, + )) + transferV2Stack = wasm.NewIBCV2CallbacksPlusMiddleware(transferV2Stack) ibcRouterV2 = ibcRouterV2. AddRoute(ibctransfertypes.PortID, transferV2Stack). AddPrefixRoute(wasmkeeper.PortIDPrefixV2, wasmkeeper.NewIBC2Handler(app.WasmKeeper)) diff --git a/go.mod b/go.mod index a554d28273..f44925940e 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( cosmossdk.io/x/upgrade v0.2.0 github.com/cometbft/cometbft v0.38.21 github.com/cosmos/cosmos-db v1.1.3 - github.com/cosmos/ibc-go/v10 v10.5.0 + github.com/cosmos/ibc-go/v10 v10.6.0 github.com/distribution/reference v0.5.0 github.com/rs/zerolog v1.34.0 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index aa78eb9a78..62a08a24e0 100644 --- a/go.sum +++ b/go.sum @@ -833,8 +833,8 @@ github.com/cosmos/gogoproto v1.7.2 h1:5G25McIraOC0mRFv9TVO139Uh3OklV2hczr13KKVHC github.com/cosmos/gogoproto v1.7.2/go.mod h1:8S7w53P1Y1cHwND64o0BnArT6RmdgIvsBuco6uTllsk= github.com/cosmos/iavl v1.2.6 h1:Hs3LndJbkIB+rEvToKJFXZvKo6Vy0Ex1SJ54hhtioIs= github.com/cosmos/iavl v1.2.6/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= -github.com/cosmos/ibc-go/v10 v10.5.0 h1:NI+cX04fXdu9JfP0V0GYeRi1ENa7PPdq0BYtVYo8Zrs= -github.com/cosmos/ibc-go/v10 v10.5.0/go.mod h1:a74pAPUSJ7NewvmvELU74hUClJhwnmm5MGbEaiTw/kE= +github.com/cosmos/ibc-go/v10 v10.6.0 h1:k7PZVSLXFtCdoWlU+ERGn2m1Np4Tw8BF8WyPGl0DOi4= +github.com/cosmos/ibc-go/v10 v10.6.0/go.mod h1:a74pAPUSJ7NewvmvELU74hUClJhwnmm5MGbEaiTw/kE= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= diff --git a/x/wasm/ibc_callbacks_plus_middleware.go b/x/wasm/ibc_callbacks_plus_middleware.go index 812a71132c..70208d20a6 100644 --- a/x/wasm/ibc_callbacks_plus_middleware.go +++ b/x/wasm/ibc_callbacks_plus_middleware.go @@ -5,7 +5,9 @@ import ( transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" errorsmod "cosmossdk.io/errors" @@ -50,6 +52,68 @@ func (m IBCV1CallbacksPlusMiddleware) GetAppVersion(ctx sdk.Context, portID, cha return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) } +var _ ibcapi.IBCModule = IBCV2CallbacksPlusMiddleware{} + +type IBCV2CallbacksPlusMiddleware struct { + app ibcapi.IBCModule +} + +func NewIBCV2CallbacksPlusMiddleware(app ibcapi.IBCModule) IBCV2CallbacksPlusMiddleware { + return IBCV2CallbacksPlusMiddleware{app: app} +} + +func (m IBCV2CallbacksPlusMiddleware) OnSendPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + signer sdk.AccAddress, +) error { + if payload.SourcePort == transfertypes.PortID { + if data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version, payload.Encoding); err == nil { + if err := validateMemo(data.Memo); err != nil { + return err + } + } + } + return m.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnRecvPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) channeltypesv2.RecvPacketResult { + return m.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnAcknowledgementPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + acknowledgement []byte, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + return m.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer) +} + +func (m IBCV2CallbacksPlusMiddleware) OnTimeoutPacket( + ctx sdk.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + return m.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) +} + // Rejects: // - src_callback.calldata (no source-side execute path). // - ibc_callback + src_callback in the same memo (ambiguous source dispatch). From 37b5b162b7c4124e0af0d156d6489dc079a0d0b2 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Mon, 25 May 2026 23:07:27 +0200 Subject: [PATCH 5/9] update impl with new requirements --- app/app.go | 15 ++- x/wasm/ibc.go | 31 +++-- x/wasm/ibc_callbacks_plus_middleware.go | 159 ++++++++++-------------- x/wasm/ibc_dedup_middleware.go | 115 ++++------------- 4 files changed, 120 insertions(+), 200 deletions(-) diff --git a/app/app.go b/app/app.go index ab9bd064fd..aee0830a87 100644 --- a/app/app.go +++ b/app/app.go @@ -648,11 +648,14 @@ func NewWasmApp( // Create Transfer Stack var transferStack porttypes.IBCModule transferStack = transfer.NewIBCModule(app.TransferKeeper) + transferStack = wasm.NewIBCV1CallbacksPlusMiddleware(transferStack) transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.IBCKeeper.ChannelKeeper, wasmStackIBCHandler, wasm.DefaultMaxIBCCallbackGas) - transferICS4Wrapper := wasm.NewIBCV1CallbacksPlusMiddleware(transferStack.(porttypes.ICS4Wrapper)) + // Chains that also wire the IBC Hooks middleware should wrap the stack + // with IBCDedupMiddleware to reject Hooks/Callbacks same-side memo collisions. + // transferStack = wasm.NewIBCDedupMiddleware(transferStack, transferStack.(porttypes.ICS4Wrapper)) // Since the callbacks middleware itself is an ics4wrapper, it needs to be passed to the ica controller keeper - app.TransferKeeper.WithICS4Wrapper(transferICS4Wrapper) + app.TransferKeeper.WithICS4Wrapper(transferStack.(porttypes.ICS4Wrapper)) // Create static IBC router, add app routes, then set and seal it ibcRouter := porttypes.NewRouter(). @@ -663,14 +666,14 @@ func NewWasmApp( app.IBCKeeper.SetRouter(ibcRouter) ibcRouterV2 := ibcapi.NewRouter() - transferV2Stack := ibcapi.IBCModule(ibccallbacksv2.NewIBCMiddleware( - transferv2.NewIBCModule(app.TransferKeeper), + transferV2Stack := ibcapi.IBCModule(wasm.NewIBCV2CallbacksPlusMiddleware(transferv2.NewIBCModule(app.TransferKeeper))) + transferV2Stack = ibccallbacksv2.NewIBCMiddleware( + transferV2Stack, app.IBCKeeper.ChannelKeeperV2, wasmStackIBCHandler, app.IBCKeeper.ChannelKeeperV2, wasm.DefaultMaxIBCCallbackGas, - )) - transferV2Stack = wasm.NewIBCV2CallbacksPlusMiddleware(transferV2Stack) + ) ibcRouterV2 = ibcRouterV2. AddRoute(ibctransfertypes.PortID, transferV2Stack). AddPrefixRoute(wasmkeeper.PortIDPrefixV2, wasmkeeper.NewIBC2Handler(app.WasmKeeper)) diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go index 92777051d4..5c4e6226cc 100644 --- a/x/wasm/ibc.go +++ b/x/wasm/ibc.go @@ -345,15 +345,30 @@ func (i IBCHandler) IBCSendPacketCallback( packetSenderAddress string, version string, ) error { - _, err := validateSender(contractAddress, packetSenderAddress) - if err != nil { + if _, err := validateSender(contractAddress, packetSenderAddress); err != nil { return err } - - // no-op, since we are not interested in this callback + // reject src_callback.calldata + if srcCallbackHasCalldata(packetData) { + return errorsmod.Wrap(types.ErrInvalid, "src_callback must not contain a calldata field") + } return nil } +func srcCallbackHasCalldata(packetData []byte) bool { + var pd transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(packetData, &pd); err != nil { + return false + } + _, obj := jsonStringHasKey(pd.Memo, "src_callback") + srcObj, ok := obj["src_callback"].(map[string]any) + if !ok { + return false + } + _, has := srcObj["calldata"] + return has +} + // IBCOnAcknowledgementPacketCallback implements the IBC Callbacks ContractKeeper interface // see https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-008-app-caller-cbs.md#contractkeeper func (i IBCHandler) IBCOnAcknowledgementPacketCallback( @@ -474,17 +489,13 @@ func (i IBCHandler) IBCReceivePacketCallback( return errorsmod.Wrap(cbErr, "parse dest_callback") } if isCb && len(cbData.Calldata) != 0 { - if !contractAddr.Equals(receiverAddr) { - return errorsmod.Wrapf(types.ErrInvalid, - "dest_callback address %s must match transfer receiver %s when using calldata", - contractAddr.String(), receiverAddr.String()) - } amountInt, ok := sdkmath.NewIntFromString(amount) if !ok { return errorsmod.Wrapf(types.ErrInvalid, "invalid token amount: %s", amount) } funds := sdk.NewCoins(sdk.NewCoin(denom, amountInt)) - _, err = i.contractKeeper.Execute(cachedCtx, contractAddr, contractAddr, cbData.Calldata, funds) + // receiverAddr is the intermediate sender + _, err = i.contractKeeper.Execute(cachedCtx, contractAddr, receiverAddr, cbData.Calldata, funds) if err != nil { return errorsmod.Wrap(err, "execute contract via calldata") } diff --git a/x/wasm/ibc_callbacks_plus_middleware.go b/x/wasm/ibc_callbacks_plus_middleware.go index 70208d20a6..1cf179203a 100644 --- a/x/wasm/ibc_callbacks_plus_middleware.go +++ b/x/wasm/ibc_callbacks_plus_middleware.go @@ -2,85 +2,96 @@ package wasm import ( "encoding/json" + "fmt" + "math" + callbackstypes "github.com/cosmos/ibc-go/v10/modules/apps/callbacks/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" - errorsmod "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/cosmos/cosmos-sdk/types/address" ) -var _ porttypes.ICS4Wrapper = IBCV1CallbacksPlusMiddleware{} - -type IBCV1CallbacksPlusMiddleware struct { - ics4Wrapper porttypes.ICS4Wrapper -} +// Verbatim from https://github.com/cosmos/ibc-apps/blob/main/modules/ibc-hooks/types/keys.go +const SenderPrefix = "ibc-wasm-hook-intermediary" -func NewIBCV1CallbacksPlusMiddleware(ics4Wrapper porttypes.ICS4Wrapper) IBCV1CallbacksPlusMiddleware { - return IBCV1CallbacksPlusMiddleware{ics4Wrapper: ics4Wrapper} +// Verbatim from https://github.com/cosmos/ibc-apps/blob/main/modules/ibc-hooks/keeper/keeper.go +func DeriveIntermediateSender(channel, originalSender, bech32Prefix string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channel, originalSender) + senderHash32 := address.Hash(SenderPrefix, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32) + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) } -func (m IBCV1CallbacksPlusMiddleware) SendPacket( - ctx sdk.Context, - sourcePort string, - sourceChannel string, - timeoutHeight clienttypes.Height, - timeoutTimestamp uint64, - data []byte, -) (uint64, error) { - var packetData transfertypes.FungibleTokenPacketData - if err := json.Unmarshal(data, &packetData); err == nil { - if err := validateMemo(packetData.Memo); err != nil { - return 0, err - } +// rewriteReceiverForCalldata replaces Receiver with the intermediate sender when memo has dest_callback.calldata. +// Returns the data unchanged otherwise. +func rewriteReceiverForCalldata(data []byte, destChannel string) []byte { + var pd transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &pd); err != nil { + return data + } + if !hasDestCalldata(pd) { + return data + } + intermediate, err := DeriveIntermediateSender(destChannel, pd.Sender, sdk.GetConfig().GetBech32AccountAddrPrefix()) + if err != nil { + return data } - return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + pd.Receiver = intermediate + out, err := json.Marshal(pd) + if err != nil { + return data + } + return out } -func (m IBCV1CallbacksPlusMiddleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { - return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) +// hasDestCalldata returns whether the packet carries a valid, non-empty dest_callback.calldata. +func hasDestCalldata(pd transfertypes.FungibleTokenPacketData) bool { + cbData, isCb, err := callbackstypes.GetCallbackData( + pd, "", "", 0, math.MaxUint64, callbackstypes.DestinationCallbackKey, + ) + return isCb && err == nil && len(cbData.Calldata) != 0 } -func (m IBCV1CallbacksPlusMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { - return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +// IBCV1CallbacksPlusMiddleware rewrites the recv packet's Receiver to the +// intermediate sender when memo carries dest_callback.calldata. +type IBCV1CallbacksPlusMiddleware struct { + callbackstypes.CallbacksCompatibleModule } -var _ ibcapi.IBCModule = IBCV2CallbacksPlusMiddleware{} +func NewIBCV1CallbacksPlusMiddleware(app porttypes.IBCModule) *IBCV1CallbacksPlusMiddleware { + compat, ok := app.(callbackstypes.CallbacksCompatibleModule) + if !ok { + panic(fmt.Errorf("wrapped app must implement %T", (*callbackstypes.CallbacksCompatibleModule)(nil))) + } + return &IBCV1CallbacksPlusMiddleware{CallbacksCompatibleModule: compat} +} -type IBCV2CallbacksPlusMiddleware struct { - app ibcapi.IBCModule +func (m *IBCV1CallbacksPlusMiddleware) OnRecvPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + packet.Data = rewriteReceiverForCalldata(packet.Data, packet.DestinationChannel) + return m.CallbacksCompatibleModule.OnRecvPacket(ctx, channelVersion, packet, relayer) } -func NewIBCV2CallbacksPlusMiddleware(app ibcapi.IBCModule) IBCV2CallbacksPlusMiddleware { - return IBCV2CallbacksPlusMiddleware{app: app} +// IBCV2CallbacksPlusMiddleware rewrites the recv packet's Receiver to the +// intermediate sender when memo carries dest_callback.calldata. +type IBCV2CallbacksPlusMiddleware struct { + callbackstypes.CallbacksCompatibleModuleV2 } -func (m IBCV2CallbacksPlusMiddleware) OnSendPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - payload channeltypesv2.Payload, - signer sdk.AccAddress, -) error { - if payload.SourcePort == transfertypes.PortID { - if data, err := transfertypes.UnmarshalPacketData(payload.Value, payload.Version, payload.Encoding); err == nil { - if err := validateMemo(data.Memo); err != nil { - return err - } - } +func NewIBCV2CallbacksPlusMiddleware(app ibcapi.IBCModule) *IBCV2CallbacksPlusMiddleware { + compat, ok := app.(callbackstypes.CallbacksCompatibleModuleV2) + if !ok { + panic(fmt.Errorf("wrapped app must implement %T", (*callbackstypes.CallbacksCompatibleModuleV2)(nil))) } - return m.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer) + return &IBCV2CallbacksPlusMiddleware{CallbacksCompatibleModuleV2: compat} } -func (m IBCV2CallbacksPlusMiddleware) OnRecvPacket( +func (m *IBCV2CallbacksPlusMiddleware) OnRecvPacket( ctx sdk.Context, sourceClient string, destinationClient string, @@ -88,48 +99,8 @@ func (m IBCV2CallbacksPlusMiddleware) OnRecvPacket( payload channeltypesv2.Payload, relayer sdk.AccAddress, ) channeltypesv2.RecvPacketResult { - return m.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) -} - -func (m IBCV2CallbacksPlusMiddleware) OnAcknowledgementPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - acknowledgement []byte, - payload channeltypesv2.Payload, - relayer sdk.AccAddress, -) error { - return m.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer) -} - -func (m IBCV2CallbacksPlusMiddleware) OnTimeoutPacket( - ctx sdk.Context, - sourceClient string, - destinationClient string, - sequence uint64, - payload channeltypesv2.Payload, - relayer sdk.AccAddress, -) error { - return m.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) -} - -// Rejects: -// - src_callback.calldata (no source-side execute path). -// - ibc_callback + src_callback in the same memo (ambiguous source dispatch). -func validateMemo(memo string) error { - _, jsonObject := jsonStringHasKey(memo, "src_callback") - if srcObj, ok := jsonObject["src_callback"].(map[string]interface{}); ok { - if _, hasCalldata := srcObj["calldata"]; hasCalldata { - return errorsmod.Wrap(types.ErrInvalid, "src_callback must not contain a calldata field") - } + if payload.SourcePort == transfertypes.PortID && payload.DestinationPort == transfertypes.PortID { + payload.Value = rewriteReceiverForCalldata(payload.Value, destinationClient) } - - _, hasHooksSrc := jsonObject["ibc_callback"] - _, hasCallbacksSrc := jsonObject["src_callback"] - if hasHooksSrc && hasCallbacksSrc { - return errorsmod.Wrap(types.ErrInvalid, "ibc_callback and src_callback must not both be present in the memo") - } - - return nil + return m.CallbacksCompatibleModuleV2.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) } diff --git a/x/wasm/ibc_dedup_middleware.go b/x/wasm/ibc_dedup_middleware.go index 11029674be..ffeb6540f7 100644 --- a/x/wasm/ibc_dedup_middleware.go +++ b/x/wasm/ibc_dedup_middleware.go @@ -22,20 +22,21 @@ var ( _ porttypes.Middleware = (*IBCDedupMiddleware)(nil) ) +// IBCDedupMiddleware rejects same-side Hooks/Callbacks memo collisions. type IBCDedupMiddleware struct { - app porttypes.IBCModule - ics4Wrapper porttypes.ICS4Wrapper + porttypes.IBCModule + porttypes.ICS4Wrapper } func NewIBCDedupMiddleware(app porttypes.IBCModule, ics4Wrapper porttypes.ICS4Wrapper) *IBCDedupMiddleware { - return &IBCDedupMiddleware{app: app, ics4Wrapper: ics4Wrapper} + return &IBCDedupMiddleware{IBCModule: app, ICS4Wrapper: ics4Wrapper} } func (m *IBCDedupMiddleware) OnRecvPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { - if modified := stripCallbacksOnHooksCollision(packet.Data); modified != nil { - packet.Data = modified + if hasMemoCollision(packet.Data, "wasm", "dest_callback") { + return CreateErrorAcknowledgement(errorsmod.Wrap(types.ErrInvalid, "memo must not contain both wasm (Hooks) and dest_callback (Callbacks)")) } - return m.app.OnRecvPacket(ctx, channelVersion, packet, relayer) + return m.IBCModule.OnRecvPacket(ctx, channelVersion, packet, relayer) } func (m *IBCDedupMiddleware) SendPacket( @@ -46,101 +47,35 @@ func (m *IBCDedupMiddleware) SendPacket( timeoutTimestamp uint64, data []byte, ) (uint64, error) { - if modified := stripCallbacksOnHooksCollision(data); modified != nil { - data = modified + if hasMemoCollision(data, "ibc_callback", "src_callback") { + return 0, errorsmod.Wrap(types.ErrInvalid, "memo must not contain both ibc_callback (Hooks) and src_callback (Callbacks)") } - return m.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) -} - -func (m *IBCDedupMiddleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { - return m.ics4Wrapper.WriteAcknowledgement(ctx, packet, ack) -} - -func (m *IBCDedupMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { - return m.ics4Wrapper.GetAppVersion(ctx, portID, channelID) -} - -// If the memo has both a hooks key (wasm | ibc_callback) and a callbacks key -// (dest_callback | src_callback), strip both callbacks keys (hooks wins). -// Returns nil when there's nothing to change. -func stripCallbacksOnHooksCollision(data []byte) []byte { - var packetData transfertypes.FungibleTokenPacketData - if err := json.Unmarshal(data, &packetData); err != nil { - return nil - } - _, jsonObject := jsonStringHasKey(packetData.Memo, "wasm") - _, hasWasm := jsonObject["wasm"] - _, hasIBCCallback := jsonObject["ibc_callback"] - if !hasWasm && !hasIBCCallback { - return nil - } - _, hasDest := jsonObject["dest_callback"] - _, hasSrc := jsonObject["src_callback"] - if !hasDest && !hasSrc { - return nil - } - - delete(jsonObject, "dest_callback") - delete(jsonObject, "src_callback") - if len(jsonObject) == 0 { - packetData.Memo = "" - } else { - bz, err := json.Marshal(jsonObject) - if err != nil { - return nil - } - packetData.Memo = string(bz) - } - out, err := json.Marshal(packetData) - if err != nil { - return nil - } - return out -} - -func (m *IBCDedupMiddleware) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, counterParty channeltypes.Counterparty, version string) (string, error) { - return m.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, counterParty, version) -} - -func (m *IBCDedupMiddleware) OnChanOpenTry(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, counterParty channeltypes.Counterparty, counterpartyVersion string) (string, error) { - return m.app.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, counterParty, counterpartyVersion) -} - -func (m *IBCDedupMiddleware) OnChanOpenAck(ctx sdk.Context, portID, channelID, counterpartyChannelID, counterpartyVersion string) error { - return m.app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) -} - -func (m *IBCDedupMiddleware) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error { - return m.app.OnChanOpenConfirm(ctx, portID, channelID) -} - -func (m *IBCDedupMiddleware) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error { - return m.app.OnChanCloseInit(ctx, portID, channelID) -} - -func (m *IBCDedupMiddleware) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error { - return m.app.OnChanCloseConfirm(ctx, portID, channelID) -} - -func (m *IBCDedupMiddleware) OnAcknowledgementPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error { - return m.app.OnAcknowledgementPacket(ctx, channelVersion, packet, acknowledgement, relayer) -} - -func (m *IBCDedupMiddleware) OnTimeoutPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) error { - return m.app.OnTimeoutPacket(ctx, channelVersion, packet, relayer) + return m.ICS4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) } func (m *IBCDedupMiddleware) SetUnderlyingApplication(app porttypes.IBCModule) { - m.app = app + m.IBCModule = app } func (m *IBCDedupMiddleware) SetICS4Wrapper(wrapper porttypes.ICS4Wrapper) { - m.ics4Wrapper = wrapper + m.ICS4Wrapper = wrapper } func (m *IBCDedupMiddleware) UnmarshalPacketData(ctx sdk.Context, portID string, channelID string, bz []byte) (any, string, error) { - if unmarshaler, ok := m.app.(porttypes.PacketDataUnmarshaler); ok { + if unmarshaler, ok := m.IBCModule.(porttypes.PacketDataUnmarshaler); ok { return unmarshaler.UnmarshalPacketData(ctx, portID, channelID, bz) } return nil, "", errorsmod.Wrap(types.ErrInvalid, "underlying app does not implement PacketDataUnmarshaler") } + +// hasMemoCollision returns true if the packet memo contains both +// hooksKey and callbacksKey. Returns false otherwise. +func hasMemoCollision(data []byte, hooksKey, callbacksKey string) bool { + var pd transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &pd); err != nil { + return false + } + hasHooks, memo := jsonStringHasKey(pd.Memo, hooksKey) + _, hasCallbacks := memo[callbacksKey] + return hasHooks && hasCallbacks +} From 5073ed932c4e631039b6f907e2a17e9b8b36c011 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Tue, 26 May 2026 18:07:23 +0200 Subject: [PATCH 6/9] fix --- x/wasm/ibc.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go index 5c4e6226cc..3cd46d8f6d 100644 --- a/x/wasm/ibc.go +++ b/x/wasm/ibc.go @@ -494,8 +494,21 @@ func (i IBCHandler) IBCReceivePacketCallback( return errorsmod.Wrapf(types.ErrInvalid, "invalid token amount: %s", amount) } funds := sdk.NewCoins(sdk.NewCoin(denom, amountInt)) - // receiverAddr is the intermediate sender - _, err = i.contractKeeper.Execute(cachedCtx, contractAddr, receiverAddr, cbData.Calldata, funds) + // Re-derive: ibccallbacks passes packet by value, so the + // rewriter's Receiver mutation doesn't reach this callback. + // https://github.com/cosmos/ibc-go/blob/v10.6.0/modules/apps/callbacks/ibc_middleware.go#L217 + intermediateBech32, err := DeriveIntermediateSender( + packet.GetDestChannel(), transferData.Sender, + sdk.GetConfig().GetBech32AccountAddrPrefix(), + ) + if err != nil { + return errorsmod.Wrap(err, "derive intermediate sender") + } + intermediate, err := sdk.AccAddressFromBech32(intermediateBech32) + if err != nil { + return errorsmod.Wrap(err, "parse intermediate sender") + } + _, err = i.contractKeeper.Execute(cachedCtx, contractAddr, intermediate, cbData.Calldata, funds) if err != nil { return errorsmod.Wrap(err, "execute contract via calldata") } From 5b2445d4bc7168cb82f37eff2dcaa84e146f66b0 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Tue, 26 May 2026 19:28:31 +0200 Subject: [PATCH 7/9] add test --- x/wasm/ibc_dedup_middleware_test.go | 136 ++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 x/wasm/ibc_dedup_middleware_test.go diff --git a/x/wasm/ibc_dedup_middleware_test.go b/x/wasm/ibc_dedup_middleware_test.go new file mode 100644 index 0000000000..c335fc34a6 --- /dev/null +++ b/x/wasm/ibc_dedup_middleware_test.go @@ -0,0 +1,136 @@ +package wasm + +import ( + "testing" + + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type mockICS4Wrapper struct { + porttypes.ICS4Wrapper + sentData []byte +} + +func (m *mockICS4Wrapper) SendPacket(_ sdk.Context, _, _ string, _ clienttypes.Height, _ uint64, data []byte) (uint64, error) { + m.sentData = data + return 1, nil +} + +func TestIBCDedupMiddlewareOnRecvPacket(t *testing.T) { + specs := map[string]struct { + memo map[string]any + rawData []byte + expFail bool + }{ + "wasm + dest_callback (same dest side) rejected": { + memo: map[string]any{ + "wasm": map[string]any{"contract": "cosmos1ccc", "msg": map[string]any{}}, + "dest_callback": map[string]any{"address": "cosmos1ccc"}, + }, + expFail: true, + }, + "wasm + src_callback (cross-side) passes through": { + memo: map[string]any{ + "wasm": map[string]any{"contract": "cosmos1ccc", "msg": map[string]any{}}, + "src_callback": map[string]any{"address": "cosmos1ccc"}, + }, + }, + "dest_callback alone passes through": {memo: map[string]any{"dest_callback": map[string]any{"address": "cosmos1ccc"}}}, + "non-transfer payload passes through": {rawData: []byte("not-a-transfer-packet")}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + inner := &recordingIBCModule{} + m := NewIBCDedupMiddleware(inner, &mockICS4Wrapper{}) + + data := spec.rawData + if data == nil { + data = transferPacketFixture(mustMarshalJSON(t, spec.memo)).Data + } + pkt := channeltypes.Packet{ + Sequence: 1, SourcePort: "transfer", SourceChannel: "channel-0", + DestinationPort: "transfer", DestinationChannel: "channel-1", + Data: data, + TimeoutHeight: clienttypes.Height{RevisionHeight: 100}, + } + + gotAck := m.OnRecvPacket(sdk.Context{}, "ics20-1", pkt, sdk.AccAddress("relayer")) + require.NotNil(t, gotAck) + + if spec.expFail { + assert.False(t, gotAck.Success()) + assert.Nil(t, inner.received) + return + } + assert.Equal(t, data, inner.received) + }) + } +} + +func TestIBCDedupMiddlewareSendPacket(t *testing.T) { + specs := map[string]struct { + memo map[string]any + rawData []byte + expFail bool + }{ + "ibc_callback + src_callback (same src side) rejected": { + memo: map[string]any{ + "ibc_callback": "cosmos1ccc", + "src_callback": map[string]any{"address": "cosmos1ccc"}, + }, + expFail: true, + }, + "wasm + src_callback (cross-side) passes through": { + memo: map[string]any{ + "wasm": map[string]any{"contract": "cosmos1ccc", "msg": map[string]any{}}, + "src_callback": map[string]any{"address": "cosmos1ccc"}, + }, + }, + "src_callback alone passes through": {memo: map[string]any{"src_callback": map[string]any{"address": "cosmos1ccc"}}}, + "non-transfer payload passes through": {rawData: []byte("not-a-transfer-packet")}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ics4 := &mockICS4Wrapper{} + m := NewIBCDedupMiddleware(&recordingIBCModule{}, ics4) + + data := spec.rawData + if data == nil { + data = transferPacketFixture(mustMarshalJSON(t, spec.memo)).Data + } + + _, gotErr := m.SendPacket(sdk.Context{}, "transfer", "channel-0", + clienttypes.Height{RevisionHeight: 100}, 0, data) + + if spec.expFail { + require.Error(t, gotErr) + assert.Nil(t, ics4.sentData) + return + } + require.NoError(t, gotErr) + assert.Equal(t, data, ics4.sentData) + }) + } +} + +func transferPacketFixture(memo string) channeltypes.Packet { + td := transfertypes.NewFungibleTokenPacketData("uosmo", "1000", "cosmos1sender", "cosmos1receiver", memo) + return channeltypes.Packet{ + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + DestinationPort: "transfer", + DestinationChannel: "channel-1", + Data: td.GetBytes(), + TimeoutHeight: clienttypes.Height{RevisionHeight: 100}, + } +} From db22d2894b294959a5e82c43b951f437227176fe Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Tue, 26 May 2026 21:15:47 +0200 Subject: [PATCH 8/9] add tests --- x/wasm/ibc_callbacks_plus_middleware_test.go | 349 +++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 x/wasm/ibc_callbacks_plus_middleware_test.go diff --git a/x/wasm/ibc_callbacks_plus_middleware_test.go b/x/wasm/ibc_callbacks_plus_middleware_test.go new file mode 100644 index 0000000000..7ad03e5f3c --- /dev/null +++ b/x/wasm/ibc_callbacks_plus_middleware_test.go @@ -0,0 +1,349 @@ +package wasm + +import ( + "encoding/hex" + "encoding/json" + "testing" + + wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types" + "github.com/cometbft/cometbft/libs/rand" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + mockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + + "github.com/CosmWasm/wasmd/x/wasm/keeper/wasmtesting" + "github.com/CosmWasm/wasmd/x/wasm/types" +) + +type mockContractOpsKeeper struct { + types.ContractOpsKeeper + executeFn func(ctx sdk.Context, contractAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) +} + +func (m *mockContractOpsKeeper) Execute(ctx sdk.Context, contractAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) ([]byte, error) { + if m.executeFn == nil { + panic("Execute not expected to be called") + } + return m.executeFn(ctx, contractAddress, caller, msg, coins) +} + +type mockDestCallbackKeeper struct { + types.IBCContractKeeper + fn func(ctx sdk.Context, contractAddr sdk.AccAddress, msg wasmvmtypes.IBCDestinationCallbackMsg) error +} + +func (m *mockDestCallbackKeeper) IBCDestinationCallback(ctx sdk.Context, contractAddr sdk.AccAddress, msg wasmvmtypes.IBCDestinationCallbackMsg) error { + return m.fn(ctx, contractAddr, msg) +} + +func TestIBCReceivePacketCallback(t *testing.T) { + myContractAddr := sdk.AccAddress(rand.Bytes(address.Len)) + contractMsg := []byte(`{"swap":{"output_denom":"uatom","min_output":"1000"}}`) + calldataMemo := mustMarshalJSON(t, map[string]any{ + "dest_callback": map[string]any{ + "address": myContractAddr.String(), + "calldata": hex.EncodeToString(contractMsg), + }, + }) + intermediate := testIntermediateAddr(t, "channel-1", "cosmos1sender") + ibcDenom := transfertypes.Denom{ + Base: "uosmo", + Trace: []transfertypes.Hop{transfertypes.NewHop("transfer", "channel-1")}, + }.IBCDenom() + + specs := map[string]struct { + memo string + receiver string + execErr error + expErr string + expExec bool + expDestCB bool + }{ + "rewritten receiver: execute with intermediate caller": { + memo: calldataMemo, + receiver: intermediate.String(), + expExec: true, + }, + "untouched receiver: execute still derives intermediate locally": { + memo: calldataMemo, + receiver: myContractAddr.String(), + expExec: true, + }, + "execute returns error: callback wraps it": { + memo: calldataMemo, + receiver: myContractAddr.String(), + execErr: types.ErrExecuteFailed.Wrap("contract reverted"), + expErr: "execute contract via calldata", + }, + "no calldata: falls through to ibc_destination_callback": { + memo: "", + receiver: myContractAddr.String(), + expDestCB: true, + }, + } + + const amount = "5000" + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + pkt := channeltypes.Packet{ + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + DestinationPort: "transfer", + DestinationChannel: "channel-1", + Data: transfertypes.NewFungibleTokenPacketData( + "uosmo", amount, "cosmos1sender", spec.receiver, spec.memo, + ).GetBytes(), + TimeoutHeight: clienttypes.Height{RevisionHeight: 100}, + } + + var gotExec, gotDestCB bool + executor := &mockContractOpsKeeper{ + executeFn: func(_ sdk.Context, gotContract, gotCaller sdk.AccAddress, gotMsg []byte, gotCoins sdk.Coins) ([]byte, error) { + gotExec = true + assert.Equal(t, myContractAddr, gotContract) + assert.Equal(t, intermediate, gotCaller) + assert.JSONEq(t, string(contractMsg), string(gotMsg)) + require.Len(t, gotCoins, 1) + assert.Equal(t, ibcDenom, gotCoins[0].Denom) + expAmount, ok := sdkmath.NewIntFromString(amount) + require.True(t, ok) + assert.Equal(t, expAmount, gotCoins[0].Amount) + if spec.execErr != nil { + return nil, spec.execErr + } + return []byte("ok"), nil + }, + } + + contractKeeper := &wasmtesting.IBCContractKeeperMock{} + if spec.expDestCB { + contractKeeper.IBCContractKeeper = &mockDestCallbackKeeper{ + fn: func(_ sdk.Context, gotAddr sdk.AccAddress, msg wasmvmtypes.IBCDestinationCallbackMsg) error { + gotDestCB = true + assert.Equal(t, myContractAddr, gotAddr) + require.NotNil(t, msg.Transfer) + assert.Equal(t, amount, msg.Transfer.Funds[0].Amount) + return nil + }, + } + } + + h := NewIBCHandler( + contractKeeper, nil, + &wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { return "transfer" }}, + nil, executor, + ) + ctx := sdk.Context{}.WithEventManager(&sdk.EventManager{}) + + gotErr := h.IBCReceivePacketCallback(ctx, pkt, channeltypes.NewResultAcknowledgement([]byte{1}), myContractAddr.String(), "ics20-1") + if spec.expErr != "" { + require.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expExec, gotExec) + assert.Equal(t, spec.expDestCB, gotDestCB) + }) + } +} + +func TestIBCSendPacketCallback(t *testing.T) { + myContractAddr := sdk.AccAddress(rand.Bytes(address.Len)).String() + + specs := map[string]struct { + memo string + expErr string + }{ + "src_callback.calldata rejected": { + memo: mustMarshalJSON(t, map[string]any{ + "src_callback": map[string]any{"address": myContractAddr, "calldata": "deadbeef"}, + }), + expErr: "src_callback must not contain a calldata field", + }, + "src_callback without calldata accepted": { + memo: mustMarshalJSON(t, map[string]any{ + "src_callback": map[string]any{"address": myContractAddr}, + }), + }, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + transferData := transfertypes.NewFungibleTokenPacketData("uosmo", "100", myContractAddr, "cosmos1receiver", spec.memo) + + h := NewIBCHandler( + &wasmtesting.IBCContractKeeperMock{}, nil, + &wasmtesting.MockIBCTransferKeeper{GetPortFn: func(ctx sdk.Context) string { return "transfer" }}, + nil, nil, + ) + + gotErr := h.IBCSendPacketCallback( + sdk.Context{}, "transfer", "channel-0", + clienttypes.Height{RevisionHeight: 100}, 0, + transferData.GetBytes(), + myContractAddr, myContractAddr, "ics20-1", + ) + if spec.expErr != "" { + require.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), spec.expErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func mustMarshalJSON(t *testing.T, m map[string]any) string { + t.Helper() + bz, err := json.Marshal(m) + require.NoError(t, err) + return string(bz) +} + +type recordingIBCModule struct { + porttypes.IBCModule + received []byte +} + +func (r *recordingIBCModule) OnRecvPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + r.received = packet.Data + return channeltypes.NewResultAcknowledgement([]byte{1}) +} + +func (r *recordingIBCModule) UnmarshalPacketData(_ sdk.Context, _, _ string, _ []byte) (any, string, error) { + return nil, "", nil +} + +func testIntermediateBech32(t *testing.T, channel, sender string) string { + t.Helper() + s, err := DeriveIntermediateSender(channel, sender, sdk.GetConfig().GetBech32AccountAddrPrefix()) + require.NoError(t, err) + return s +} + +func testIntermediateAddr(t *testing.T, channel, sender string) sdk.AccAddress { + t.Helper() + a, err := sdk.AccAddressFromBech32(testIntermediateBech32(t, channel, sender)) + require.NoError(t, err) + return a +} + +func TestIBCV1CallbacksPlusMiddleware(t *testing.T) { + calldataHex := hex.EncodeToString([]byte(`{"swap":{}}`)) + + specs := map[string]struct { + memo string + expRewrite bool + }{ + "dest_callback with calldata rewrites receiver": { + memo: mustMarshalJSON(t, map[string]any{ + "dest_callback": map[string]any{"address": "cosmos1contract", "calldata": calldataHex}, + }), + expRewrite: true, + }, + "no memo": {memo: ""}, + "dest_callback without calldata": {memo: `{"dest_callback":{"address":"cosmos1ccc"}}`}, + "malformed memo (not json)": {memo: `{not-json`}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + transferData := transfertypes.NewFungibleTokenPacketData("uosmo", "100", "cosmos1sender", "cosmos1receiver", spec.memo) + pkt := channeltypes.Packet{ + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + DestinationPort: "transfer", + DestinationChannel: "channel-1", + Data: transferData.GetBytes(), + TimeoutHeight: clienttypes.Height{RevisionHeight: 100}, + } + + inner := &recordingIBCModule{} + m := NewIBCV1CallbacksPlusMiddleware(inner) + m.OnRecvPacket(sdk.Context{}, "ics20-1", pkt, sdk.AccAddress("relayer")) + + require.NotNil(t, inner.received) + if !spec.expRewrite { + assert.Equal(t, transferData.GetBytes(), inner.received) + return + } + var gotData transfertypes.FungibleTokenPacketData + require.NoError(t, json.Unmarshal(inner.received, &gotData)) + assert.Equal(t, testIntermediateBech32(t, "channel-1", "cosmos1sender"), gotData.Receiver) + assert.Equal(t, "cosmos1sender", gotData.Sender) + assert.Equal(t, spec.memo, gotData.Memo) + }) + } +} + +func TestIBCV2CallbacksPlusMiddleware(t *testing.T) { + calldataHex := hex.EncodeToString([]byte(`{"swap":{}}`)) + calldataMemo := mustMarshalJSON(t, map[string]any{ + "dest_callback": map[string]any{"address": "cosmos1contract", "calldata": calldataHex}, + }) + payloadValue := transfertypes.NewFungibleTokenPacketData("uosmo", "100", "cosmos1sender", "cosmos1receiver", calldataMemo).GetBytes() + + specs := map[string]struct { + payload channeltypesv2.Payload + expRewrite bool + }{ + "transfer port with dest_callback.calldata rewrites receiver": { + payload: channeltypesv2.Payload{ + SourcePort: transfertypes.PortID, + DestinationPort: transfertypes.PortID, + Version: "ics20-1", + Encoding: transfertypes.EncodingJSON, + Value: payloadValue, + }, + expRewrite: true, + }, + "non-transfer port passes through unchanged": { + payload: channeltypesv2.Payload{ + SourcePort: "different-port", + DestinationPort: "different-port", + Version: "v1", + Encoding: transfertypes.EncodingJSON, + Value: payloadValue, + }, + }, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + origValue := append([]byte(nil), spec.payload.Value...) + var gotRecv bool + var gotPayload channeltypesv2.Payload + inner := mockv2.NewIBCModule() + inner.IBCApp.OnRecvPacket = func(_ sdk.Context, _, _ string, _ uint64, payload channeltypesv2.Payload, _ sdk.AccAddress) channeltypesv2.RecvPacketResult { + gotRecv = true + gotPayload = payload + return channeltypesv2.RecvPacketResult{Status: channeltypesv2.PacketStatus_Success, Acknowledgement: []byte{1}} + } + m := NewIBCV2CallbacksPlusMiddleware(inner) + _ = m.OnRecvPacket(sdk.Context{}, "client-0", "client-1", 1, spec.payload, sdk.AccAddress("relayer")) + + require.True(t, gotRecv) + if !spec.expRewrite { + assert.Equal(t, origValue, gotPayload.Value) + return + } + var gotData transfertypes.FungibleTokenPacketData + require.NoError(t, json.Unmarshal(gotPayload.Value, &gotData)) + assert.Equal(t, testIntermediateBech32(t, "client-1", "cosmos1sender"), gotData.Receiver) + }) + } +} From a11867b14faba807dcf8176b21a28179756f5db9 Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Mon, 15 Jun 2026 14:29:20 +0200 Subject: [PATCH 9/9] fix lint --- x/wasm/ibc.go | 3 +-- x/wasm/ibc_callbacks_plus_middleware_test.go | 27 ++++++++------------ x/wasm/ibc_dedup_middleware.go | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/x/wasm/ibc.go b/x/wasm/ibc.go index 3cd46d8f6d..64931bc670 100644 --- a/x/wasm/ibc.go +++ b/x/wasm/ibc.go @@ -4,8 +4,6 @@ import ( "encoding/json" "math" - sdkmath "cosmossdk.io/math" - wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types" callbackstypes "github.com/cosmos/ibc-go/v10/modules/apps/callbacks/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -15,6 +13,7 @@ import ( ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/wasm/ibc_callbacks_plus_middleware_test.go b/x/wasm/ibc_callbacks_plus_middleware_test.go index 7ad03e5f3c..b2c1720b3d 100644 --- a/x/wasm/ibc_callbacks_plus_middleware_test.go +++ b/x/wasm/ibc_callbacks_plus_middleware_test.go @@ -56,7 +56,10 @@ func TestIBCReceivePacketCallback(t *testing.T) { "calldata": hex.EncodeToString(contractMsg), }, }) - intermediate := testIntermediateAddr(t, "channel-1", "cosmos1sender") + intermediateBech32, err := DeriveIntermediateSender("channel-1", "cosmos1sender", sdk.GetConfig().GetBech32AccountAddrPrefix()) + require.NoError(t, err) + intermediate, err := sdk.AccAddressFromBech32(intermediateBech32) + require.NoError(t, err) ibcDenom := transfertypes.Denom{ Base: "uosmo", Trace: []transfertypes.Hop{transfertypes.NewHop("transfer", "channel-1")}, @@ -227,20 +230,6 @@ func (r *recordingIBCModule) UnmarshalPacketData(_ sdk.Context, _, _ string, _ [ return nil, "", nil } -func testIntermediateBech32(t *testing.T, channel, sender string) string { - t.Helper() - s, err := DeriveIntermediateSender(channel, sender, sdk.GetConfig().GetBech32AccountAddrPrefix()) - require.NoError(t, err) - return s -} - -func testIntermediateAddr(t *testing.T, channel, sender string) sdk.AccAddress { - t.Helper() - a, err := sdk.AccAddressFromBech32(testIntermediateBech32(t, channel, sender)) - require.NoError(t, err) - return a -} - func TestIBCV1CallbacksPlusMiddleware(t *testing.T) { calldataHex := hex.EncodeToString([]byte(`{"swap":{}}`)) @@ -283,7 +272,9 @@ func TestIBCV1CallbacksPlusMiddleware(t *testing.T) { } var gotData transfertypes.FungibleTokenPacketData require.NoError(t, json.Unmarshal(inner.received, &gotData)) - assert.Equal(t, testIntermediateBech32(t, "channel-1", "cosmos1sender"), gotData.Receiver) + wantReceiver, err := DeriveIntermediateSender("channel-1", "cosmos1sender", sdk.GetConfig().GetBech32AccountAddrPrefix()) + require.NoError(t, err) + assert.Equal(t, wantReceiver, gotData.Receiver) assert.Equal(t, "cosmos1sender", gotData.Sender) assert.Equal(t, spec.memo, gotData.Memo) }) @@ -343,7 +334,9 @@ func TestIBCV2CallbacksPlusMiddleware(t *testing.T) { } var gotData transfertypes.FungibleTokenPacketData require.NoError(t, json.Unmarshal(gotPayload.Value, &gotData)) - assert.Equal(t, testIntermediateBech32(t, "client-1", "cosmos1sender"), gotData.Receiver) + wantReceiver, err := DeriveIntermediateSender("client-1", "cosmos1sender", sdk.GetConfig().GetBech32AccountAddrPrefix()) + require.NoError(t, err) + assert.Equal(t, wantReceiver, gotData.Receiver) }) } } diff --git a/x/wasm/ibc_dedup_middleware.go b/x/wasm/ibc_dedup_middleware.go index ffeb6540f7..4fa0499df4 100644 --- a/x/wasm/ibc_dedup_middleware.go +++ b/x/wasm/ibc_dedup_middleware.go @@ -61,7 +61,7 @@ func (m *IBCDedupMiddleware) SetICS4Wrapper(wrapper porttypes.ICS4Wrapper) { m.ICS4Wrapper = wrapper } -func (m *IBCDedupMiddleware) UnmarshalPacketData(ctx sdk.Context, portID string, channelID string, bz []byte) (any, string, error) { +func (m *IBCDedupMiddleware) UnmarshalPacketData(ctx sdk.Context, portID, channelID string, bz []byte) (any, string, error) { if unmarshaler, ok := m.IBCModule.(porttypes.PacketDataUnmarshaler); ok { return unmarshaler.UnmarshalPacketData(ctx, portID, channelID, bz) }