This module is forked from https://github.com/osmosis-labs/osmosis/tree/main/x/ibc-hooks
The IBC hooks module is an IBC middleware that enables ICS-20 token transfers to initiate contract calls. This functionality allows cross-chain contract calls that involve token movement. IBC hooks are useful for a variety of use cases, including cross-chain swaps, which are an extremely powerful primitive.
IBC hooks are made possible through the memo
field included in every ICS-20 transfer packet, as introduced in IBC v3.4.0.
The IBC hooks IBC middleware parses an ICS20 transfer, and if the memo
field is of a particular form, executes a Wasm contract call.
The following sections detail the memo
format for Wasm contract calls and the execution guarantees provided.
Before diving into the IBC metadata format, it's important to understand the Cosmwasm execute message format to get a sense of the specific fields that need to be set. Provided below is the CosmWasm MsgExecuteContract
format as defined in the Wasm module.
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
}
For use with IBC hooks, the message fields above can be derived from the following:
Sender
: IBC packet senders cannot be explicitly trusted, as they can be deceitful. Chains cannot risk the sender being confused with a particular local user or module address. To prevent this, thesender
is replaced with an account that represents the sender prefixed by the channel and a Wasm module prefix. This is done by setting the sender toBech32(Hash("ibc-wasm-hook-intermediary" || channelID || sender))
, where thechannelId
is the channel id on the local chain.Contract
: This field should be directly obtained from the ICS-20 packet metadataMsg
: 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. The denom in the packet must be specified as the counterparty chain's representation of the denom.
WARNING: Due to a bug in the packet forward middleware, we cannot trust the sender from chains that use PFM. Until that is fixed, we recommend chains to not trust the sender on contracts executed via IBC hooks.
The fully constructed execute message will look like the following:
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}
}
Given the details above, you can propagate the implied ICS-20 packet data structure. ICS20 is JSON native, so you can use JSON for the memo format.
{
//... 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 ICS-20 packet is formatted correctly for IBC hooks if all of the following are true:
- The
memo
is not blank. - The
memo
is valid JSON. - The
memo
has at least one key, with the value"wasm"
. - The
memo["wasm"]
has exactly two entries,"contract"
and"msg"
. - The
memo["wasm"]["msg"]
is a valid JSON object. - The
receiver == "" || receiver == memo["wasm"]["contract"]
.
An ICS-20 packet is directed toward IBC hooks if all of the following are true:
- The
memo
is not blank. - The
memo
is valid JSON. - The
memo
has at least one key, with the name"wasm"
.
If an ICS-20 packet is not directed towards IBC hooks, IBC hooks doesn't do anything. If an ICS-20 packet is directed towards IBC hooks, and is formatted incorrectly, then IBC hooks returns an error.
- Pre-IBC hooks:
- Ensure the incoming IBC packet is cryptographically valid.
- Ensure the incoming IBC packet is not timed out.
- In IBC hooks, pre-packet execution:
- Ensure the packet is correctly formatted (as defined above).
- Edit the receiver to be the hardcoded IBC module account.
- In IBC hooks, post packet execution:
- Construct a Wasm message as defined before.
- Execute the Wasm message.
- If the Wasm message has an error, return
ErrAck
. - Otherwise, continue through middleware.
A contract that sends an IBC transfer may need to listen for the ack
from that packet. Ack
callbacks allow
contracts to listen in on the ack
of specific packets.
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.
The cross-chain swaps implementation sends an IBC transfer. If the transfer were to fail, the sender should be able to retrieve their funds which would otherwise be stuck in the contract. To do this, users should be allowed to retrieve the funds after the timeout has passed. However, without the Ack
information, one cannot guarantee that the send hasn't failed (i.e.: returned an error ack notifying that the receiving chain didn't accept it).
For the callback to be processed, the transfer packet's memo
should contain the following in its JSON:
{"ibc_callback": "osmo1contractAddr"}
The IBC 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.
The contract that awaits the callback should implement the following interface for a sudo message:
#[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),
}
Follow these steps to install the IBC hooks module. The following lines are all added to app.go
- Import the following packages.
// import (
...
ibchooks "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7"
ibchookskeeper "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/keeper"
ibchookstypes "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types"
...
// )
- Add the module.
// var (
//DefaultNodeHome string
//ModuleBasics = module.NewBasicManager(
...
ibchooks.AppModuleBasic{},
...
// )
- Add the IBC hooks keeper.
// type App struct {
...
IBCHooksKeeper ibchookskeeper.Keeper
...
// }
- Initiate the keepers and helpers.
...
// 'ibc-hooks' module - depends on
// 1. 'auth'
// 2. 'bank'
// 3. 'distr'
app.keys[ibchookstypes.StoreKey] = storetypes.NewKVStoreKey(ibchookstypes.StoreKey)
app.IBCHooksKeeper = ibchookskeeper.NewKeeper(
app.keys[ibchookstypes.StoreKey],
)
app.Ics20WasmHooks = ibchooks.NewWasmHooks(&app.IBCHooksKeeper, nil, AccountAddressPrefix) // The contract keeper needs to be set later
// initialize the wasm keeper with
// wasmKeeper := wasm.NewKeeper( ... )
app.WasmKeeper = &wasmKeeper
// Pass the contract keeper to all the structs (generally ICS4Wrappers for ibc middlewares) that need it
app.ContractKeeper = wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper)
app.Ics20WasmHooks.ContractKeeper = app.ContractKeeper
app.HooksICS4Wrapper = ibchooks.NewICS4Middleware(
app.IBCKeeper.ChannelKeeper,
app.Ics20WasmHooks,
)
// Hooks Middleware
transferIBCModule := ibctransfer.NewIBCModule(app.TransferKeeper)
app.TransferStack = ibchooks.NewIBCMiddleware(&transferIBCModule, &app.HooksICS4Wrapper)
...
- Register the genesis, begin blocker and end block hooks.
...
app.ModuleManager.SetOrderBeginBlockers(
// upgrades should be run first
upgradetypes.ModuleName,
capabilitytypes.ModuleName,
minttypes.ModuleName,
consensustypes.ModuleName,
distrtypes.ModuleName,
slashingtypes.ModuleName,
evidencetypes.ModuleName,
stakingtypes.ModuleName,
authtypes.ModuleName,
banktypes.ModuleName,
govtypes.ModuleName,
crisistypes.ModuleName,
ibctransfertypes.ModuleName,
ibcexported.ModuleName,
icatypes.ModuleName,
ibcfeetypes.ModuleName,
genutiltypes.ModuleName,
authz.ModuleName,
feegrant.ModuleName,
group.ModuleName,
paramstypes.ModuleName,
vestingtypes.ModuleName,
nft.ModuleName,
ibchookstypes.ModuleName,
wasm.ModuleName,
// this line is used by starport scaffolding # stargate/app/beginBlockers
)
app.ModuleManager.SetOrderEndBlockers(
crisistypes.ModuleName,
govtypes.ModuleName,
stakingtypes.ModuleName,
consensustypes.ModuleName,
ibctransfertypes.ModuleName,
ibcexported.ModuleName,
icatypes.ModuleName,
capabilitytypes.ModuleName,
authtypes.ModuleName,
banktypes.ModuleName,
distrtypes.ModuleName,
slashingtypes.ModuleName,
minttypes.ModuleName,
genutiltypes.ModuleName,
evidencetypes.ModuleName,
authz.ModuleName,
feegrant.ModuleName,
group.ModuleName,
paramstypes.ModuleName,
upgradetypes.ModuleName,
vestingtypes.ModuleName,
nft.ModuleName,
ibcfeetypes.ModuleName,
ibchookstypes.ModuleName,
wasm.ModuleName,
// this line is used by starport scaffolding # stargate/app/endBlockers
)
// NOTE: The genutils module must occur after staking so that pools are
// properly initialized with tokens from genesis accounts.
// NOTE: Capability module must occur first so that it can initialize any capabilities
// so that other modules that want to create or claim capabilities afterwards in InitChain
// can do so safely.
app.ModuleManager.SetOrderInitGenesis(
capabilitytypes.ModuleName,
authtypes.ModuleName,
banktypes.ModuleName,
distrtypes.ModuleName,
stakingtypes.ModuleName,
slashingtypes.ModuleName,
consensustypes.ModuleName,
govtypes.ModuleName,
minttypes.ModuleName,
crisistypes.ModuleName,
genutiltypes.ModuleName,
ibctransfertypes.ModuleName,
ibcexported.ModuleName,
icatypes.ModuleName,
evidencetypes.ModuleName,
authz.ModuleName,
feegrant.ModuleName,
group.ModuleName,
paramstypes.ModuleName,
upgradetypes.ModuleName,
vestingtypes.ModuleName,
nft.ModuleName,
ibcfeetypes.ModuleName,
ibchookstypes.ModuleName,
wasm.ModuleName,
// this line is used by starport scaffolding # stargate/app/initGenesis
)
...
Tests are included in the tests folder.