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.
Prerequisites
Make sure the following tools are installed before you begin:
Tool | Purpose |
---|---|
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 |
Clone the Example Project
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.
Launch Localnet
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
, anduserMnemonic
in later steps.
Deploying a Universal Contract
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
Deposit to ZetaChain
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.
Deposit and Call
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.
Build and Deploy a Sui Contract
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
:
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
Get Coins from the Faucet
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.
Deposit from a Sui Contract
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
Argument | Description |
---|---|
--package $SUI_CONTRACT | The Sui contract package ID you published earlier |
--module connected | The module in your contract that defines the deposit function |
--function deposit | The function to call on-chain |
--type-args 0x2::sui::SUI | The type of the token being deposited (native SUI) |
--args [gateway object ID] | The ID of the Gateway object (from localnet output table) |
--args $COIN | The coin object ID being transferred |
--args $UNIVERSAL | The 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.
Deposit and Call from a Sui Contract
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")
Argument | Description |
---|---|
--package $SUI_CONTRACT | The Sui contract package ID you published earlier |
--module connected | The module in your contract that defines the deposit_and_call function |
--function deposit_and_call | The function to invoke |
--type-args 0x2::sui::SUI | The type of token being sent (native SUI) |
--args [gateway object ID] | Gateway object ID from the localnet output |
--args $COIN | Coin object ID to send |
--args $UNIVERSAL | Receiver 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.