Channel lifecycle

A channel is a connection between two IBC ports that allows them to send packets to each other. In this section, we will cover how to establish a channel and how to close it. Since a channel is a connection between two ports, it can connect two chain modules, two contracts, or a module and a contract.

💡

For the sake of readability, we will assume that both ends of the channel are contracts in the following explanation. For more general information about the channel lifecycle, see the ICS 004 specification (opens in a new tab) or IBC channel docs (opens in a new tab).

Each channel also has an order that can be either Ordered or Unordered. This is encoded in the IbcOrder (opens in a new tab) enum. In an ordered channel, packets must be processed by the receiving chain in the order in which they were sent. In an unordered channel, packets are processed in the order they arrive.

Establishing a channel

To send packets between two chains, you need to establish a channel between them. This process involves two calls per chain. Once the channel is established, you can start sending packets through it, which we will cover in the next section. To initiate the channel creation, you can use a relayer binary such as hermes (opens in a new tab) to perform the handshake between the two endpoints. Returning an error from one of the calls will cause the channel handshake to fail.

In the following, we will refer to the two chains we want to connect as chain A and B. The handshake starts on chain A.

We will take a closer look at the handshake process, but here is a brief summary of the steps:

  1. ibc_channel_open on chain A with IbcChannelOpenMsg::OpenInit
  2. ibc_channel_open on chain B with IbcChannelOpenMsg::OpenTry
  3. ibc_channel_connect on chain A with IbcChannelConnectMsg::OpenAck
  4. ibc_channel_connect on chain B with IbcChannelConnectMsg::OpenConfirm

Channel open

When the channel creation is started by the relayer, the first call to the contract is made on chain A. This call is made to the ibc_channel_open entrypoint with the IbcChannelOpenMsg::OpenInit variant. Then the same entrypoint is called on chain B with the IbcChannelOpenMsg::OpenTry variant. See the following example and the IbcChannelOpenMsg (opens in a new tab) documentation.

ibc.rs
use cw_storage_plus::Item;
 
/// enforces ordering and versioning constraints
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
    deps: DepsMut,
    env: Env,
    msg: IbcChannelOpenMsg
) -> StdResult<IbcChannelOpenResponse> {
    let channel = msg.channel();
 
    // in this example, we only allow a single channel per contract instance
    // you can do more complex checks here
    ensure!(!CHANNEL.exists(deps.storage), StdError::generic_err("channel already exists"));
 
    // we should check if the channel is what we expect (e.g. the order)
    if channel.order != IbcOrder::Ordered {
        return Err(StdError::generic_err("only ordered channels are supported"));
    }
 
    // the OpenTry variant (on chain B) also has the counterparty version
    // we should check if it is what we expect
    if let Some(counter_version) = msg.counterparty_version() {
        if counter_version != IBC_APP_VERSION {
            return Err(StdError::generic_err(format!(
                "Counterparty version must be `{IBC_APP_VERSION}`"
            )));
        }
    }
 
    // now, we save the channel ID to storage, so we can use it later
    // this also prevents any further channel openings
    CHANNEL.save(deps.storage, &ChannelInfo {
        channel_id: channel.endpoint.channel_id.clone(),
        finalized: false,
    })?;
 
    // return the channel version we support
    Ok(Some(Ibc3ChannelOpenResponse {
        version: IBC_APP_VERSION.to_string(),
    }))
}
 
#[cw_serde]
struct ChannelInfo {
    channel_id: String,
    /// whether the channel is completely set up
    finalized: bool,
}
 
const CHANNEL: Item<ChannelInfo> = Item::new("channel");
const IBC_APP_VERSION: &str = "my-protocol-v1";

In the example above, we return the same version we expect from the counterparty, but you can return a different version if the counterparty accepts it. The version is used to ensure that both chains are running the protocol that the other one expects. You can also return None if you just want to accept the counterparty version.

Permissions

Opening a channel is generally a permissionless process, so make sure to keep that in mind when implementing the entrypoints. In the examples above, we only allow a single channel per contract instance and always make sure not to overwrite an existing channel. Note that we already save the channel in ibc_channel_open. This causes overlapping channel openings to fail the channel handshake. The drawback is that the contract can only connect to that one channel, so if the handshake fails, the contract cannot connect to another channel.

You can add additional checks to ensure that the channel is connecting to the correct counterparty or use a map to keep track of multiple channels connecting to different counterparties.

You can also be more restrictive and only allow the contract itself to initiate the channel creation handshake. This can be done by adding a state item to the contract that is only set to true before the contract sends the message to initiate the handshake and immediately set to false in the ibc_channel_open entrypoint.

Advanced example: Contract initiating the handshake on its own
💡

The following example requires the cosmos-sdk-proto crate that provides protobuf types for Cosmos SDK messages.

ibc.rs
use cosmos_sdk_proto::ibc::core::channel::v1::{
    Channel, Counterparty, MsgChannelOpenInit, Order, State,
};
use cosmos_sdk_proto::traits::Message;
use cw_storage_plus::Item;
 
