Add x/ibc-hooks to wormchain repo (#3270)

* Add x/ibc-hooks to wormchain repo

* Use transfer type's codec to unmarshal json
This commit is contained in:
Steve 2023-08-10 09:18:58 -05:00 committed by GitHub
parent 8e5807bdea
commit 41b8f7152b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1411 additions and 0 deletions

View File

@ -0,0 +1,185 @@
# IBC-hooks
## Wasm Hooks
The wasm hook is an IBC middleware which is used to allow ICS-20 token transfers to initiate contract calls.
This allows cross-chain contract calls, that involve token movement.
This is useful for a variety of usecases.
One of primary importance is cross-chain swaps, which is an extremely powerful primitive.
The mechanism enabling this is a `memo` field on every ICS20 transfer packet as of [IBC v3.4.0](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b).
Wasm hooks is an IBC middleware that parses an ICS20 transfer, and if the `memo` field is of a particular form, executes a wasm contract call. We now detail the `memo` format for `wasm` contract calls, and the execution guarantees provided.
### Cosmwasm Contract Execution Format
Before we dive into the IBC metadata format, we show the cosmwasm execute message format, so the reader has a sense of what are the fields we need to be setting in.
The cosmwasm `MsgExecuteContract` is defined [here](https://github.com/CosmWasm/wasmd/blob/4fe2fbc8f322efdaf187e2e5c99ce32fd1df06f0/x/wasm/types/tx.pb.go#L340-L349
) as the following type:
```go
type MsgExecuteContract struct {
// Sender is the that actor that signed the messages
Sender string
// Contract is the address of the smart contract
Contract string
// Msg json encoded message to be passed to the contract
Msg RawContractMessage
// Funds coins that are transferred to the contract on execution
Funds sdk.Coins
}
```
So we detail where we want to get each of these fields from:
* Sender: We cannot trust the sender of an IBC packet, the counterparty chain has full ability to lie about it.
We cannot risk this sender being confused for a particular user or module address on Osmosis.
So we replace the sender with an account to represent the sender prefixed by the channel and a wasm module prefix.
This is done by setting the sender to `Bech32(Hash("ibc-wasm-hook-intermediary" || channelID || sender))`, where the channelId is the channel id on the local chain.
* Contract: This field should be directly obtained from the ICS-20 packet metadata
* Msg: This field should be directly obtained from the ICS-20 packet metadata.
* Funds: This field is set to the amount of funds being sent over in the ICS 20 packet. One detail is that the denom in the packet is the counterparty chains representation of the denom, so we have to translate it to Osmosis' representation.
So our constructed cosmwasm message that we execute will look like:
```go
msg := MsgExecuteContract{
// Sender is the that actor that signed the messages
Sender: "osmo1-hash-of-channel-and-sender",
// Contract is the address of the smart contract
Contract: packet.data.memo["wasm"]["ContractAddress"],
// Msg json encoded message to be passed to the contract
Msg: packet.data.memo["wasm"]["Msg"],
// Funds coins that are transferred to the contract on execution
Funds: sdk.NewCoin{Denom: ibc.ConvertSenderDenomToLocalDenom(packet.data.Denom), Amount: packet.data.Amount}
```
### ICS20 packet structure
So given the details above, we propogate the implied ICS20 packet data structure.
ICS20 is JSON native, so we use JSON for the memo format.
```json
{
//... other ibc fields that we don't care about
"data":{
"denom": "denom on counterparty chain (e.g. uatom)", // will be transformed to the local denom (ibc/...)
"amount": "1000",
"sender": "addr on counterparty chain", // will be transformed
"receiver": "contract addr or blank",
"memo": {
"wasm": {
"contract": "osmo1contractAddr",
"msg": {
"raw_message_fields": "raw_message_data",
}
}
}
}
}
```
An ICS20 packet is formatted correctly for wasmhooks iff the following all hold:
* `memo` is not blank
* `memo` is valid JSON
* `memo` has at least one key, with value `"wasm"`
* `memo["wasm"]` has exactly two entries, `"contract"` and `"msg"`
* `memo["wasm"]["msg"]` is a valid JSON object
* `receiver == "" || receiver == memo["wasm"]["contract"]`
We consider an ICS20 packet as directed towards wasmhooks iff all of the following hold:
* `memo` is not blank
* `memo` is valid JSON
* `memo` has at least one key, with name `"wasm"`
If an ICS20 packet is not directed towards wasmhooks, wasmhooks doesn't do anything.
If an ICS20 packet is directed towards wasmhooks, and is formated incorrectly, then wasmhooks returns an error.
### Execution flow
Pre wasm hooks:
* Ensure the incoming IBC packet is cryptogaphically valid
* Ensure the incoming IBC packet is not timed out.
In Wasm hooks, pre packet execution:
* Ensure the packet is correctly formatted (as defined above)
* Edit the receiver to be the hardcoded IBC module account
In wasm hooks, post packet execution:
* Construct wasm message as defined before
* Execute wasm message
* if wasm message has error, return ErrAck
* otherwise continue through middleware
## Ack callbacks
A contract that sends an IBC transfer, may need to listen for the ACK from that packet. To allow
contracts to listen on the ack of specific packets, we provide Ack callbacks.
### Design
The sender of an IBC transfer packet may specify a callback for when the ack of that packet is received in the memo
field of the transfer packet.
Crucially, _only_ the IBC packet sender can set the callback.
### Use case
The crosschain swaps implementation sends an IBC transfer. If the transfer were to fail, we want to allow the sender
to be able to retrieve their funds (which would otherwise be stuck in the contract). To do this, we allow users to
retrieve the funds after the timeout has passed, but without the ack information, we cannot guarantee that the send
hasn't failed (i.e.: returned an error ack notifying that the receiving change didn't accept it)
### Implementation
#### Callback information in memo
For the callback to be processed, the transfer packet's memo should contain the following in its JSON:
`{"ibc_callback": "osmo1contractAddr"}`
The wasm hooks will keep the mapping from the packet's channel and sequence to the contract in storage. When an ack is
received, it will notify the specified contract via a sudo message.
#### Interface for receiving the Acks and Timeouts
The contract that awaits the callback should implement the following interface for a sudo message:
```rust
#[cw_serde]
pub enum IBCLifecycleComplete {
#[serde(rename = "ibc_ack")]
IBCAck {
/// The source channel (osmosis side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
/// String encoded version of the ack as seen by OnAcknowledgementPacket(..)
ack: String,
/// Weather an ack is a success of failure according to the transfer spec
success: bool,
},
#[serde(rename = "ibc_timeout")]
IBCTimeout {
/// The source channel (osmosis side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
},
}
/// Message type for `sudo` entry_point
#[cw_serde]
pub enum SudoMsg {
#[serde(rename = "ibc_lifecycle_complete")]
IBCLifecycleComplete(IBCLifecycleComplete),
}
```
# Testing strategy
See go tests.

