Build
Tutorials
Sui

ZetaChain enables Sui-based applications to interact directly with universal smart contracts deployed on ZetaChain. Using ZetaChain’s universal interoperability layer, Sui apps can:

  • Deposit SUI and supported fungible tokens to ZetaChain
  • Make cross-chain calls to universal contracts
  • Receive cross-chain calls and token transfers from universal contracts

In this tutorial, you'll learn how to:

  • Set up a local development environment with both ZetaChain and Sui
  • Deploy a universal contract on ZetaChain
  • Deposit SUI tokens from a Sui address to ZetaChain
  • Make a cross-chain call to trigger a function on a universal contract

By the end, you’ll understand how to transfer assets and trigger logic on ZetaChain from Sui, both from a client wallet and from a Sui smart contract.

Make sure the following tools are installed before you begin:

ToolPurpose
Sui CLI (opens in a new tab)Run a local Sui validator, manage addresses and objects, deploy contracts
Foundry (opens in a new tab)Encode ABI payloads for cross-chain calls using cast
Node.js (opens in a new tab)Run the ZetaChain CLI and JavaScript-based tooling
Yarn (opens in a new tab)Install and manage project dependencies
jq (opens in a new tab)Parse JSON output in shell scripts

Start by generating a new project using the ZetaChain CLI:

npx zetachain@latest new --project call
cd call

This sets up a ready-to-use example with Sui and ZetaChain contracts.

Install dependencies:

yarn

The project is now ready for local development and testing.

Start a local development environment with both ZetaChain and Sui running side by side:

yarn zetachain localnet start --chains sui

This command boots up:

  • A local ZetaChain instance
  • A local Sui validator
  • Pre-deployed gateway contracts on both networks

Leave this terminal open. Once started, you’ll see a table like this:

SUI
┌──────────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ (index)          │ Values                                                                           │
├──────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ gatewayPackageId │ '0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86'             │
│ gatewayObjectId  │ '0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497'             │
│ userMnemonic     │ 'grape subway rack mean march bubble carry avoid muffin consider thing street'   │
│ userAddress      │ '0x2fec3fafe08d2928a6b8d9a6a77590856c458d984ae090ccbd4177ac13729e65'             │
│ tokenUSDC        │ '6b0b8d1bbc40893a7f793f52c46aeea9db9f2f710c6a623c666bff712e26c94a::token::TOKEN' │
└──────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

💡 Keep this output handy. You’ll reference the gatewayObjectId, gatewayObjectId, and userMnemonic in later steps.

You’ll now deploy a universal smart contract to ZetaChain.

First, grab the local Gateway address and a funded private key from your localnet setup:

GATEWAY_ZETACHAIN=$(jq -r '.["31337"].contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json) && echo $GATEWAY_ZETACHAIN
PRIVATE_KEY=$(jq -r '.private_keys[0]' ~/.zetachain/localnet/anvil.json) && echo $PRIVATE_KEY

Compile the contract:

forge build

Then deploy the Universal contract:

UNIVERSAL=$(forge create Universal \
  --rpc-url http://localhost:8545 \
  --private-key $PRIVATE_KEY \
  --evm-version paris \
  --broadcast \
  --json \
  --constructor-args $GATEWAY_ZETACHAIN | jq -r .deployedTo) && echo $UNIVERSAL

Now that your universal contract is deployed on ZetaChain, you can deposit SUI tokens from the Sui Gateway contract to that contract address.

Use the following command to deposit tokens from Sui to your universal contract on ZetaChain:

npx zetachain sui deposit \
  --mnemonic "grape subway rack mean march bubble carry avoid muffin consider thing street" \
  --receiver $UNIVERSAL \
  --gateway-package 0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86 \
  --gateway-object 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
  --amount 0.001 \
  --chain-id 104

🔎 Replace --gateway-package and --gateway-object with the values printed in your localnet output table.

This command calls the deposit function on the Sui Gateway contract. ZetaChain observes the deposit event and propagates the deposit to ZetaChain by minting ZRC-20 SUI tokens and transferring them to your universal contract.

In this step, you’ll deposit SUI tokens from Sui and simultaneously trigger a function call on the universal contract deployed on ZetaChain.

npx zetachain sui deposit-and-call \
  --mnemonic "grape subway rack mean march bubble carry avoid muffin consider thing street" \
  --receiver $UNIVERSAL \
  --gateway-package 0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86 \
  --gateway-object 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
  --amount 0.001 \
  --chain-id 104 \
  --types string \
  --values hello

This command calls the deposit_and_call function on the Sui Gateway contract, transferring the specified SUI amount, encoding the payload ("hello") using the provided --types and --values, and triggering the onCall function on the universal contract deployed on ZetaChain.

To complete the round trip, you can also deploy a Sui contract that initiates deposits to ZetaChain from on-chain logic.

Go to the Sui contract directory:

cd sui

Check the Move.toml:

sui/Move.toml

This example project already imports the Gateway module from Localnet. When using testnet or mainnet, the Gateway will be imported from a public source instead.

Build the Sui contract:

sui move build --force

Publish the package to your local Sui instance:

SUI_CONTRACT=$(sui client publish \
  --skip-dependency-verification \
  --json 2>/dev/null | jq -r '.objectChanges[] | select(.type == "published") | .packageId') && echo $SUI_CONTRACT

Your Sui account needs some SUI tokens to pay for transactions.

To request tokens from the local faucet, run:

sui client faucet

This sends a faucet request to the local validator and credits your active address with test SUI.

In this step, you’ll call the deposit function from a deployed Sui contract, sending SUI tokens to a universal contract on ZetaChain.

Get a Coin Object

First, find one of the SUI coin objects owned by your active address:

COIN=$(sui client objects \
  $(sui client active-address) \
  --json | jq -r '.[] | select(.data.type == "0x2::coin::Coin<0x2::sui::SUI>") | .data.objectId' | head -n 1) && echo $COIN

This sets $COIN to a valid token object you can spend.

Call the deposit Function

sui client call \
  --package $SUI_CONTRACT \
  --module connected \
  --function deposit \
  --type-args 0x2::sui::SUI \
  --args 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
      $COIN \
      $UNIVERSAL
ArgumentDescription
--package $SUI_CONTRACTThe Sui contract package ID you published earlier
--module connectedThe module in your contract that defines the deposit function
--function depositThe function to call on-chain
--type-args 0x2::sui::SUIThe type of the token being deposited (native SUI)
--args [gateway object ID]The ID of the Gateway object (from localnet output table)
--args $COINThe coin object ID being transferred
--args $UNIVERSALThe receiver address on ZetaChain (your deployed universal contract)

This call triggers the deposit function in your Sui contract, which in turn sends the coin to the Gateway. ZetaChain observes the deposit and transfers ZRC-20 tokens to the specified address on ZetaChain.

You can also trigger cross-chain logic from within a Sui smart contract by calling the deposit_and_call function. This deposits SUI to ZetaChain and calls a universal contract with an encoded payload—all from on-chain logic.

Get a Coin Object

COIN=$(sui client objects \
  $(sui client active-address) \
  --json | jq -r '.[] | select(.data.type == "0x2::coin::Coin<0x2::sui::SUI>") | .data.objectId' | head -n 1) && echo $COIN

Call the deposit_and_call Function

sui client call \
  --package $SUI_CONTRACT \
  --module connected \
  --function deposit_and_call \
  --type-args 0x2::sui::SUI \
  --args 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
      $COIN \
      $UNIVERSAL \
      $(cast abi-encode "function(string)" "alice")
ArgumentDescription
--package $SUI_CONTRACTThe Sui contract package ID you published earlier
--module connectedThe module in your contract that defines the deposit_and_call function
--function deposit_and_callThe function to invoke
--type-args 0x2::sui::SUIThe type of token being sent (native SUI)
--args [gateway object ID]Gateway object ID from the localnet output
--args $COINCoin object ID to send
--args $UNIVERSALReceiver address on ZetaChain (your deployed universal contract)
--args [encoded payload]ABI-encoded call data (in this case, the string "alice")

The last argument (cast abi-encode ...) uses Foundry’s cast tool to encode a string in a format the EVM understands.

🧠 ZetaChain doesn’t enforce a specific payload format, but when calling a Solidity contract, you’ll usually want to encode the data using the EVM ABI standard.