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
forge soldeer update
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 \
--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
:
[dependencies]
gateway = { local = "/usr/local/share/localnet/protocol-contracts-sui" }
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 And Call from a Sui Contract
In Sui, calling a universal contract from a Move contract is done through a Programmable Transaction Block (PTB).
PTBs let you compose multiple actions into a single atomic transaction: you can
call a Move function, capture its return value, and feed that value into the
next call (such as Sui Gateway’s deposit_and_call
). A client app (for example,
a web app) builds and signs the PTB, submits it to Sui, and Sui executes the
sequence as one transaction.
In the created project, there’s a TypeScript command that builds such a PTB:
commands/suiDepositAndCall.ts
. By default, it ABI‑encodes a payload and sends
it directly to the Sui Gateway. We’ll modify it to:
- Call our Sui contract first and get its return value.
- Pass that returned value as the payload to the Sui Gateway.
Open commands/suiDepositAndCall.ts
and replace the payload construction:
// const payload = tx.pure.vector("u8", utils.arrayify(payloadABI));
const payload = tx.moveCall({
target: `${options.connected}::connected::hello`,
arguments: [tx.pure.string(params.values[0] as string)],
});
This calls your Sui module function connected::hello
with the first provided
value (a string in this example). The returned value from the Move call becomes
the payload
argument that is then forwarded to the Sui Gateway’s
deposit_and_call
within the same PTB.
Because a Sui contract can’t easily ABI‑encode data for EVM, we’ll update the universal Solidity contract to accept raw bytes for the message payload.
Update the universal contract’s onCall
signature and logic to accept bytes
and interpret it as a UTF‑8 string:
function onCall(
MessageContext calldata context,
address zrc20,
uint256 amount,
bytes calldata message
) external override onlyGateway {
// string memory name = abi.decode(message, (string));
emit HelloEvent("Hello on ZetaChain", string(message));
}
Rebuild and deploy the contract on your local ZetaChain:
forge build
UNIVERSAL=$(forge create Universal \
--rpc-url http://localhost:8545 \
--private-key $PRIVATE_KEY \
--broadcast \
--json \
--constructor-args $GATEWAY_ZETACHAIN | jq -r .deployedTo) && echo $UNIVERSAL
Now execute the PTB that calls your Sui contract, takes its return value, and passes it to the Sui Gateway, which then calls your universal contract on ZetaChain:
npx tsx commands deposit-and-call \
--private-key suiprivkey1qrqtrevmd40vxlv3q6fcgmm09af5p8f8j67ezve0u3nhrs0psslgjnw3y5p \
--receiver $UNIVERSAL \
--gateway-package 0xc52d19fd2f0bcb94d0c285d480b6a8af515f5ec19d0cff8818fc9bf0d3731b85 \
--gateway-object 0xa755a1106ae7039a25c10578b6f3c5b73963055e0d6fc40e8abcebea454a6389 \
--amount 0.001 \
--chain-id 104 \
--types string \
--values alice \
--connected $SUI_CONTRACT
You should see an event emitted in the terminal similar to:
[ZetaChain] Event from onCall: {"_type":"log","address":"0xa6e99A4ED7498b3cdDCBB61a6A607a4925Faa1B7","blockHash":"0x694028003ddc35ab9da2a18c262f8df88f882f3a4c3db8d9935b6346ed9f9b7f","blockNumber":192,"data":"0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001248656c6c6f206f6e205a657461436861696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20616c696365000000000000000000000000000000000000000000","index":2,"removed":false,"topics":["0x39f8c79736fed93bca390bb3d6ff7da07482edb61cd7dafcfba496821d6ab7a3"],"transactionHash":"0x82e7ed404b1c3bfb06677174b74d54d1bb5b1b6248433ead6bd48c0cda519777","transactionIndex":0}
To inspect it, you can decode the log data:
cast abi-decode "data()(string,string)" \
0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001248656c6c6f206f6e205a657461436861696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20616c696365000000000000000000000000000000000000000000
Expected output:
"Hello on ZetaChain"
"hello alice"