View File

@ -0,0 +1,76 @@
package cli
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/spf13/cobra"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/keeper"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/types"
)
func indexRunCmd(cmd *cobra.Command, args []string) error {
usageTemplate := `Usage:{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}
{{if .HasAvailableSubCommands}}Available Commands:{{range .Commands}}{{if .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
cmd.SetUsageTemplate(usageTemplate)
return cmd.Help()
}
// GetQueryCmd returns the cli query commands for this module.
func GetQueryCmd() *cobra.Command {
cmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: indexRunCmd,
}
cmd.AddCommand(
GetCmdWasmSender(),
)
return cmd
}
// GetCmdPoolParams return pool params.
func GetCmdWasmSender() *cobra.Command {
cmd := &cobra.Command{
Use: "wasm-sender <channelID> <originalSender>",
Short: "Generate the local address for a wasm hooks sender",
Long: strings.TrimSpace(
fmt.Sprintf(`Generate the local address for a wasm hooks sender.
Example:
$ %s query ibc-hooks wasm-hooks-sender channel-42 juno12smx2wdlyttvyzvzg54y2vnqwq2qjatezqwqxu
`,
version.AppName,
),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
channelID := args[0]
originalSender := args[1]
// ToDo: Make this flexible as an arg
prefix := sdk.GetConfig().GetBech32AccountAddrPrefix()
senderBech32, err := keeper.DeriveIntermediateSender(channelID, originalSender, prefix)
if err != nil {
return err
}
fmt.Println(senderBech32)
return nil
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}

View File

@ -0,0 +1,144 @@
package ibc_hooks
import (
// external libraries
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
// ibc-go
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
)
type Hooks interface{}
type OnChanOpenInitOverrideHooks interface {
OnChanOpenInitOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string) (string, error)
}
type OnChanOpenInitBeforeHooks interface {
OnChanOpenInitBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string)
}
type OnChanOpenInitAfterHooks interface {
OnChanOpenInitAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string, finalVersion string, err error)
}
// OnChanOpenTry Hooks
type OnChanOpenTryOverrideHooks interface {
OnChanOpenTryOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string) (string, error)
}
type OnChanOpenTryBeforeHooks interface {
OnChanOpenTryBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string)
}
type OnChanOpenTryAfterHooks interface {
OnChanOpenTryAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string, version string, err error)
}
// OnChanOpenAck Hooks
type OnChanOpenAckOverrideHooks interface {
OnChanOpenAckOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) error
}
type OnChanOpenAckBeforeHooks interface {
OnChanOpenAckBeforeHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string)
}
type OnChanOpenAckAfterHooks interface {
OnChanOpenAckAfterHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string, err error)
}
// OnChanOpenConfirm Hooks
type OnChanOpenConfirmOverrideHooks interface {
OnChanOpenConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error
}
type OnChanOpenConfirmBeforeHooks interface {
OnChanOpenConfirmBeforeHook(ctx sdk.Context, portID, channelID string)
}
type OnChanOpenConfirmAfterHooks interface {
OnChanOpenConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error)
}
// OnChanCloseInit Hooks
type OnChanCloseInitOverrideHooks interface {
OnChanCloseInitOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error
}
type OnChanCloseInitBeforeHooks interface {
OnChanCloseInitBeforeHook(ctx sdk.Context, portID, channelID string)
}
type OnChanCloseInitAfterHooks interface {
OnChanCloseInitAfterHook(ctx sdk.Context, portID, channelID string, err error)
}
// OnChanCloseConfirm Hooks
type OnChanCloseConfirmOverrideHooks interface {
OnChanCloseConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error
}
type OnChanCloseConfirmBeforeHooks interface {
OnChanCloseConfirmBeforeHook(ctx sdk.Context, portID, channelID string)
}
type OnChanCloseConfirmAfterHooks interface {
OnChanCloseConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error)
}
// OnRecvPacket Hooks
type OnRecvPacketOverrideHooks interface {
OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement
}
type OnRecvPacketBeforeHooks interface {
OnRecvPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress)
}
type OnRecvPacketAfterHooks interface {
OnRecvPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, ack ibcexported.Acknowledgement)
}
// OnAcknowledgementPacket Hooks
type OnAcknowledgementPacketOverrideHooks interface {
OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error
}
type OnAcknowledgementPacketBeforeHooks interface {
OnAcknowledgementPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress)
}
type OnAcknowledgementPacketAfterHooks interface {
OnAcknowledgementPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress, err error)
}
// OnTimeoutPacket Hooks
type OnTimeoutPacketOverrideHooks interface {
OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error
}
type OnTimeoutPacketBeforeHooks interface {
OnTimeoutPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress)
}
type OnTimeoutPacketAfterHooks interface {
OnTimeoutPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, err error)
}
// SendPacket Hooks
type SendPacketOverrideHooks interface {
SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error
}
type SendPacketBeforeHooks interface {
SendPacketBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI)
}
type SendPacketAfterHooks interface {
SendPacketAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, err error)
}
// WriteAcknowledgement Hooks
type WriteAcknowledgementOverrideHooks interface {
WriteAcknowledgementOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error
}
type WriteAcknowledgementBeforeHooks interface {
WriteAcknowledgementBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement)
}
type WriteAcknowledgementAfterHooks interface {
WriteAcknowledgementAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement, err error)
}
// GetAppVersion Hooks
type GetAppVersionOverrideHooks interface {
GetAppVersionOverride(i ICS4Middleware, ctx sdk.Context, portID, channelID string) (string, bool)
}
type GetAppVersionBeforeHooks interface {
GetAppVersionBeforeHook(ctx sdk.Context, portID, channelID string)
}
type GetAppVersionAfterHooks interface {
GetAppVersionAfterHook(ctx sdk.Context, portID, channelID string, result string, success bool)
}

View File

@ -0,0 +1,257 @@
package ibc_hooks
import (
// external libraries
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
// ibc-go
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types"
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
)
var _ porttypes.Middleware = &IBCMiddleware{}
type IBCMiddleware struct {
App porttypes.IBCModule
ICS4Middleware *ICS4Middleware
}
func NewIBCMiddleware(app porttypes.IBCModule, ics4 *ICS4Middleware) IBCMiddleware {
return IBCMiddleware{
App: app,
ICS4Middleware: ics4,
}
}
// OnChanOpenInit implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanOpenInit(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID string,
channelID string,
channelCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
version string,
) (string, error) {
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitOverrideHooks); ok {
return hook.OnChanOpenInitOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitBeforeHooks); ok {
hook.OnChanOpenInitBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version)
}
finalVersion, err := im.App.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitAfterHooks); ok {
hook.OnChanOpenInitAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version, finalVersion, err)
}
return version, err
}
// OnChanOpenTry implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanOpenTry(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID,
channelID string,
channelCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
counterpartyVersion string,
) (string, error) {
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryOverrideHooks); ok {
return hook.OnChanOpenTryOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryBeforeHooks); ok {
hook.OnChanOpenTryBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion)
}
version, err := im.App.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryAfterHooks); ok {
hook.OnChanOpenTryAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion, version, err)
}
return version, err
}
// OnChanOpenAck implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanOpenAck(
ctx sdk.Context,
portID,
channelID string,
counterpartyChannelID string,
counterpartyVersion string,
) error {
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckOverrideHooks); ok {
return hook.OnChanOpenAckOverride(im, ctx, portID, channelID, counterpartyChannelID, counterpartyVersion)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckBeforeHooks); ok {
hook.OnChanOpenAckBeforeHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion)
}
err := im.App.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckAfterHooks); ok {
hook.OnChanOpenAckAfterHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion, err)
}
return err
}
// OnChanOpenConfirm implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanOpenConfirm(
ctx sdk.Context,
portID,
channelID string,
) error {
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmOverrideHooks); ok {
return hook.OnChanOpenConfirmOverride(im, ctx, portID, channelID)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmBeforeHooks); ok {
hook.OnChanOpenConfirmBeforeHook(ctx, portID, channelID)
}
err := im.App.OnChanOpenConfirm(ctx, portID, channelID)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmAfterHooks); ok {
hook.OnChanOpenConfirmAfterHook(ctx, portID, channelID, err)
}
return err
}
// OnChanCloseInit implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanCloseInit(
ctx sdk.Context,
portID,
channelID string,
) error {
// Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitOverrideHooks); ok {
return hook.OnChanCloseInitOverride(im, ctx, portID, channelID)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitBeforeHooks); ok {
hook.OnChanCloseInitBeforeHook(ctx, portID, channelID)
}
err := im.App.OnChanCloseInit(ctx, portID, channelID)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitAfterHooks); ok {
hook.OnChanCloseInitAfterHook(ctx, portID, channelID, err)
}
return err
}
// OnChanCloseConfirm implements the IBCMiddleware interface
func (im IBCMiddleware) OnChanCloseConfirm(
ctx sdk.Context,
portID,
channelID string,
) error {
// Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmOverrideHooks); ok {
return hook.OnChanCloseConfirmOverride(im, ctx, portID, channelID)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmBeforeHooks); ok {
hook.OnChanCloseConfirmBeforeHook(ctx, portID, channelID)
}
err := im.App.OnChanCloseConfirm(ctx, portID, channelID)
if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmAfterHooks); ok {
hook.OnChanCloseConfirmAfterHook(ctx, portID, channelID, err)
}
return err
}
// OnRecvPacket implements the IBCMiddleware interface
func (im IBCMiddleware) OnRecvPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) ibcexported.Acknowledgement {
if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketOverrideHooks); ok {
return hook.OnRecvPacketOverride(im, ctx, packet, relayer)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketBeforeHooks); ok {
hook.OnRecvPacketBeforeHook(ctx, packet, relayer)
}
ack := im.App.OnRecvPacket(ctx, packet, relayer)
if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketAfterHooks); ok {
hook.OnRecvPacketAfterHook(ctx, packet, relayer, ack)
}
return ack
}
// OnAcknowledgementPacket implements the IBCMiddleware interface
func (im IBCMiddleware) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
relayer sdk.AccAddress,
) error {
if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketOverrideHooks); ok {
return hook.OnAcknowledgementPacketOverride(im, ctx, packet, acknowledgement, relayer)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketBeforeHooks); ok {
hook.OnAcknowledgementPacketBeforeHook(ctx, packet, acknowledgement, relayer)
}
err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketAfterHooks); ok {
hook.OnAcknowledgementPacketAfterHook(ctx, packet, acknowledgement, relayer, err)
}
return err
}
// OnTimeoutPacket implements the IBCMiddleware interface
func (im IBCMiddleware) OnTimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) error {
if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketOverrideHooks); ok {
return hook.OnTimeoutPacketOverride(im, ctx, packet, relayer)
}
if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketBeforeHooks); ok {
hook.OnTimeoutPacketBeforeHook(ctx, packet, relayer)
}
err := im.App.OnTimeoutPacket(ctx, packet, relayer)
if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketAfterHooks); ok {
hook.OnTimeoutPacketAfterHook(ctx, packet, relayer, err)
}
return err
}
// SendPacket implements the ICS4 Wrapper interface
func (im IBCMiddleware) SendPacket(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
packet ibcexported.PacketI,
) error {
return im.ICS4Middleware.SendPacket(ctx, chanCap, packet)
}
// WriteAcknowledgement implements the ICS4 Wrapper interface
func (im IBCMiddleware) WriteAcknowledgement(
ctx sdk.Context,
chanCap *capabilitytypes.Capability,
packet ibcexported.PacketI,
ack ibcexported.Acknowledgement,
) error {
return im.ICS4Middleware.WriteAcknowledgement(ctx, chanCap, packet, ack)
}
func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) {
return im.ICS4Middleware.GetAppVersion(ctx, portID, channelID)
}

View File

@ -0,0 +1,77 @@
package ibc_hooks
import (
// external libraries
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
// ibc-go
porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types"
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
)
var _ porttypes.ICS4Wrapper = &ICS4Middleware{}
type ICS4Middleware struct {
channel porttypes.ICS4Wrapper
// Hooks
Hooks Hooks
}
func NewICS4Middleware(channel porttypes.ICS4Wrapper, hooks Hooks) ICS4Middleware {
return ICS4Middleware{
channel: channel,
Hooks: hooks,
}
}
func (i ICS4Middleware) SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error {
if hook, ok := i.Hooks.(SendPacketOverrideHooks); ok {
return hook.SendPacketOverride(i, ctx, channelCap, packet)
}
if hook, ok := i.Hooks.(SendPacketBeforeHooks); ok {
hook.SendPacketBeforeHook(ctx, channelCap, packet)
}
err := i.channel.SendPacket(ctx, channelCap, packet)
if hook, ok := i.Hooks.(SendPacketAfterHooks); ok {
hook.SendPacketAfterHook(ctx, channelCap, packet, err)
}
return err
}
func (i ICS4Middleware) WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error {
if hook, ok := i.Hooks.(WriteAcknowledgementOverrideHooks); ok {
return hook.WriteAcknowledgementOverride(i, ctx, chanCap, packet, ack)
}
if hook, ok := i.Hooks.(WriteAcknowledgementBeforeHooks); ok {
hook.WriteAcknowledgementBeforeHook(ctx, chanCap, packet, ack)
}
err := i.channel.WriteAcknowledgement(ctx, chanCap, packet, ack)
if hook, ok := i.Hooks.(WriteAcknowledgementAfterHooks); ok {
hook.WriteAcknowledgementAfterHook(ctx, chanCap, packet, ack, err)
}
return err
}
func (i ICS4Middleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) {
if hook, ok := i.Hooks.(GetAppVersionOverrideHooks); ok {
return hook.GetAppVersionOverride(i, ctx, portID, channelID)
}
if hook, ok := i.Hooks.(GetAppVersionBeforeHooks); ok {
hook.GetAppVersionBeforeHook(ctx, portID, channelID)
}
version, err := i.channel.GetAppVersion(ctx, portID, channelID)
if hook, ok := i.Hooks.(GetAppVersionAfterHooks); ok {
hook.GetAppVersionAfterHook(ctx, portID, channelID, version, err)
}
return version, err
}

View File

@ -0,0 +1,62 @@
package keeper
import (
"fmt"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/tendermint/tendermint/libs/log"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type (
Keeper struct {
storeKey sdk.StoreKey
}
)
// NewKeeper returns a new instance of the x/ibchooks keeper
func NewKeeper(
storeKey sdk.StoreKey,
) Keeper {
return Keeper{
storeKey: storeKey,
}
}
// Logger returns a logger for the x/tokenfactory module
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
func GetPacketKey(channel string, packetSequence uint64) []byte {
return []byte(fmt.Sprintf("%s::%d", channel, packetSequence))
}
// StorePacketCallback stores which contract will be listening for the ack or timeout of a packet
func (k Keeper) StorePacketCallback(ctx sdk.Context, channel string, packetSequence uint64, contract string) {
store := ctx.KVStore(k.storeKey)
store.Set(GetPacketKey(channel, packetSequence), []byte(contract))
}
// GetPacketCallback returns the bech32 addr of the contract that is expecting a callback from a packet
func (k Keeper) GetPacketCallback(ctx sdk.Context, channel string, packetSequence uint64) string {
store := ctx.KVStore(k.storeKey)
return string(store.Get(GetPacketKey(channel, packetSequence)))
}
// DeletePacketCallback deletes the callback from storage once it has been processed
func (k Keeper) DeletePacketCallback(ctx sdk.Context, channel string, packetSequence uint64) {
store := ctx.KVStore(k.storeKey)
store.Delete(GetPacketKey(channel, packetSequence))
}
func DeriveIntermediateSender(channel, originalSender, bech32Prefix string) (string, error) {
senderStr := fmt.Sprintf("%s/%s", channel, originalSender)
senderHash32 := address.Hash(types.SenderPrefix, []byte(senderStr))
sender := sdk.AccAddress(senderHash32[:])
return sdk.Bech32ifyAddressBytes(bech32Prefix, sender)
}

View File

@ -0,0 +1,138 @@
package ibc_hooks
import (
"encoding/json"
"fmt"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/client/cli"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/types"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic defines the basic application module used by the ibc-hooks module.
type AppModuleBasic struct{}
var _ module.AppModuleBasic = AppModuleBasic{}
// Name returns the ibc-hooks module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterLegacyAminoCodec registers the ibc-hooks module's types on the given LegacyAmino codec.
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {}
// RegisterInterfaces registers the module's interface types.
func (b AppModuleBasic) RegisterInterfaces(_ cdctypes.InterfaceRegistry) {}
// DefaultGenesis returns default genesis state as raw bytes for the
// module.
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
emptyString := "{}"
return []byte(emptyString)
}
// ValidateGenesis performs genesis state validation for the ibc-hooks module.
func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error {
return nil
}
// RegisterRESTRoutes registers the REST routes for the ibc-hooks module.
func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, rtr *mux.Router) {}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the ibc-hooks module.
func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {}
// GetTxCmd returns no root tx command for the ibc-hooks module.
func (AppModuleBasic) GetTxCmd() *cobra.Command { return nil }
// GetQueryCmd returns the root query command for the ibc-hooks module.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd()
}
// ___________________________________________________________________________
// AppModule implements an application module for the ibc-hooks module.
type AppModule struct {
AppModuleBasic
authKeeper authkeeper.AccountKeeper
}
// NewAppModule creates a new AppModule object.
func NewAppModule(ak authkeeper.AccountKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
authKeeper: ak,
}
}
// Name returns the ibc-hooks module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the ibc-hooks module invariants.
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route returns the message routing key for the ibc-hooks module.
func (AppModule) Route() sdk.Route { return sdk.Route{} }
// QuerierRoute returns the module's querier route name.
func (AppModule) QuerierRoute() string {
return ""
}
// LegacyQuerierHandler returns the x/ibc-hooks module's sdk.Querier.
func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier {
return func(sdk.Context, []string, abci.RequestQuery) ([]byte, error) {
return nil, fmt.Errorf("legacy querier not supported for the x/%s module", types.ModuleName)
}
}
// RegisterServices registers a gRPC query service to respond to the
// module-specific gRPC queries.
func (am AppModule) RegisterServices(cfg module.Configurator) {
}
// InitGenesis performs genesis initialization for the ibc-hooks module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
return json.RawMessage([]byte("{}"))
}
// BeginBlock returns the begin blocker for the ibc-hooks module.
func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
}
// EndBlock returns the end blocker for the ibc-hooks module. It returns no validator
// updates.
func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
// ConsensusVersion implements AppModule/ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return 1 }

View File

@ -0,0 +1,15 @@
package types
import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
var (
ErrBadMetadataFormatMsg = "wasm metadata not properly formatted for: '%v'. %s"
ErrBadExecutionMsg = "cannot execute contract: %v"
ErrMsgValidation = sdkerrors.Register("wasm-hooks", 2, "error in wasmhook message validation")
ErrMarshaling = sdkerrors.Register("wasm-hooks", 3, "cannot marshal the ICS20 packet")
ErrInvalidPacket = sdkerrors.Register("wasm-hooks", 4, "invalid packet data")
ErrBadResponse = sdkerrors.Register("wasm-hooks", 5, "cannot create response")
ErrWasmError = sdkerrors.Register("wasm-hooks", 6, "wasm error")
ErrBadSender = sdkerrors.Register("wasm-hooks", 7, "bad sender")
)

View File

@ -0,0 +1,8 @@
package types
const (
ModuleName = "ibchooks"
StoreKey = "hooks-for-ibc" // not using the module name because of collisions with key "ibc"
IBCCallbackKey = "ibc_callback"
SenderPrefix = "ibc-wasm-hook-intermediary"
)

View File

@ -0,0 +1,449 @@
package ibc_hooks
import (
"encoding/json"
"fmt"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/keeper"
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/types"
)
type ContractAck struct {
ContractResult []byte `json:"contract_result"`
IbcAck []byte `json:"ibc_ack"`
}
type WasmHooks struct {
ContractKeeper *wasmkeeper.PermissionedKeeper
ibcHooksKeeper *keeper.Keeper
bech32PrefixAccAddr string
}
func NewWasmHooks(ibcHooksKeeper *keeper.Keeper, contractKeeper *wasmkeeper.PermissionedKeeper, bech32PrefixAccAddr string) WasmHooks {
return WasmHooks{
ContractKeeper: contractKeeper,
ibcHooksKeeper: ibcHooksKeeper,
bech32PrefixAccAddr: bech32PrefixAccAddr,
}
}
func (h WasmHooks) ProperlyConfigured() bool {
return h.ContractKeeper != nil && h.ibcHooksKeeper != nil
}
func (h WasmHooks) OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement {
if !h.ProperlyConfigured() {
// Not configured
return im.App.OnRecvPacket(ctx, packet, relayer)
}
isIcs20, data := isIcs20Packet(packet)
if !isIcs20 {
return im.App.OnRecvPacket(ctx, packet, relayer)
}
// Validate the memo
isWasmRouted, contractAddr, msgBytes, err := ValidateAndParseMemo(data.GetMemo(), data.Receiver)
if !isWasmRouted {
return im.App.OnRecvPacket(ctx, packet, relayer)
}
if err != nil {
return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error())
}
if msgBytes == nil || contractAddr == nil { // This should never happen
return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation)
}
// Calculate the receiver / contract caller based on the packet's channel and sender
channel := packet.GetDestChannel()
sender := data.GetSender()
senderBech32, err := keeper.DeriveIntermediateSender(channel, sender, h.bech32PrefixAccAddr)
if err != nil {
return NewEmitErrorAcknowledgement(ctx, types.ErrBadSender, fmt.Sprintf("cannot convert sender address %s/%s to bech32: %s", channel, sender, err.Error()))
}
// The funds sent on this packet need to be transferred to the intermediary account for the sender.
// For this, we override the ICS20 packet's Receiver (essentially hijacking the funds to this new address)
// and execute the underlying OnRecvPacket() call (which should eventually land on the transfer app's
// relay.go and send the sunds to the intermediary account.
//
// If that succeeds, we make the contract call
data.Receiver = senderBech32
bz, err := json.Marshal(data)
if err != nil {
return NewEmitErrorAcknowledgement(ctx, types.ErrMarshaling, err.Error())
}
packet.Data = bz
// Execute the receive
ack := im.App.OnRecvPacket(ctx, packet, relayer)
if !ack.Success() {
return ack
}
amount, ok := sdk.NewIntFromString(data.GetAmount())
if !ok {
// This should never happen, as it should've been caught in the underlaying call to OnRecvPacket,
// but returning here for completeness
return NewEmitErrorAcknowledgement(ctx, types.ErrInvalidPacket, "Amount is not an int")
}
// The packet's denom is the denom in the sender chain. This needs to be converted to the local denom.
denom := MustExtractDenomFromPacketOnRecv(packet)
funds := sdk.NewCoins(sdk.NewCoin(denom, amount))
// Execute the contract
execMsg := wasmtypes.MsgExecuteContract{
Sender: senderBech32,
Contract: contractAddr.String(),
Msg: msgBytes,
Funds: funds,
}
response, err := h.execWasmMsg(ctx, &execMsg)
if err != nil {
return NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error())
}
fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()}
bz, err = json.Marshal(fullAck)
if err != nil {
return NewEmitErrorAcknowledgement(ctx, types.ErrBadResponse, err.Error())
}
return channeltypes.NewResultAcknowledgement(bz)
}
func (h WasmHooks) execWasmMsg(ctx sdk.Context, execMsg *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) {
if err := execMsg.ValidateBasic(); err != nil {
return nil, fmt.Errorf(types.ErrBadExecutionMsg, err.Error())
}
wasmMsgServer := wasmkeeper.NewMsgServerImpl(h.ContractKeeper)
return wasmMsgServer.ExecuteContract(sdk.WrapSDKContext(ctx), execMsg)
}
func isIcs20Packet(packet channeltypes.Packet) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) {
var data transfertypes.FungibleTokenPacketData
if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
return false, data
}
return true, data
}
// 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]
if !ok {
return false, jsonObject
}
return true, jsonObject
}
func ValidateAndParseMemo(memo string, receiver string) (isWasmRouted bool, contractAddr sdk.AccAddress, msgBytes []byte, err error) {
isWasmRouted, metadata := jsonStringHasKey(memo, "wasm")
if !isWasmRouted {
return isWasmRouted, sdk.AccAddress{}, nil, nil
}
wasmRaw := metadata["wasm"]
// Make sure the wasm key is a map. If it isn't, ignore this packet
wasm, ok := wasmRaw.(map[string]interface{})
if !ok {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, "wasm metadata is not a valid JSON map object")
}
// Get the contract
contract, ok := wasm["contract"].(string)
if !ok {
// The tokens will be returned
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["contract"]`)
}
contractAddr, err = sdk.AccAddressFromBech32(contract)
if err != nil {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] is not a valid bech32 address`)
}
// The contract and the receiver should be the same for the packet to be valid
if contract != receiver {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] should be the same as the receiver of the packet`)
}
// Ensure the message key is provided
if wasm["msg"] == nil {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["msg"]`)
}
// Make sure the msg key is a map. If it isn't, return an error
_, ok = wasm["msg"].(map[string]interface{})
if !ok {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["msg"] is not a map object`)
}
// Get the message string by serializing the map
msgBytes, err = json.Marshal(wasm["msg"])
if err != nil {
// The tokens will be returned
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, err.Error())
}
return isWasmRouted, contractAddr, msgBytes, nil
}
func (h WasmHooks) SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error {
concretePacket, ok := packet.(channeltypes.Packet)
if !ok {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}
isIcs20, data := isIcs20Packet(concretePacket)
if !isIcs20 {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}
isCallbackRouted, metadata := jsonStringHasKey(data.GetMemo(), types.IBCCallbackKey)
if !isCallbackRouted {
return i.channel.SendPacket(ctx, chanCap, packet) // continue
}
// We remove the callback metadata from the memo as it has already been processed.
// If the only available key in the memo is the callback, we should remove the memo
// from the data completely so the packet is sent without it.
// This way receiver chains that are on old versions of IBC will be able to process the packet
callbackRaw := metadata[types.IBCCallbackKey] // This will be used later.
delete(metadata, types.IBCCallbackKey)
bzMetadata, err := json.Marshal(metadata)
if err != nil {
return sdkerrors.Wrap(err, "Send packet with callback error")
}
stringMetadata := string(bzMetadata)
if stringMetadata == "{}" {
data.Memo = ""
} else {
data.Memo = stringMetadata
}
dataBytes, err := json.Marshal(data)
if err != nil {
return sdkerrors.Wrap(err, "Send packet with callback error")
}
packetWithoutCallbackMemo := channeltypes.Packet{
Sequence: concretePacket.Sequence,
SourcePort: concretePacket.SourcePort,
SourceChannel: concretePacket.SourceChannel,
DestinationPort: concretePacket.DestinationPort,
DestinationChannel: concretePacket.DestinationChannel,
Data: dataBytes,
TimeoutTimestamp: concretePacket.TimeoutTimestamp,
TimeoutHeight: concretePacket.TimeoutHeight,
}
err = i.channel.SendPacket(ctx, chanCap, packetWithoutCallbackMemo)
if err != nil {
return err
}
// Make sure the callback contract is a string and a valid bech32 addr. If it isn't, ignore this packet
contract, ok := callbackRaw.(string)
if !ok {
return nil
}
_, err = sdk.AccAddressFromBech32(contract)
if err != nil {
return nil
}
h.ibcHooksKeeper.StorePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence(), contract)
return nil
}
func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error {
err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
if err != nil {
return err
}
if !h.ProperlyConfigured() {
// Not configured. Return from the underlying implementation
return nil
}
contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
if contract == "" {
// No callback configured
return nil
}
contractAddr, err := sdk.AccAddressFromBech32(contract)
if err != nil {
return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out
}
success := "false"
if !IsJsonAckError(acknowledgement) {
success = "true"
}
// Notify the sender that the ack has been received
ackAsJson, err := json.Marshal(acknowledgement)
if err != nil {
// If the ack is not a json object, error
return err
}
sudoMsg := []byte(fmt.Sprintf(
`{"ibc_lifecycle_complete": {"ibc_ack": {"channel": "%s", "sequence": %d, "ack": %s, "success": %s}}}`,
packet.SourceChannel, packet.Sequence, ackAsJson, success))
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
if err != nil {
// error processing the callback
// ToDo: Open Question: Should we also delete the callback here?
return sdkerrors.Wrap(err, "Ack callback error")
}
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
return nil
}
func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
err := im.App.OnTimeoutPacket(ctx, packet, relayer)
if err != nil {
return err
}
if !h.ProperlyConfigured() {
// Not configured. Return from the underlying implementation
return nil
}
contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
if contract == "" {
// No callback configured
return nil
}
contractAddr, err := sdk.AccAddressFromBech32(contract)
if err != nil {
return sdkerrors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out
}
sudoMsg := []byte(fmt.Sprintf(
`{"ibc_lifecycle_complete": {"ibc_timeout": {"channel": "%s", "sequence": %d}}}`,
packet.SourceChannel, packet.Sequence))
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
if err != nil {
// error processing the callback. This could be because the contract doesn't implement the message type to
// process the callback. Retrying this will not help, so we can delete the callback from storage.
// Since the packet has timed out, we don't expect any other responses that may trigger the callback.
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
"ibc-timeout-callback-error",
sdk.NewAttribute("contract", contractAddr.String()),
sdk.NewAttribute("message", string(sudoMsg)),
sdk.NewAttribute("error", err.Error()),
),
})
}
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
return nil
}
// NewEmitErrorAcknowledgement creates a new error acknowledgement after having emitted an event with the
// details of the error.
func NewEmitErrorAcknowledgement(ctx sdk.Context, err error, errorContexts ...string) channeltypes.Acknowledgement {
errorType := "ibc-acknowledgement-error"
logger := ctx.Logger().With("module", errorType)
attributes := make([]sdk.Attribute, len(errorContexts)+1)
attributes[0] = sdk.NewAttribute("error", err.Error())
for i, s := range errorContexts {
attributes[i+1] = sdk.NewAttribute("error-context", s)
logger.Error(fmt.Sprintf("error-context: %v", s))
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
errorType,
attributes...,
),
})
return channeltypes.NewErrorAcknowledgement(err)
}
// IsJsonAckError checks an IBC acknowledgement to see if it's an error.
// This is a replacement for ack.Success() which is currently not working on some circumstances
func IsJsonAckError(acknowledgement []byte) bool {
var ackErr channeltypes.Acknowledgement_Error
if err := json.Unmarshal(acknowledgement, &ackErr); err == nil && len(ackErr.Error) > 0 {
return true
}
return false
}
// MustExtractDenomFromPacketOnRecv takes a packet with a valid ICS20 token data in the Data field and returns the
// denom as represented in the local chain.
// If the data cannot be unmarshalled this function will panic
func MustExtractDenomFromPacketOnRecv(packet ibcexported.PacketI) string {
var data transfertypes.FungibleTokenPacketData
if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
panic("unable to unmarshal ICS20 packet data")
}
var denom string
if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
// remove prefix added by sender chain
voucherPrefix := transfertypes.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
unprefixedDenom := data.Denom[len(voucherPrefix):]
// coin denomination used in sending from the escrow address
denom = unprefixedDenom
// The denomination used to send the coins is either the native denom or the hash of the path
// if the denomination is not native.
denomTrace := transfertypes.ParseDenomTrace(unprefixedDenom)
if denomTrace.Path != "" {
denom = denomTrace.IBCDenom()
}
} else {
prefixedDenom := transfertypes.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) + data.Denom
denom = transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom()
}
return denom
}