CosmWasm and IBC
Usecases
The IBC protocol provides a permissionless way for relaying data packets between blockchains, and combining IBC with CosmWasm enables powerful use cases, such as:
Cross-chain asset transfer
IBC allows for 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 multiple blockchain networks.
Interoperable infrastructure
IBC and CosmWasm together create a highly interoperable infrastructure that enables seamless communication and exchange of data between different blockchain networks. This helps to promote cross-chain collaboration and innovation, as well as enhance the security and scalability of the overall ecosystem.
Interoperable infrastructure
IBC enables multiple blockchains to participate in a shared governance system. CosmWasm allows developers to create smart contracts that facilitate multi-chain governance, allowing users to participate in decision-making processes that affect multiple blockchain networks simultaneously.
Using IBC with CosmWasm
ICS standards
You can use ICS20 and ICS721, as they are established IBC standards and are currently implemented in CosmWasm.
Anyways, you can also implement your own ICS protocols inside the contract (e.g. cw20-ics20)
IBC channels
You must establish an IBC channel between the two contracts in order to connect them. In this section, we name the chains connected by the IBC channel as chain A and chain B.
The IBC channel establishment process uses a four way handshake:
OpenInit lets chain A declare its information to chain B, and lets chain A ask chain B to verify that information.
OpenTry lets chain B respond chain A with its information (that needs to be verified by chain A).
OpenAck lets chain A open the IBC channel and send the acknowledgment to chain B.
OpenConfirm lets chain B receive the acknowledgment and open che channel from its side.
Entry Points
In order to execute the IBC handshake, you would need to implement six entry points in your smart contract. These entry points are necessary to enable IBC communication, and are related to the four steps handshake process:
ibc_channel_open is used to handle the MsgChannelOpenInit and MsgChannelOpenTry steps of the channel handshake.
ibc_channel_connect is used to handle the MsgChannelOpenAck and MsgChannelOpenConfirm steps of the channel handshake.
ibc_channel_close is used to handle channel closure.
ibc_packet_receive is used to handle MsgRecvPacket.
ibc_packet_ack is used to handle MsgAcknowledgement.
ibc_packet_timeout is used to handle MsgTimeout.
IBC counter example
As an example, let's look into how to implement a simple IBC counter smart contract with CosmWasm. You can find the code for this example in this Github repository.
When prompted on a chain, the IBC counter Smart Contract would:
- Send messages to its counterpart chain.
- Count the number of times messages have been received on both sides.
As IBC allows us to send packets from chain A to chain B and receive responses, the flow is:
- Let the contract/module in chain A request to send a packet.
- The packet is then relayed to chain B, which receives the packet and calculates an acknowledgment. The acknowledgment may contain a success result or an error message, which are bytes that are then interpreted by the receiving contract.
- The acknowledgment is then relayed back to chain A, completing the cycle.
- As the packet may never be delivered before the timeout, calling the timeout handler on chain A abort the process.
- In the case in which chain A sends the packet and receive a "timeout" message, no receive nor acknowledgement callbacks are ever executed.
In order to use this contract, you would need to:
- Store and instantiate the contract on two IBC-enabled chains (A and B).
- Use the active channels listed in the IBC channels, or run your own relayer.
- Execute the Increment {} method on one contract to increment the send a message and increment the count on the other one.
- Use the GetCount { connection } query to determine the message count for a given connection.
Let's know have a closer look at some key parts of the code.
contract.rs
When the Increment message for a particular channel is executed on a contract instance, a response is created that contains an IbcMsg::SendPacket message with the Increment message to execute on the counterparty:
#[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())
// outbound IBC message, where packet is then received on other chain
.add_message(IbcMsg::SendPacket {
channel_id: channel,
data: to_binary(&IbcExecuteMsg::Increment {})?,
// default timeout of two minutes.
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(120)),
})),
}
}
/// called on IBC packet receive in other chain
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)
})
}
You can query the contract instance to retrieve the current value of the counter:
/// called on IBC packet receive in other chain
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
Here you would need to implement all the six entry points.
When the contract instance on the counterparty chain receives the Increment message, the ibc_packet_receive function is executed:
pub fn do_ibc_packet_receive(
deps: DepsMut,
_env: Env,
msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, ContractError> {
// The channel this packet is being relayed along on this chain.
let channel = msg.packet.dest.channel_id;
let msg: IbcExecuteMsg = from_binary(&msg.packet.data)?;
match msg {
IbcExecuteMsg::Increment {} => execute_increment(deps, channel),
}
}
Eventually the code tries to increment the counter in the contract instance's state and returns an IbcReceiveResponse that sets the acknowledgment:
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 simply a map with the channel ID as key and the counter for that channel as value:
use cw_storage_plus::Map;
/// (channel_id) -> count. Reset on channel closure.
pub const CONNECTION_COUNTS: Map<String, u32> = Map::new("connection_counts");
/// (channel_id) -> timeout_count. Reset on channel closure.
pub const TIMEOUT_COUNTS: Map<String, u32> = Map::new("timeout_count");
msg.rs
The msg.rs defines the messages to instantiate the contract, execute the increment message on the contract, send the increment message to the counterparty and query the contract In this case, the contract only sends one type of packet data (i.e. the Increment message):
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
// GetCount returns the current count as a json-encoded number
#[returns(crate::msg::GetCountResponse)]
GetCount {
// The ID of the LOCAL channel you'd like to query the count
// for.
channel: String,
},
// GetTimeoutCount returns the number of timeouts have occurred on
// the LOCAL channel `channel`.
#[returns(crate::msg::GetCountResponse)]
GetTimeoutCount { channel: String },
}
// We define a custom struct for each query response
#[cw_serde]
pub struct GetCountResponse {
pub count: u32,
}