/// Creates a new Cosmos SDK message that initiates the channel handshake
pub fn new_msg_channel_open_init(
    deps: Deps,
    env: &Env,
    connection_id: String,
    counterparty_port_id: String,
) -> StdResult<MsgChannelOpenInit> {
    // retrieve the contract's IBC port
    let port_id = deps
        .querier
        .query_wasm_contract_info(&env.contract.address)?
        .ibc_port
        .unwrap(); // this is never `None` for contracts with all IBC entrypoints
 
    // prepare the channel open message
    Ok(MsgChannelOpenInit {
        port_id,
        channel: Some(Channel {
            state: State::Init.into(),
            ordering: Order::Ordered.into(),
            counterparty: Some(Counterparty {
                port_id: counterparty_port_id,
                channel_id: String::new(),
            }),
            connection_hops: vec![connection_id],
            version: IBC_APP_VERSION.to_string(),
            upgrade_sequence: 0,
        }),
        signer: env.contract.address.to_string(),
    })
}
 
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_open(
    deps: DepsMut,
    _env: Env,
    msg: IbcChannelOpenMsg,
) -> StdResult<IbcChannelOpenResponse> {
    match msg {
        IbcChannelOpenMsg::OpenInit { channel } => {
            // check if channel opening is allowed
            ensure!(
                ALLOW_CHANNEL_OPENING.load(deps.storage)?,
                StdError::generic_err("channel opening is disabled")
            );
            // disable channel opening again
            ALLOW_CHANNEL_OPENING.save(deps.storage, &false)?;
 
            // ...
        }
        IbcChannelOpenMsg::OpenTry { .. } => { /* ... */ }
    }
 
    Ok(None)
}
 
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    // ...
 
    // prepare the channel open message
    let channel_open_init = new_msg_channel_open_init(
        deps.as_ref(),
        &env,
        msg.connection_id,
        msg.counterparty_port_id,
    )?;
 
    // enable channel opening
    ALLOW_CHANNEL_OPENING.save(deps.storage, &true)?;
 
    // send an any message to initiate the channel opening
    // Note that you need to use the deprecated `CosmosMsg::Stargate` variant if you need to
    // support pre-2.0 chains
    Ok(Response::new().add_message(AnyMsg {
        type_url: "/ibc.core.channel.v1.MsgChannelOpenInit".into(),
        value: channel_open_init.encode_to_vec().into(),
    }))
}
 
pub const ALLOW_CHANNEL_OPENING: Item<bool> = Item::new("allow_opening");
const IBC_APP_VERSION: &str = "my-protocol-v1";

As you can see, the contract itself sends the message to initialize the channel handshake in this example. Please note that the rest of the handshake still needs to be done by a relayer.

Channel connect

After the OpenTry variant is called on chain B, the relayer calls the ibc_channel_connect entrypoint, first with the IbcChannelConnectMsg::OpenAck variant on chain A, then the IbcChannelConnectMsg::OpenConfirm variant on chain B. The full data this entrypoint receives can be seen in the IbcChannelConnectMsg (opens in a new tab) documentation. Here is more example code:

ibc.rs
use cw_storage_plus::Item;
 
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_connect(
    deps: DepsMut,
    env: Env,
    msg: IbcChannelConnectMsg,
) -> StdResult<IbcBasicResponse> {
    let channel = msg.channel();
 
    // in this example, we only allow a single channel per contract instance
    // you can do more complex checks here
    let mut channel_info = CHANNEL.load(deps.storage)?;
    ensure!(!channel_info.finalized, StdError::generic_err("channel already finalized"));
    debug_assert_eq!(channel_info.channel_id, channel.endpoint.channel_id, "channel ID mismatch");
 
    // at this point, we are finished setting up the channel and can mark it as finalized
    channel_info.finalized = true;
    CHANNEL.save(deps.storage, &channel_info)?;
 
    Ok(IbcBasicResponse::new())
}
 
#[cw_serde]
struct ChannelInfo {
    channel_id: String,
    /// whether the channel is completely set up
    finalized: bool,
}
 
const CHANNEL: Item<ChannelInfo> = Item::new("channel");

Closing a channel

Similarly to opening a channel, closing a channel involves a handshake process. However, this time the process only involves one call per chain. The relayer initiates the process by calling the ibc_channel_close entrypoint with the IbcChannelCloseMsg::CloseInit variant on chain A, followed by the same entrypoint with the IbcChannelCloseMsg::CloseConfirm on chain B. The full data can be seen in the IbcChannelCloseMsg (opens in a new tab) documentation. Here is an example:

ibc.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_channel_close(
    deps: DepsMut,
    env: Env,
    msg: IbcChannelCloseMsg,
) -> StdResult<IbcBasicResponse> {
    Err(StdError::generic_err("closing not allowed"))
}
⚠️

While closing channels is something that is possible in IBC, you should think carefully about whether you want to allow it in your protocol. In many cases, it might be better to keep the channel open and return an error when someone tries to close it.

In this entrypoint, you can handle the closing process as you see fit. This can involve cleaning up any storage related to the channel or simply returning an error to prevent the channel from closing.