Skip to main content

Build With IBC Callbacks Module

TL;DR: Trigger a CosmWasm contract on ZIGChain from an ICS-20 transfer on Osmosis testnet using a dest_callback memo—build the sample counter, send the transfer, relay the packet, and verify the counter update.

Overview

This tutorial shows how to trigger a CosmWasm contract on ZIGChain from an ICS-20 transfer sent on Osmosis testnet using a dest_callback memo. By the end, you will compile and deploy the sample counter contract, send a callback-enabled transfer, relay the packet, and verify the counter update on ZIGChain.

The source example is available in the public ZIGChain examples repository: Callbacks Module example (examples/integrations/callbacks-module).

Disclaimer

This tutorial demonstrates an example flow and sample contract behavior. Validate chain settings, gas values, permissions, and contract logic for your own environment before any production use.

Key Concepts

  • ICS-20: Standard IBC token transfer application used in Step 5.
  • dest_callback: JSON memo field naming the ZIGChain contract and gas budget for the post-receive callback.
  • ibc_destination_callback: CosmWasm entry point the sample counter implements after a successful transfer acknowledgement.
  • Hermes: IBC relayer used to create channels and clear packets (Steps 4 and 6).
  • CODE_ID: On-chain identifier assigned when wasm is stored (Step 3).
  • CONTRACT: Instantiated contract address referenced in the transfer memo (Steps 3 and 5).

Prerequisites

Before you begin, install:

  • zigchaind v3.0.0 or above (for ZIGChain queries and transactions)
  • osmosisd (for Osmosis testnet transfer transactions)
  • hermes v1.10+ (for channel setup and packet relaying)
  • Rust + wasm32-unknown-unknown target
  • Docker (for deterministic CosmWasm optimizer builds)
  • jq (for parsing JSON responses)

Install the wasm target once:

rustup target add wasm32-unknown-unknown

Step 1: Configure Environment

Set up environment variables for both chains and placeholders you will populate during deployment:

# ZIGChain testnet
export ZIG_CHAIN_ID="zig-test-2"
export ZIG_NODE="https://testnet-rpc.zigchain.com:443"
export ZIG_LCD="https://testnet-api.zigchain.com"
export ZIG_DENOM="uzig"
export ZIG_GAS_PRICES="0.0025${ZIG_DENOM}"
export ZIG_KEY="my-key"

# Osmosis testnet
export OSMO_CHAIN_ID="osmo-test-5"
export OSMO_NODE="https://rpc.testnet.osmosis.zone:443"
export OSMO_DENOM="uosmo"
export OSMO_GAS_PRICES="0.1${OSMO_DENOM}"
export OSMO_KEY="my-key"

# Filled later
export CODE_ID=""
export CONTRACT=""
export OSMO_CHANNEL=""
export ZIG_CHANNEL=""
export ZIG_RECEIVER=""

Create keys if needed:

zigchaind keys add "$ZIG_KEY"
osmosisd keys add "$OSMO_KEY"

Before deploying the contract on ZIGChain testnet, ensure your zig1... wallet is whitelisted for CosmWasm uploads. Follow CosmWasm Whitelisting first, then continue.

Fund both testnet wallets before continuing:

Verify balances:

zigchaind q bank balances "$(zigchaind keys show "$ZIG_KEY" -a)" --node "$ZIG_NODE"
osmosisd q bank balances "$(osmosisd keys show "$OSMO_KEY" -a)" --node "$OSMO_NODE"

Step 2: Download the Example and Build the Contract

2.1 Clone the repository

Option A — Full clone (recommended for first-time setup):

git clone https://github.com/ZIGChain/zigchain-examples.git
cd zigchain-examples/examples/integrations/callbacks-module

Option B — Sparse checkout (download only callbacks module):

git clone --depth 1 --filter=blob:none --sparse https://github.com/ZIGChain/zigchain-examples.git zigchain-examples
cd zigchain-examples
git sparse-checkout set examples/integrations/callbacks-module
cd examples/integrations/callbacks-module

2.2 Build optimized wasm with Docker

From zigchain-examples/examples/integrations/callbacks-module, compile the optimized wasm:

docker run --rm -v "$(pwd)/contract":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/optimizer:0.16.1

Expected artifacts:

  • contract/artifacts/counter.wasm
  • contract/artifacts/checksums.txt

This sample contract exposes ibc_destination_callback and updates a counter only when the packet acknowledgement confirms transfer success.


Step 3: Store and Instantiate on ZIGChain

3.1 Store wasm

TX=$(zigchaind tx wasm store contract/artifacts/counter.wasm \
--from "$ZIG_KEY" \
--chain-id "$ZIG_CHAIN_ID" --node "$ZIG_NODE" \
--gas auto --gas-adjustment 1.4 --gas-prices "$ZIG_GAS_PRICES" \
--output json --yes | jq -r '.txhash')

Extract CODE_ID:

