Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -621,8 +622,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:
Expand All @@ -646,10 +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 := 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().
Expand All @@ -660,8 +666,16 @@ func NewWasmApp(
app.IBCKeeper.SetRouter(ibcRouter)

ibcRouterV2 := ibcapi.NewRouter()
transferV2Stack := ibcapi.IBCModule(wasm.NewIBCV2CallbacksPlusMiddleware(transferv2.NewIBCModule(app.TransferKeeper)))
transferV2Stack = ibccallbacksv2.NewIBCMiddleware(
transferV2Stack,
app.IBCKeeper.ChannelKeeperV2,
wasmStackIBCHandler,
app.IBCKeeper.ChannelKeeperV2,
wasm.DefaultMaxIBCCallbackGas,
)
ibcRouterV2 = ibcRouterV2.
AddRoute(ibctransfertypes.PortID, transferv2.NewIBCModule(app.TransferKeeper)).
AddRoute(ibctransfertypes.PortID, transferV2Stack).
AddPrefixRoute(wasmkeeper.PortIDPrefixV2, wasmkeeper.NewIBC2Handler(app.WasmKeeper))

app.IBCKeeper.SetRouterV2(ibcRouterV2)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
105 changes: 97 additions & 8 deletions x/wasm/ibc.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package wasm

import (
"encoding/json"
"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"
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"
sdkmath "cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"

Expand All @@ -37,10 +40,20 @@ type IBCHandler struct {
channelKeeper types.ChannelKeeper
transferKeeper types.ICS20TransferPortSource
appVersionGetter appVersionGetter
contractKeeper types.ContractOpsKeeper
}

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 NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, vg appVersionGetter, contractKeeper types.ContractOpsKeeper) IBCHandler {
return IBCHandler{
keeper: k,
channelKeeper: ck,
transferKeeper: tk,
appVersionGetter: vg,
contractKeeper: contractKeeper,
}
}

func (i IBCHandler) SetICS4Wrapper(_ porttypes.ICS4Wrapper) {
}

// OnChanOpenInit implements the IBCModule interface
Expand Down Expand Up @@ -331,15 +344,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(
Expand Down Expand Up @@ -447,13 +475,52 @@ 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 {
amountInt, ok := sdkmath.NewIntFromString(amount)
if !ok {
return errorsmod.Wrapf(types.ErrInvalid, "invalid token amount: %s", amount)
}
funds := sdk.NewCoins(sdk.NewCoin(denom, amountInt))
// 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")
}
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,
},
},
}
Expand Down Expand Up @@ -533,3 +600,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
}
106 changes: 106 additions & 0 deletions x/wasm/ibc_callbacks_plus_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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"
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"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
)

// Verbatim from https://github.com/cosmos/ibc-apps/blob/main/modules/ibc-hooks/types/keys.go
const SenderPrefix = "ibc-wasm-hook-intermediary"

// 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)
}

// 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
}
pd.Receiver = intermediate
out, err := json.Marshal(pd)
if err != nil {
return data
}
return out
}

// 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
}

// IBCV1CallbacksPlusMiddleware rewrites the recv packet's Receiver to the
// intermediate sender when memo carries dest_callback.calldata.
type IBCV1CallbacksPlusMiddleware struct {
callbackstypes.CallbacksCompatibleModule
}

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}
}

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)
}

// IBCV2CallbacksPlusMiddleware rewrites the recv packet's Receiver to the
// intermediate sender when memo carries dest_callback.calldata.
type IBCV2CallbacksPlusMiddleware struct {
callbackstypes.CallbacksCompatibleModuleV2
}

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 &IBCV2CallbacksPlusMiddleware{CallbacksCompatibleModuleV2: compat}
}

func (m *IBCV2CallbacksPlusMiddleware) OnRecvPacket(
ctx sdk.Context,
sourceClient string,
destinationClient string,
sequence uint64,
payload channeltypesv2.Payload,
relayer sdk.AccAddress,
) channeltypesv2.RecvPacketResult {
if payload.SourcePort == transfertypes.PortID && payload.DestinationPort == transfertypes.PortID {
payload.Value = rewriteReceiverForCalldata(payload.Value, destinationClient)
}
return m.CallbacksCompatibleModuleV2.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer)
}
Loading
Loading