This site requires Javascript to be enabled.

CosmWasm and IBC

The Inter-Blockchain Communication (IBC) protocol provides a permissionless way for relaying data packets between blockchains. When combined with CosmWasm, this enables powerful use cases that extend the functionality and interoperability of smart contracts across multiple blockchain networks.

Use cases

Cross-Chain asset transfer

IBC facilitates the transfer of assets between different blockchain networks. With CosmWasm, developers can create smart contracts that support cross-chain asset transfers, allowing users to send and receive tokens across various blockchain ecosystems. This opens up new possibilities for decentralized finance (DeFi) applications, cross-chain exchanges, and multi-chain dApps.

Interoperable infrastructure

The combination of IBC and CosmWasm creates a highly interoperable infrastructure that allows seamless communication and data exchange between different blockchain networks. This promotes cross-chain collaboration and innovation, enhancing the security and scalability of the overall ecosystem. Developers can build applications that leverage the strengths of multiple chains, creating a more robust and interconnected blockchain environment.

Multi-Chain governance

IBC enables multiple blockchains to participate in shared governance systems. CosmWasm allows developers to create smart contracts that facilitate multi-chain governance, enabling users to participate in decision-making processes that affect multiple blockchain networks simultaneously. This can be particularly useful for decentralized autonomous organizations (DAOs) and cross-chain voting systems.

Using IBC with CosmWasm

Getting started

To get started, you need to enable the stargate feature of the cosmwasm-std crate. This will enable additional functionality that is not available on all chains, including IBC support.

cosmwasm-std = { version = "2.0.3", features = ["stargate"] }

Info

The naming "stargate" is somewhat confusing. It is a reference to the Cosmos SDK 0.40 upgrade with the same name. This upgrade introduced (among other things) IBC.


Basic concepts

In order to understand how IBC works, it is important to understand some basic concepts:

  • Port: Every instantiation of an ibc-enabled contract creates a unique port, similarly to how it creates a unique address. Native Cosmos SDK modules can also have one or multiple unique ports.
  • Channel: A connection between two ports on different blockchains that allows them to send packets to each other. Each port can have multiple channels.
  • Packet: A piece of binary data that is sent through a channel. It can time out if it is not delivered within a certain time frame.
  • Relayer: An off-chain service that is responsible for passing packets between blockchains. The relayer watches for new packet commitments on one chain and submits them to the other chain. This is the way in which your packets actually reach the other chain. Anyone can run a relayer.

We will go into more detail on how these concepts are related to CosmWasm in the later sections, but this should give you some basic terminology to start with. If you want to learn more about IBC, you can read the IBC specification or check out the IBC documentation.

ICS standards

CosmWasm supports established IBC standards such as ICS20 and ICS721, which enable token transfers and NFT transfers across chains, respectively. These standards are implemented in CosmWasm and can be leveraged to build cross-chain applications. Additionally, developers can implement custom ICS protocols within their contracts, such as cw20-ics20, which extends the CW20 token standard for cross-chain transfers. The following guide gives a walkthrough of enabling cross-chain CW20 token transfers with CW20-ICS20.

IBC channels

To connect two contracts on different blockchains via IBC, an IBC channel must be established between them. The process involves a four-step handshake between two chains, commonly referred to as chain A and chain B:

  1. OpenInit: Chain A declares its channel information to chain B and requests verification.
  2. OpenTry: Chain B responds to chain A with its channel information, which chain A must verify.
  3. OpenAck: Chain A finalizes the channel and sends an acknowledgment to chain B.
  4. OpenConfirm: Chain B receives the acknowledgment and finalizes the channel on its end.

You can find the list of Archway's IBC channels here.

Entry points

To enable IBC communication, smart contracts must implement the following six entry points, corresponding to the IBC handshake process and packet handling:

  1. ibc_channel_open: Handles the MsgChannelOpenInit and MsgChannelOpenTry steps.
  2. ibc_channel_connect: Handles the MsgChannelOpenAck and MsgChannelOpenConfirm steps.
  3. ibc_channel_close: Handles channel closure events.
  4. ibc_packet_receive: Handles incoming packets (MsgRecvPacket).
  5. ibc_packet_ack: Handles packet acknowledgments (MsgAcknowledgement).
  6. ibc_packet_timeout: Handles packet timeouts (MsgTimeout).

Three for the channel lifecycle:

#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_channel_open(    deps: DepsMut,    env: Env,    msg: IbcChannelOpenMsg) -> StdResult<IbcChannelOpenResponse> {    Ok(None)}#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_channel_connect(    deps: DepsMut,    env: Env,    msg: IbcChannelConnectMsg,) -> StdResult<IbcBasicResponse> {    Ok(IbcBasicResponse::new())}#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_channel_close(    deps: DepsMut,    env: Env,    msg: IbcChannelCloseMsg,) -> StdResult<IbcBasicResponse> {    Ok(IbcBasicResponse::new())}

And three for the packet lifecycle:

#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_packet_receive(    deps: DepsMut,    env: Env,    msg: IbcPacketReceiveMsg,) -> StdResult<IbcReceiveResponse> {    Ok(IbcReceiveResponse::new(b""))}#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_packet_ack(    deps: DepsMut,    env: Env,    msg: IbcPacketAckMsg,) -> StdResult<IbcBasicResponse> {    Ok(IbcBasicResponse::new())}#[cfg_attr(not(feature = "library"), entry_point)]pub fn ibc_packet_timeout(    _deps: DepsMut,    _env: Env,    _msg: IbcPacketTimeoutMsg,) -> StdResult<IbcBasicResponse> {    Ok(IbcBasicResponse::new())}

IBC counter example

To demonstrate the implementation of an IBC-enabled smart contract in CosmWasm, consider the IBC Counter example. This contract counts the number of messages sent and received across two connected chains. You can find the code for this example in the IBC Counter GitHub repository.

Contract execution flow

  1. Send a Packet: The contract on chain A sends a packet containing an increment message to chain B.
  2. Receive and Acknowledge: Chain B receives the packet, processes it, and sends back an acknowledgment to chain A.
  3. Handle Timeout: If the packet is not delivered before the timeout, the timeout handler on chain A aborts the process.

Key parts of the code

contract.rs

When the Increment message is executed on chain A, a response is created containing an IbcMsg::SendPacket message to be sent to chain B:

#[cfg_attr(not(feature = "library"), entry_point)]pub fn execute(    deps: DepsMut,    env: Env,    _info: MessageInfo,    msg: ExecuteMsg,) -> Result<Response, ContractError> {    match msg {        ExecuteMsg::Increment { channel } => Ok(Response::new()            .add_attribute("method", "execute_increment")            .add_attribute("channel", channel.clone())            .add_message(IbcMsg::SendPacket {                channel_id: channel,                data: to_binary(&IbcExecuteMsg::Increment {})?,                timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(120)),            })),    }}

The try_increment function increments the counter in the contract state when a packet is received:

pub fn try_increment(deps: DepsMut, channel: String) -> Result<u32, StdError> {    CONNECTION_COUNTS.update(deps.storage, channel, |count| -> StdResult<_> {        Ok(count.unwrap_or_default() + 1)    })}

ibc.rs

Implement the six IBC entry points, with ibc_packet_receive handling the incoming packets:

pub fn do_ibc_packet_receive(    deps: DepsMut,    _env: Env,    msg: IbcPacketReceiveMsg,) -> Result<IbcReceiveResponse, ContractError> {    let channel = msg.packet.dest.channel_id;    let msg: IbcExecuteMsg = from_binary(&msg.packet.data)?;    match msg {        IbcExecuteMsg::Increment {} => execute_increment(deps, channel),    }}fn execute_increment(deps: DepsMut, channel: String) -> Result<IbcReceiveResponse, ContractError> {    let count = try_increment(deps, channel)?;    Ok(IbcReceiveResponse::new()        .add_attribute("method", "execute_increment")        .add_attribute("count", count.to_string())        .set_ack(make_ack_success()))}

state.rs

The state of the contract is maintained in a map where the channel ID is the key, and the counter is the value:

use cw_storage_plus::Map;pub const CONNECTION_COUNTS: Map<String, u32> = Map::new("connection_counts");pub const TIMEOUT_COUNTS: Map<String, u32> = Map::new("timeout_count");

msg.rs

Defines the messages for contract instantiation, execution, and querying:

#[cw_serde]#[derive(QueryResponses)]pub enum QueryMsg {    #[returns(crate::msg::GetCountResponse)]    GetCount { channel: String },    #[returns(crate::msg::GetCountResponse)]    GetTimeoutCount { channel: String },}#[cw_serde]pub struct GetCountResponse {    pub count: u32,}