export CODE_ID=$(zigchaind q tx "$TX" --node "$ZIG_NODE" --output json \
| jq -r '.events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "code_id=$CODE_ID"

3.2 Instantiate

ADMIN=$(zigchaind keys show "$ZIG_KEY" -a)

zigchaind tx wasm instantiate "$CODE_ID" '{}' \
--label "counter-callback-demo" \
--admin "$ADMIN" \
--from "$ZIG_KEY" \
--chain-id "$ZIG_CHAIN_ID" --node "$ZIG_NODE" \
--gas auto --gas-adjustment 1.4 --gas-prices "$ZIG_GAS_PRICES" \
--output json --yes

Get contract address:

export CONTRACT=$(zigchaind q wasm list-contract-by-code "$CODE_ID" \
--node "$ZIG_NODE" --output json | jq -r '.contracts[-1]')
echo "contract=$CONTRACT"

Quick query check:

zigchaind q wasm contract-state smart "$CONTRACT" '{"count":{}}' --node "$ZIG_NODE"

Step 4: Create or Reuse an Osmosis to ZIGChain Transfer Channel

If you do not already have an open transfer channel between osmo-test-5 and zig-test-2, create one with Hermes:

hermes create channel \
--a-chain osmo-test-5 --b-chain zig-test-2 \
--a-port transfer --b-port transfer \
--new-client-connection --yes

Save channel IDs from output:

  • Osmosis side -> OSMO_CHANNEL
  • ZIGChain side -> ZIG_CHANNEL

Set receiver on ZIGChain:

export ZIG_RECEIVER="$(zigchaind keys show "$ZIG_KEY" -a)"

If you are reusing an existing channel, enumerate open transfer channels on Osmosis and pick the one whose client-state chain id is zig-test-2.


Step 5: Send ICS-20 Transfer With dest_callback

5.1 Minimal callback (+1 counter increment)

MEMO=$(jq -nc --arg addr "$CONTRACT" \
'{dest_callback: {address: $addr, gas_limit: "5000000"}}')

osmosisd tx ibc-transfer transfer \
transfer "$OSMO_CHANNEL" "$ZIG_RECEIVER" \
"1000${OSMO_DENOM}" \
--memo "$MEMO" \
--from "$OSMO_KEY" \
--chain-id "$OSMO_CHAIN_ID" --node "$OSMO_NODE" \
--gas auto --gas-adjustment 1.4 --gas-prices "$OSMO_GAS_PRICES" \
--packet-timeout-timestamp $(( $(date +%s%N) + 600000000000 )) \
--yes

Memo format:

{
"dest_callback": {
"address": "<contract-address-on-zigchain>",
"gas_limit": "500000"
}
}

5.2 With custom payload (counter.bump_by)

MEMO=$(jq -nc \
--arg addr "$CONTRACT" \
--argjson bump 5 \
'{dest_callback: {address: $addr, gas_limit: "500000"},
counter: {bump_by: $bump}}')

Then submit the same osmosisd tx ibc-transfer transfer ... command with this memo. The middleware reads dest_callback, while the contract reads counter.bump_by from packet memo data.


Step 6: Relay the Packet

Relay pending packets on the channel:

hermes clear packets \
--chain osmo-test-5 \
--port transfer \
--channel "$OSMO_CHANNEL"

Look for successful RecvPacket handling in output.


Step 7: Verify Callback Execution

Check count:

zigchaind q wasm contract-state smart "$CONTRACT" '{"count":{}}' --node "$ZIG_NODE"

Check transfer metadata captured by the contract:

zigchaind q wasm contract-state smart "$CONTRACT" '{"last_transfer":{}}' \
--node "$ZIG_NODE" --output json | jq

Optional event-level verification:

zigchaind q txs --query "recv_packet.packet_dst_channel='${ZIG_CHANNEL}'" \
--node "$ZIG_NODE" --output json --limit 1 \
| jq '.txs[0].events[] | select(.type=="wasm") | .attributes'

Troubleshooting

Counter stays at 0 after the transfer confirms on Osmosis.

  1. Packet not relayed yet — re-run hermes clear packets (Step 6) and watch its output for Success: RecvPacket tied to your OSMO_CHANNEL.
  2. Callbacks middleware is not wired into ZIGChain's ICS-20 stack. On v3.0.0 or above testnets/binaries it is, but if you're targeting a custom build, confirm with the ZIGChain team.
  3. gas_limit too low — bump it in the memo and retry.
  4. Memo JSON malformed — the middleware falls back to a plain transfer (tokens arrive, callback does not). Re-emit with jq -nc as shown above rather than hand-crafting.

Missing export ibc_destination_callback in the RecvPacket events.

The contract was compiled without a top-level #[entry_point] fn ibc_destination_callback(...). On cosmwasm-std 2.x this is a dedicated wasm export, not a sudo variant. Rebuild with the entry point and re-store; the old code id cannot process callbacks.

Wasm contract requires unavailable capabilities: {"cosmwasm_3_0"}.

ZIGChain v3.0.0 or above runs wasmvm v2.2.x. Pin cosmwasm-std = "2.2" with features ["cosmwasm_2_2", "stargate"] and parse the ICS-20 packet from cb.packet.data directly (the .transfer convenience field only exists in 3.0).

insufficient fees.

Bump --gas-adjustment to 1.5 and/or set a higher --gas-prices.

Receiver address rejected.

Make sure ZIG_RECEIVER is a ZIGChain bech32 address (zig1...), not an Osmosis address.


Next Steps

  • Extend the contract to enforce auth on reset paths.
  • Add richer memo payload schemas for app-specific callback logic.
  • Integrate the same callback pattern into your cross-chain settlement workflows.

References