diff --git a/src/pages/developers/tutorials/_meta.json b/src/pages/developers/tutorials/_meta.json index 339f600da..e02c01847 100644 --- a/src/pages/developers/tutorials/_meta.json +++ b/src/pages/developers/tutorials/_meta.json @@ -19,6 +19,11 @@ "readTime": "30 min", "description": "Implement a universal swap app compatible with chains like Ethereum, Solana and Bitcoin" }, + "messaging": { + "title": "Messaging", + "readTime": "30 min", + "description": "Learn how to make cross-chain calls between contracts on EVM chains" + }, "solana": { "title": "Solana", "readTime": "60 min", diff --git a/src/pages/developers/tutorials/messaging.mdx b/src/pages/developers/tutorials/messaging.mdx new file mode 100644 index 000000000..7bd06ef91 --- /dev/null +++ b/src/pages/developers/tutorials/messaging.mdx @@ -0,0 +1,401 @@ +This tutorial shows how to send cross-chain messages between contracts on two +EVM chains using ZetaChain’s messaging infrastructure. + +Unlike universal apps deployed directly on ZetaChain, this approach allows you +to keep all contract logic on connected EVM chains. Messages are automatically +routed between them through ZetaChain, without requiring any contracts to be +deployed on ZetaChain itself. + +> Why use this pattern? Unlike universal apps deployed directly on ZetaChain, +> here all business logic stays on the connected EVM chains you already know. +> ZetaChain merely transports the payload, no contract code is deployed on +> ZetaChain itself. + +By the end of this tutorial, you will: + +- Deploy a messaging contract to two EVM testnets (Base and Ethereum Sepolia) +- Link them for cross-chain communication +- Send a message and token value from one to the other +- Track the cross-chain transaction from source to destination + +## Prerequisites + +Make sure you have the following installed: + +- [Node.js](https://nodejs.org/en) v18+ +- [Yarn](https://yarnpkg.com/) +- [Foundry](https://getfoundry.sh/) +- [jq](https://jqlang.org/) for parsing JSON in shell scripts +- A funded private key for both Base Sepolia (84532) and Ethereum Sepolia + (11155111) + +## Create a Project + +Create a new project using the `messaging` template: + +``` +npx zetachain new --project messaging +``` + +Install TypeSCcipt and Foundry dependencies: + +``` +cd messaging +yarn +forge soldeer update +``` + +Compile contracts: + +``` +forge build +``` + +Save your private key in an environment variable so shell scripts can read it: + +``` +PRIVATE_KEY=... +``` + +## Messaging Contract + +To enable cross-chain messaging, your contract must inherit from ZetaChain’s +`Messaging` base contract and implement a few required functions. + +Import the `Messaging.sol` contract from the ZetaChain standard contracts +package: + +```solidity +import "@zetachain/standard-contracts/contracts/messaging/contracts/Messaging.sol"; +``` + +Inherit from `Messaging` in your contract: + +```solidity +contract Example is Messaging { ... } +``` + +Initialize the contract with the required parameters in the constructor: + +```solidity +constructor( + address payable _gateway, + address owner, + address _router +) Messaging(_gateway, owner, _router) {} +``` + +The Messa`ging base contract provides built-in access to Gateway and Router, and +ensures your contract is correctly wired into ZetaChain’s cross-chain messaging +system. + +You must implement three core internal functions for handling message delivery +and fallback: + +#### onMessageReceive + +This is called automatically on the destination chain when a cross-chain message +arrives successfully. + +```solidity +function onMessageReceive( + bytes memory data, + bytes memory sender, + uint256 amount, + bytes memory asset +) internal override { + //... +} +``` + +Use this function to decode the message and execute logic like updating state, +triggering downstream calls, or transferring the received token value. + +### onMessageRevert + +This is triggered if the destination contract’s onMessageReceive fails (e.g., +due to invalid calldata or logic errors). + +```solidity +function onMessageRevert( + bytes memory data, + bytes memory sender, + uint256 amount, + bytes memory asset +) internal override { + //... +} +``` + +### onRevert + +This is called when a message fails during routing before reaching the +destination chain. It executes on the source chain. + +```solidity +function onRevert(RevertContext calldata context) + external + payable + override + onlyGateway +{ + if (context.sender != router) revert Unauthorized(); + //... +} +``` + +You can use this to refund the user, trigger compensation logic, or emit a +notification. + +### Sending a Message + +To initiate a cross-chain message, your contract must call the `depositAndCall` +function on the EVM Gateway. This function is what hands off your message and +optional token value to ZetaChain’s messaging layer for routing. + +Depending on whether you're sending native gas (like ETH) or ERC-20 tokens, +you’ll use one of the following two forms of `depositAndCall`. + +If you want to send a message with ETH as the value: + +```solidity +gateway.depositAndCall{value: msg.value}( + router, + message, + revertOptions +); +``` + +If you're sending supported ERC-20 tokens: + +```solidity +gateway.depositAndCall( + router, + amount, + asset, + message, + revertOptions +); +``` + +`asset` is the ERC-20 token address being sent (must be supported by ZetaChain). + +### What’s Inside the Message Payload? + +The `message` argument is a single `bytes` field. It is ABI-encoded and must +follow a structure that the Universal Router on ZetaChain understands. + +```solidity +abi.encode( + receiver, // bytes: destination contract address on the target chain + targetToken, // address: ZRC-20 address of the token to transfer to the destination contract + data, // bytes: message payload (e.g., ABI-encoded "hello") + gasLimit, // uint256: gas to forward for execution on the target chain + revertOptions // struct: defines what to do on failure +) +``` + +If you're sending the string `"hello"` to a contract on Ethereum Sepolia, you +might encode: + +```solidity +bytes memory data = abi.encode("hello"); +bytes memory message = abi.encode( + abi.encodePacked(receiver), // Destination contract address (as bytes) + targetToken, // Token to transfer on destination chain + data, // ABI-encoded message + 300_000, // Gas limit + revertOptions // Struct specifying fallback behavior +); +``` + +This message is then passed to `depositAndCall()` and routed through ZetaChain +to the destination chain, where it is decoded and passed into the destination +contract’s `onMessageReceive()`. + +### What Is the Universal Router? + +When you send a cross-chain message via `gateway.depositAndCall(...)`, the +actual logic that handles routing and execution on ZetaChain is implemented +inside a contract called the **Universal Router**. + +This contract runs on ZetaChain and acts as the **entry point for all +cross-chain messaging logic**. It is responsible for: + +- **Parsing the message payload** sent from the source chain +- **Swapping tokens** (if necessary) into: + + - The destination chain's **gas token** to cover execution fees + - The destination contract's **target token** to be delivered + +- **Forwarding the message** and token to the destination contract +- **Handling fallback logic** in case the destination call fails + +All contracts that use the Messaging base contract share the same Universal +Router. This shared router simplifies development, ensuring consistent behavior +across all messaging-based apps. + +The Universal Router ensures that your contract only needs to focus on sending +an encoded payload, everything else, from gas handling to token transfer to +delivery mechanics, is managed for you. + +You don’t need to interact with the Universal Router directly. Just encode your +message, call the Gateway, and ZetaChain will handle the rest. + +> 🔧 Advanced: If you need more control, for example, to customize how tokens +> are swapped, route to different contracts, or handle messages differently, you +> can deploy your own instance of a router and point your contracts to it by +> passing its address to the Messaging constructor. + +## Deploy Messaging Contracts + +Deploy to Base Sepolia: + +``` +MESSAGING_BASE=$(./commands/index.ts deploy --rpc https://sepolia.base.org --private-key $PRIVATE_KEY | jq -r .contractAddress) +``` + +Deploy to Ethereum Sepolia: + +``` +MESSAGING_ETHEREUM=$(./commands/index.ts deploy --rpc https://sepolia.drpc.org --private-key $PRIVATE_KEY | jq -r .contractAddress) +``` + +## Connect Two Contracts + +Before two contracts can communicate across chains, they need to explicitly +trust each other. This prevents malicious contracts from spoofing cross-chain +messages. In this step, you’ll establish a bidirectional link between the two +deployed messaging contracts. + +Each contract needs to know: + +- The address of the counterpart contract on the remote chain +- The chain ID of that remote chain + +This is done using the `setConnected()` function in the messaging contract. +ZetaChain’s cross-chain infrastructure will only deliver messages to a contract +if it has registered the sender as trusted. + +⚠️ If you skip this step or set the wrong address/chain ID, messages will be +rejected on the destination chain. + +``` +./commands/index.ts connect \ + --contract $MESSAGING_BASE \ + --target-contract $MESSAGING_ETHEREUM \ + --rpc https://sepolia.base.org \ + --target-chain-id 11155111 \ + --private-key $PRIVATE_KEY +``` + +``` +./commands/index.ts connect \ + --contract $MESSAGING_ETHEREUM \ + --target-contract $MESSAGING_BASE \ + --rpc https://sepolia.drpc.org \ + --target-chain-id 84532 \ + --private-key $PRIVATE_KEY +``` + +Once both directions are linked, the contracts can send and receive messages. + +## Send a Cross-Chain Message + +Now that your contracts are deployed and connected, you can send a message from +one to the other. + +This example sends the string "hello" from the contract on Base Sepolia to the +contract on Ethereum Sepolia, + +``` +./commands/index.ts message \ + --rpc https://sepolia.base.org \ + --private-key $PRIVATE_KEY \ + --contract $MESSAGING_BASE \ + --target-contract $MESSAGING_ETHEREUM \ + --types string \ + --values hello \ + --target-token 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 \ + --amount 0.005 +``` + +| Flag | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--rpc https://sepolia.base.org` | The RPC endpoint for the **source chain** (Base Sepolia). This is where the transaction will be sent. | +| `--private-key $PRIVATE_KEY` | The account that signs and funds the transaction on the source chain. It must hold the token being sent. | +| `--contract $MESSAGING_BASE` | Address of the messaging contract deployed on the source chain. This contract initiates the cross-chain call. | +| `--target-contract $MESSAGING_ETHEREUM` | The address of the contract on the **destination chain**. This is the final recipient of the message. | +| `--types string` | The ABI type of the message you’re sending. This can be a single type (like `string`) or a tuple (e.g., `string,uint256`). | +| `--values hello` | The actual value to encode and send across chains, in this case, just the string `"hello"`. | +| `--target-token 0x...` | The ZRC20 token address on ZetaChain that represents the destination chain’s token. This tells ZetaChain what asset to transfer to the target contract. | +| `--amount 0.005` | The total amount of tokens sent. A portion of this covers gas fees on the destination chain; the remainder is transferred to the destination contract. | + +### How Amount is Handled + +When you send a cross-chain message using `--amount`, you're not just +transferring tokens, you’re also prepaying for gas on the destination chain. +Here's what happens behind the scenes: + +1. You **supply tokens** on the source chain (e.g., Base ETH). This can be + native gas (like ETH) or any supported ERC-20. +2. You specify a **target token** via `--target-token`, which points to the + ZRC-20 representing the token you want to deliver on the destination chain + (e.g., Ethereum ETH). +3. On ZetaChain: + + - A **portion of the supplied amount** is automatically swapped into the + ZRC-20 version of the **gas token** for the destination chain (Ethereum ETH + in this example). This is used to cover the **execution cost** of the + message on the destination chain. + - The **remaining amount** is swapped into the **target token** (which might + be the same token) and forwarded to the destination contract. + +This makes the system fully automated: you don’t need to hold or acquire the +destination chain’s native token to interact with it. + +```json +{ + "contractAddress": "0xee2E8dfefd723e879CAa30A1DaD94046Fa3D24D4", + "targetContract": "0x7c9BbA0630c9452F726bc15D0a73cdF769438efE", + "targetToken": "0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0", + "message": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000", + "transactionHash": "0x939e230dd504efdf1fce31202a5980b4d0376430ddf535d080666256353c02c3", + "amount": "0.005" +} +``` + +## Track the Cross-Chain Transaction + +Use the transaction hash from the previous step to query its cross-chain status: + +``` +npx zetachain query cctx --hash 0x939e230dd504efdf1fce31202a5980b4d0376430ddf535d080666256353c02c3 +``` + +``` +84532 → 7001 ✅ OutboundMined +CCTX: 0xd88d92d0b9b0a2fde416bf6383e430b51de48114b0b03e7cc34e7f8d8df15cb7 +Tx Hash: 0x939e230dd504efdf1fce31202a5980b4d0376430ddf535d080666256353c02c3 (on chain 84532) +Tx Hash: 0x8c368f6a3cfc55950b5d2b0d98c63d1904a79300490ec7d1c258505f372054e3 (on chain 7001) +Sender: 0xee2E8dfefd723e879CAa30A1DaD94046Fa3D24D4 +Receiver: 0x5BD35697D4a62DE429247cbBDCc5c47F70477775 +Message: 00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000005ba149a7bd6dc1f937fa9046a9e05c05f3b18b000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000147c9bba0630c9452f726bc15d0a73cdf769438efe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f000000000000000000000000000000000000000000000000000000000000000000000000000000ee2e8dfefd723e879caa30a1dad94046fa3d24d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000000 +Amount: 5000000000000000 Gas tokens + +7001 → 11155111 ✅ OutboundMined +CCTX: 0x8952c9f95dfb5673a9fbfa2196842b750f5530f4931a55088b1276599328fd64 +Tx Hash: 0xd88d92d0b9b0a2fde416bf6383e430b51de48114b0b03e7cc34e7f8d8df15cb7 (on chain 7001) +Tx Hash: 0xf30e4414087e8b5c81e257e8a97ac9105dde37cbbd6bb33a1691c4a30585507e (on chain 11155111) +Sender: 0x5BD35697D4a62DE429247cbBDCc5c47F70477775 +Receiver: 0x7c9BbA0630c9452F726bc15D0a73cdF769438efE +Message: 00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000009ad718280b1cf00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000014a34000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656c6c6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014ee2e8dfefd723e879caa30a1dad94046fa3d24d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +Amount: 1074034700777807 Gas tokens +``` + +This confirms that the message has successfully moved from Base Sepolia through +ZetaChain to Ethereum Sepolia. + +You can verify the destination chain transaction on Etherscan: + +https://sepolia.etherscan.io/tx/0xf30e4414087e8b5c81e257e8a97ac9105dde37cbbd6bb33a1691c4a30585507e