diff --git a/src/pages/developers/frontend/_meta.json b/src/pages/developers/frontend/_meta.json
index 2d747c67..49143dc6 100644
--- a/src/pages/developers/frontend/_meta.json
+++ b/src/pages/developers/frontend/_meta.json
@@ -4,5 +4,11 @@
"description": "A set of ready to use React components that lets you build user interfaces for universal apps on ZetaChain",
"readType": "Beginner",
"readTime": "30 min"
+ },
+ "bitcoin": {
+ "title": "Bitcoin Frontend",
+ "description": "Learn how to build a simple frontend app that let's you call a universal app on ZetaChain from Bitcoin using a browser wallet extension",
+ "readType": "Beginner",
+ "readTime": "30 min"
}
}
\ No newline at end of file
diff --git a/src/pages/developers/tutorials/bitcoin.mdx b/src/pages/developers/frontend/bitcoin.mdx
similarity index 98%
rename from src/pages/developers/tutorials/bitcoin.mdx
rename to src/pages/developers/frontend/bitcoin.mdx
index 72c0fda5..53b26e19 100644
--- a/src/pages/developers/tutorials/bitcoin.mdx
+++ b/src/pages/developers/frontend/bitcoin.mdx
@@ -203,8 +203,8 @@ npx http-server
Open the page in your browser and fill in the form. You can test functionality
with your own contract address or follow one of the provided tutorials, for
-example, the [Staking](/developers/tutorials/staking/) tutorial to deploy a
-contract that you can call from Bitcoin.
+example, the [Swap](/developers/tutorials/swap/) tutorial to deploy a contract
+that you can call from Bitcoin.
Fill out the form and click the "Send transaction" button. You will be prompted
to confirm the transaction.
diff --git a/src/pages/developers/reference/_meta.json b/src/pages/developers/reference/_meta.json
deleted file mode 100644
index 302ef5fd..00000000
--- a/src/pages/developers/reference/_meta.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "transactions": {
- "title": "Types of Transactions",
- "description": "Examples of different kinds of transactions when interacting with universal apps on ZetaChain"
- },
- "template": {
- "title": "Universal App Template",
- "description": "Hardhat-based template for building universal apps"
- }
-}
\ No newline at end of file
diff --git a/src/pages/developers/reference/transactions.mdx b/src/pages/developers/reference/transactions.mdx
deleted file mode 100644
index bfb37d25..00000000
--- a/src/pages/developers/reference/transactions.mdx
+++ /dev/null
@@ -1,175 +0,0 @@
----
-title: Types of Transactions
----
-
-import { Alert } from "~/components/shared";
-
-## Depositing to EOA
-
-### ✅ Native gas token to TSS with empty input data
-
-To deposit native gas tokens from a connected chain to ZetaChain make a token
-transfer to the TSS address on the connected chain.
-
-When depositing from an EVM chain you don't have to specify the recipient in the
-input data. If the input data is empty, the sender address will be used as the
-recipient address.
-
-When depositing from Bitcoin you must specify the recipient address in the memo.
-
-```
-npx hardhat deposit --amount 0.01 --network sepolia_testnet
-```
-
-https://sepolia.etherscan.io/tx/0x88d180a6424bdaf245ebddcbe4af4e67fd05b0037426fcebe6f9ae011b46fa5a
-
-### ✅ Native gas token to TSS with recipient in input data
-
-When depositing from an EVM chain you can optionally specify a recipient address
-in the input data (first 20 bytes).
-
-```
-npx hardhat deposit --amount 0.01 --network sepolia_testnet --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-```
-
-https://sepolia.etherscan.io/tx/0xaee07e4ab765e4a53d555cf62713100b95f2d0f87b40911acb7dca4342aea7b1
-
-### ✅ USDC to ERC-20 custody contract with no input data
-
-Supported ERC-20 can be deposited to ZetaChain by calling the `deposit` method
-of the ERC-20 custody contract on a connected chain.
-
-```
-npx hardhat deposit --amount 1 --network sepolia_testnet --erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
-```
-
-https://sepolia.etherscan.io/tx/0x281437be30bbde834cb05676fc59ad0084b8828a2b4f9503e8cf46d12f3ce4a2
-
-### ❌ USDC to ERC-20 custody contract with a message
-
-Depositing ERC-20 to an EOA with a message will result in a transaction revert.
-
-```
-npx hardhat deposit --amount 1 --network sepolia_testnet --erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 --message '[["string"], ["hello"]]' --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 --ignore-checks
-```
-
-https://sepolia.etherscan.io/tx/0x8cefbf39999a18a56e54923276d6b85afcc6b366c21752e2e792ec7aeed375e5
-
-## Depositing to Omnichain Contract
-
-### ✅ Native gas token to TSS with recipient and valid message
-
-To deposit native gas tokens from a connected chain and call an omnichain
-contract on ZetaChain make a token transfer to the TSS address on the connected
-chain and specify omnichain contract address as the first 20 bytes of the input
-data and provide a valid message.
-
-This is an example deposit and call of [the example omnichain swap
-contract](/developers/tutorials/swap/).
-
-```
-npx hardhat deposit --amount 3 --network sepolia_testnet --recipient 0x20C4770A73DF5e2ab4B38c08d1055c2173034257 --message '[["address", "bytes"], ["0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891", "0x4955a3F38ff86ae92A914445099caa8eA2B9bA32"]]'
-```
-
-https://sepolia.etherscan.io/tx/0xf291232b1fa6b6e9dd9dbfc4c09b22f32ed9ca61e6f778e985ded1af1d43ee3d
-
-### ✅ Native gas token to TSS with multiple outputs
-
-A single deposit and call transaction can result in multiple outbound
-transactions triggering outputs on multiple connected chains.
-
-This is an example deposit and call of [the example omnichain multioutput
-contract](/developers/tutorials/multioutput/).
-
-```
-npx hardhat deposit --amount 4 --network sepolia_testnet --recipient 0xa573Df1F0729FE6F1BD69b0a5dbFE393e6e09f47 --message '[["address", "bytes", "bytes"], ["0x4955a3F38ff86ae92A914445099caa8eA2B9bA32", "0x746231713873687a663761666333726877386e367736656333327338683665326d727730373764306767", "0x000000000000000000000000d97b1de3619ed2c6beb3860147e30ca8a7dc989100000000000000000000000065a45c57636f9bcced4fe193a602008578bca90b"]]'
-```
-
-https://sepolia.etherscan.io/tx/0xce9386cff141cfe64d38589bdd6d2de843aa07559beea1cc3f3d002b47338711
-
-### ❌ USDC with recipient and valid message
-
-This should succeed, but it fails: https://github.com/zeta-chain/node/issues/1906
-
-```
-npx hardhat deposit --amount 1 --network sepolia_testnet --recipient 0x20C4770A73DF5e2ab4B38c08d1055c2173034257 --message '[["address", "bytes"], ["0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891", "0x4955a3F38ff86ae92A914445099caa8eA2B9bA32"]]' --erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
-```
-
-https://sepolia.etherscan.io/tx/0xf680fa673a4077cec2e2d752260295375113a8c0525c74c5225fb2d2d73cb6d9
-
-### ❌ Native gas token to TSS with recipient and invalid message
-
-If an omnichain contract expects a specific message, an invalid message will
-trigger a contract revert, resulting in a reverted deposit.
-
-```
-npx hardhat deposit --amount 0.01 --network sepolia_testnet --recipient 0x20C4770A73DF5e2ab4B38c08d1055c2173034257
-```
-
-https://sepolia.etherscan.io/tx/0xecf2916c940a4fdf85f250ea822532bba92879cf615197ce3cdd2d9a1137a095
-
-## Withdrawing
-
-### ✅ Withdrawing native gas token ZRC-20 from ZetaChain
-
-To withdraw native gas tokens ZRC-20 from ZetaChain to a connected chain, call
-the `withdraw` method of the ZRC-20 token contract on ZetaChain.
-
-The withdraw amount must be higher than [the withdraw
-fee](/developers/evm/gas/#withdraw). Each ZRC-20 token has its own withdraw fee.
-
-```
-npx hardhat withdraw --amount 0.1 --zrc20 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0 --network zeta_testnet
-```
-
-https://athens.explorer.zetachain.com/evm/tx/0x0013b02a9420109fe0bcedf4733c973d6113cd6e93b120406fadd52188814b21
-
-### ✅ Withdrawing ERC-20 (USDC) ZRC-20 from ZetaChain
-
-To withdraw ERC-20 ZRC-20 from ZetaChain to a connected chain, call the
-`withdraw` method of the ZRC-20 token contract on ZetaChain.
-
-Withdraw fee for ERC-20 ZRC-20 is denominated in the connected chain's native
-gas token. For example, for ZRC-20 USDC from Ethereum the withdraw fee is
-denominated in ETH. The caller of with withdraw method must have enough native
-gas ZRC-20 (approved) to pay the withdraw fee.
-
-```
-npx hardhat withdraw --amount 1 --zrc20 0xcC683A782f4B30c138787CB5576a86AF66fdc31d --network zeta_testnet
-```
-
-https://athens.explorer.zetachain.com/evm/tx/0x540738849a6414809aeb0a9c959e3401b460aeae87c86bc9fd55bd6c1009b014
-
-## Sending ZETA
-
-### ✅ ZETA Between Connected Chains
-
-```jsx
-npx hardhat send-zeta --amount 3 --network mumbai_testnet --destination bsc_testnet
-```
-
-https://mumbai.polygonscan.com/tx/0x217300fc78a04eaf9c6217749b3e20aa99548771de48638b5016efae853c9368
-
-### ❌ ZETA Between Connected Chains (insufficient amount)
-
-```jsx
-npx hardhat send-zeta --amount 0.1 --network mumbai_testnet --destination bsc_testnet --ignore-checks
-```
-
-https://mumbai.polygonscan.com/tx/0x9132a994cbf536486c1354a6333e6d8f59156971888eebe8f74f882d48d16770
-
-### ✅ ZETA to ZetaChain
-
-```jsx
-npx hardhat send-zeta --amount 3 --network mumbai_testnet --destination zeta_testnet
-```
-
-https://mumbai.polygonscan.com/tx/0x40038e68d3f997952c302c95bcf10f841f8d3e960db3e71249ae0fe2b81a2e6f
-
-### ✅ ZETA from ZetaChain
-
-```jsx
-npx hardhat send-zeta --amount 3 --network zeta_testnet --destination mumbai_testnet
-```
-
-https://athens.explorer.zetachain.com/cc/tx/0xa28ff75a237afcdc3a5ee7a8c79a4e813e899ab6d9b0c22d20e5cdf10e028a6a
diff --git a/src/pages/developers/tutorials/_meta.json b/src/pages/developers/tutorials/_meta.json
index cbdd343c..22d888df 100644
--- a/src/pages/developers/tutorials/_meta.json
+++ b/src/pages/developers/tutorials/_meta.json
@@ -5,43 +5,28 @@
"description": "Learn how to set up a smart contract template, create an account, and use faucet"
},
"hello": {
- "title": "First Universal App",
+ "title": "First Universal App on Localnet",
"readTime": "10 min",
"description": "Learn how to create, deploy and interact with a universal app"
},
- "localnet": {
- "title": "Localnet",
+ "swap-tss": {
+ "title": "Swap on Testnet",
"readTime": "30 min",
- "description": "Build and interact with your universal app in a local dev environment"
+ "description": "Implement an omnichain swap app compatible with chains like Ethereum, BNB and Bitcoin"
},
"swap": {
- "title": "Swap",
+ "title": "Swap on Localnet",
"readTime": "30 min",
"description": "Implement an omnichain swap app compatible with chains like Ethereum, BNB and Bitcoin"
},
"swap-any": {
- "title": "Swap Any Token",
+ "title": "Swap Any Token on Localnet",
"readTime": "60 min",
"description": "Enhance the omnichain swap app with the ability to swap to any token"
},
- "staking": {
- "title": "Staking",
- "readTime": "60 min",
- "description": "Explore how universal apps can be used for omnichain staking"
- },
- "nft": {
- "title": "NFT",
- "readTime": "60 min",
- "description": "Learn how to create a client-side UI for your universal app with Next.js and TypeScript"
- },
- "bitcoin": {
- "title": "Bitcoin Frontend",
- "readTime": "20 min",
- "description": "Learn how to make calls to universal apps from Bitcoin with wallets like XDEFI and UniSat"
- },
- "multioutput": {
- "title": "Single Input, Multiple Outputs",
- "readTime": "60 min",
- "description": "Learn how a single call to a universal app can create multiple outbound transactions"
+ "localnet": {
+ "title": "Localnet",
+ "readTime": "30 min",
+ "description": "Build and interact with your universal app in a local dev environment"
}
}
\ No newline at end of file
diff --git a/src/pages/developers/tutorials/hello.mdx b/src/pages/developers/tutorials/hello.mdx
index 410b1dea..54fc1086 100644
--- a/src/pages/developers/tutorials/hello.mdx
+++ b/src/pages/developers/tutorials/hello.mdx
@@ -1,28 +1,28 @@
---
-title: Your First Univeral App
+title: Your First Universal App
---
+import { Alert } from "~/components/shared";
+
In this tutorial, you will create a simple universal app contract that accepts a
message with a string and emits an event with that string when called from a
connected chain. For example, a user on Ethereum will be able to send a message
-"alice" and the universal contract on ZetaChain will emit a "Greet" event with a
-value "alice".
+"alice" and the universal contract on ZetaChain will emit an event with the
+string "Hello on ZetaChain, alice".
You will learn how to:
-- Use the Hardhat template to create a new universal app using a single command
-- Define your universal app contract to handle messages from connected chains
-- Deploy the contract to ZetaChain
-- Interact with the contract by sending a message from Ethereum testnet
-- Track an incoming cross-chain transaction
+- Define your universal app contract to handle messages from connected chains.
+- Deploy the contract to localnet.
+- Interact with the contract by sending a message from a connected EVM
+ blockchain in localnet.
+- Handle reverts gracefully by implementing revert logic.
-
+
+ {" "}
+ This tutorial depends on the gateway, which is available on localnet but not yet deployed on testnet. It will be compatible
+ with testnet after the gateway is deployed. In other words, you can't deploy this tutorial on testnet yet.{" "}
+
## Prerequisites
@@ -31,67 +31,37 @@ You will learn how to:
## Set Up Your Environment
-Clone the Hardhat contract template:
+Clone the example contracts repository and install the dependencies:
```
-git clone https://github.com/zeta-chain/template
+git clone https://github.com/zeta-chain/example-contracts
-cd template/contracts
+cd example-contracts/examples/hello
yarn
```
-Make sure that you've followed the [Getting
-Started](/developers/tutorials/intro) tutorial to set up your development
-environment, create an account and request testnet tokens.
-
-## Create the Contract
-
-To create a new universal app contract, use the `omnichain` Hardhat task:
-
-```
-npx hardhat omnichain Greeting name
-```
-
-The `omnichain` task accepts a contract name (`Greeting`) and a list of fields.
-The list of fields defines the values that will be included in the message
-passed to a universal app contract.
-
-A field may have a type specified after the field name, separated by a colon. If
-no type is specified, the type defaults to `string`.
-
-Supported types are: `address`, `bool`, `bytes32`, `string`,
-`int`,`int8`,`int16`,`int128`,`int256`,`uint`,`uint8`,`uint16`,`uint128`,`uint256`.
-
-In this example, the message will contain only one value: `name` of type
-`string`.
-
-The `omnichain` task has created:
-
-- `contracts/Greeting.sol`: a Solidity universal app contract
-- `tasks/deploy.ts`: a Hardhat task to deploy the contract
-- `tasks/interact.ts`: a Hardhat task to interact with the contract
-
-It also modified `hardhat.config.ts` to import both `deploy` and `interact`
-tasks.
-
## Universal App Contract
-Let's review the contents of the `Greeting` contract:
+Let's review the contents of the `Hello` contract:
-```solidity filename="contracts/Greeting.sol"
+```solidity filename="contracts/Hello.sol"
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
+pragma solidity 0.8.26;
+
+import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
+contract Hello is UniversalContract {
+ GatewayZEVM public gateway;
-contract Greeting is zContract, OnlySystem {
- SystemContract public systemContract;
+ event HelloEvent(string, string);
+ event RevertEvent(string, RevertContext);
- constructor(address systemContractAddress) {
- systemContract = SystemContract(systemContractAddress);
+ constructor(address payable gatewayAddress) {
+ gateway = GatewayZEVM(gatewayAddress);
}
function onCrossChainCall(
@@ -99,341 +69,239 @@ contract Greeting is zContract, OnlySystem {
address zrc20,
uint256 amount,
bytes calldata message
- ) external virtual override onlySystem(systemContract) {
- (string memory name) = abi.decode(
- message,
- (string)
- );
- // TODO: implement the logic
+ ) external override {
+ string memory name = abi.decode(message, (string));
+ emit HelloEvent("Hello on ZetaChain", name);
+ }
+
+ function onRevert(RevertContext calldata revertContext) external override {
+ emit RevertEvent("Revert on EVM", revertContext);
}
}
```
-`Greeting` is a simple contract that inherits from the [`zContract`
-interface](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/zevm/interfaces/zContract.sol).
+`Hello` is a simple contract that inherits from the [`UniversalContract`
+interface](https://github.com/zeta-chain/protocol-contracts/blob/main/v2/contracts/zevm/interfaces/UniversalContract.sol),
+which defines the required functions for cross-chain communication.
-The contract declares a state variable of type `SystemContract` that stores a
-reference to the system contract.
+The contract declares a state variable of type `GatewayZEVM` that stores a
+reference to the ZetaChain's gateway contract.
-The constructor function accepts the address of the system contract and stores
-it in the `systemContract` state variable.
+The constructor function accepts the address of the ZetaChain gateway contract
+and initializes the `gateway` state variable.
-`onCrossChainCall` is a function that is called when the contract gets called by
-a token transfer transaction sent to the TSS address on a connected chain (when
-a gas token is deposited) or a `deposit` method call on the ERC-20 custody
-contract (when an ERC-20 token is deposited). The function receives the
-following inputs:
+`onCrossChainCall` is a function that is executed when the contract is called
+from a connected chain through a gateway. The function receives the following
+inputs:
- `context`: is a struct of type
[`zContext`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/zevm/interfaces/zContract.sol)
that contains the following values:
- - `origin`: EOA address that sent the token transfer transaction to the TSS
- address (triggering the omnichain contract) or the value passed to the
- `deposit` method call on the ERC-20 custody contract.
+ - `origin`: EOA or contract caller address that called the gateway on a
+ connected chain.
- `chainID`: integer ID of the connected chain from which the omnichain
contract was triggered.
- `sender` (reserved for future use, currently empty)
- `zrc20`: the address of the ZRC-20 token contract that represents an asset
from a connected chain on ZetaChain.
-- `amount`: the amount of tokens that were transferred to the TSS address or an
- amount of tokens that were deposited to the ERC-20 custody contract.
+- `amount`: the amount of tokens that were sent to the universal app
- `message`: the contents of the `data` field of the token transfer transaction.
-The `onCrossChainCall` function should only be called by the system contract (in
-other words, by the ZetaChain protocol) to prevent a caller from supplying
-arbitrary values in `context`. The `onlySystem` modifier ensures that the
-function is called only as a response to a token transfer transaction sent to
-the TSS address or an ERC-20 custody contract.
+The `onCrossChainCall` function should only be called by the ZetaChain protocol
+to prevent a caller from supplying arbitrary values in `context`.
-By default, the `onCrossChainCall` function doesn't do anything else. You will
-implement the logic yourself based on your use case.
+`onCrossChainCall` decodes the `name` from the `message` and emits an event.
-Modify the contract to emit an event after parsing the `message`:
+## Understanding the `Revert` Contract
-```solidity filename="contracts/Greeting.sol" {10,26}
+The `Revert` contract is used to handle reverts that occur on ZetaChain and
+allows you to define custom logic for such cases.
+
+```solidity
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
+pragma solidity 0.8.26;
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
+import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";
-contract Greeting is zContract, OnlySystem {
- SystemContract public systemContract;
- event Greet(string message);
+contract Revert {
+ event RevertEvent(string, RevertContext);
+ event HelloEvent(string, string);
- constructor(address systemContractAddress) {
- systemContract = SystemContract(systemContractAddress);
+ function hello(string memory message) external {
+ emit HelloEvent("Hello on EVM", message);
}
- function onCrossChainCall(
- zContext calldata context,
- address zrc20,
- uint256 amount,
- bytes calldata message
- ) external virtual override onlySystem(systemContract) {
- (string memory name) = abi.decode(
- message,
- (string)
- );
- emit Greet(name);
+ function onRevert(RevertContext calldata revertContext) external {
+ emit RevertEvent("Revert on EVM", revertContext);
}
+
+ receive() external payable {}
+
+ fallback() external payable {}
}
```
-## Deploy Task
+## Start Localnet
-The `omnichain` task has created a Hardhat task to deploy the contract:
+[Localnet](/developers/tutorials/localnet) is a development environment that
+simulates the behavior of ZetaChain protocol contracts on a single local
+blockchain.
-```ts filename="tasks/deploy.ts"
-import { getAddress, ParamChainName } from "@zetachain/protocol-contracts";
-import { task } from "hardhat/config";
-import { HardhatRuntimeEnvironment } from "hardhat/types";
+Start localnet by running:
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const network = hre.network.name as ParamChainName;
+```
+npx hardhat localnet
+```
- if (!/zeta_(testnet|mainnet)/.test(network)) {
- throw new Error('🚨 Please use either "zeta_testnet" or "zeta_mainnet" network to deploy to ZetaChain.');
- }
+## Deploy the Contract
- const [signer] = await hre.ethers.getSigners();
- if (signer === undefined) {
- throw new Error(
- `Wallet not found. Please, run "npx hardhat account --save" or set PRIVATE_KEY env variable (for example, in a .env file)`
- );
- }
+Compile the contracts and deploy them to localnet: s
- const systemContract = getAddress("systemContract", network);
+```
+npx run deploy
+```
- const factory = await hre.ethers.getContractFactory(args.name);
- const contract = await factory.deploy(systemContract);
- await contract.deployed();
+You should see output similar to:
- const isTestnet = network === "zeta_testnet";
- const zetascan = isTestnet ? "athens.explorer" : "explorer";
- const blockscout = isTestnet ? "zetachain-athens-3" : "zetachain";
+```
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
- if (args.json) {
- console.log(JSON.stringify(contract));
- } else {
- console.log(`🔑 Using account: ${signer.address}
+🚀 Successfully deployed "Hello" contract on localhost.
+📜 Contract address: 0x67d269191c92Caf3cD7723F116c85e6E9bf55933
-🚀 Successfully deployed contract on ${network}.
-📜 Contract address: ${contract.address}
-🌍 ZetaScan: https://${zetascan}.zetachain.com/address/${contract.address}
-🌍 Blockcsout: https://${blockscout}.blockscout.com/address/${contract.address}
-`);
- }
-};
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
-task("deploy", "Deploy the contract", main)
- .addFlag("json", "Output in JSON")
- .addOptionalParam("name", "Contract to deploy", "Greeting");
+🚀 Successfully deployed "Revert" contract on localhost.
+📜 Contract address: 0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E
```
-Omnichain contracts are supposed to be deployed to ZetaChain, so the task checks
-that the `--network` flag value is always `zeta_testnet`.
+## Interact with the Contract
-The task uses the `getAddress` function from `@zetachain/protocol-contracts` to
-get the address of the system contract on ZetaChain.
+Use the `evm-call` script to execute the `gateway.call` method on the connected
+EVM chain. This method sends a message to the `Hello` contract on ZetaChain.
-The task then uses Ethers.js to deploy the contract to ZetaChain.
+```
+npx hardhat evm-call --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --network localhost --types '["string"]' alice
+```
-## Interact Task
+Parameters:
-The `omnichain` task has also created a Hardhat task to interact with the
-contract:
+- `--receiver`: The address of the `Hello` contract on ZetaChain.
+- `--types`: The ABI types of the message parameters.
+- `alice`: The message to send.
-```ts filename="tasks/interact.ts"
-import { task } from "hardhat/config";
-import { HardhatRuntimeEnvironment } from "hardhat/types";
-import { parseUnits } from "@ethersproject/units";
-import { getAddress } from "@zetachain/protocol-contracts";
-import ERC20Custody from "@zetachain/protocol-contracts/abi/evm/ERC20Custody.sol/ERC20Custody.json";
-import { prepareData } from "@zetachain/toolkit/client";
-import { utils, ethers } from "ethers";
-import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20.json";
+The EVM gateway processes the call and emits a "Called" event.
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const [signer] = await hre.ethers.getSigners();
+```
+[EVM]: Gateway: 'Called' event emitted
+```
- const data = prepareData(args.contract, ["string"], [args.name]);
+ZetaChain picks up the event and executes the `onCrossChainCall` function of the
+`Hello` contract.
- let tx;
+```text
+[ZetaChain]: Universal contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 executing onCrossChainCall (context: {"origin":"0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0","sender":"0x735b14BB79463307AAcBED86DAf3322B1e6226aB","chainID":1}), zrc20: 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921, amount: 0, message: 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005616c696365000000000000000000000000000000000000000000000000000000)
+```
- if (args.token) {
- const custodyAddress = getAddress("erc20Custody", hre.network.name as any);
- if (!custodyAddress) {
- throw new Error(`No ERC20 Custody contract found for ${hre.network.name} network`);
- }
+The `Hello` contract decodes the message and emits a `HelloEvent`.
- const custodyContract = new ethers.Contract(custodyAddress, ERC20Custody.abi, signer);
- const tokenContract = new ethers.Contract(args.token, ERC20.abi, signer);
- const decimals = await tokenContract.decimals();
- const value = parseUnits(args.amount, decimals);
- const approve = await tokenContract.approve(custodyAddress, value);
- await approve.wait();
-
- tx = await custodyContract.deposit(signer.address, args.token, value, data);
- tx.wait();
- } else {
- const value = parseUnits(args.amount, 18);
- const to = getAddress("tss", hre.network.name as any);
- tx = await signer.sendTransaction({ data, to, value });
- }
-
- if (args.json) {
- console.log(JSON.stringify(tx, null, 2));
- } else {
- console.log(`🔑 Using account: ${signer.address}\n`);
-
- console.log(`🚀 Successfully broadcasted a token transfer transaction on ${hre.network.name} network.
-📝 Transaction hash: ${tx.hash}
- `);
- }
-};
-
-task("interact", "Interact with the contract", main)
- .addParam("contract", "The address of the withdraw contract on ZetaChain")
- .addParam("amount", "Amount of tokens to send")
- .addOptionalParam("token", "The address of the token to send")
- .addFlag("json", "Output in JSON")
- .addParam("name");
-```
-
-The task uses the `prepareData` function from `@zetachain/toolkit/helpers` to
-prepare the `data` field of the token transfer transaction. `prepareData`
-accepts an omnichain contract address on ZetaChain, a list of argument types,
-and a list of argument names. The `data` field contains the following
-information:
-
-- the address of the contract on ZetaChain
-- the arguments to pass to the `onCrossChainCall` function in the `message`
- parameter
-
-In the code generated above there are no arguments, so the `data` field is
-simply the address of the contract on ZetaChain.
-
-Calling omnichain contracts is differs depending on whether a gas token is being
-deposited or an ERC-20 token.
-
-If an ERC-20 token address is passed to the `--token` optional parameter, the
-interact task assumes you want to deposit an ERC-20 token in an omnichain
-contract.
-
-To deposit an ERC-20 token into an omnichain contract you need to call the
-`deposit` method of the ERC-20 custody contract. The task first gets the address
-of the custody contract on the current network, creates an instance of a token
-contract, gets the number of decimals of the token, and approves the custody
-contract to spend the specified amount of ERC-20 tokens. The task then calls the
-`deposit` method of the custody contract, passing the following information:
-
-- `signer.address`: the sender address that will be available in the `origin`
- field of the `context` parameter of the `onCrossChainCall` function
-- `args.token`: the address of the ERC-20 token being deposited
-- `value`: the amount of tokens being deposited
-- `data`: the contents of the `message`
-
-If the `--token` optional parameter is not used, the interact task assumes you
-want to deposit a gas token. To deposit a gas token you need to send a token
-transfer transaction to the TSS address on a connected chain.
-
-`getAddress` retrieves the address of the TSS on the current network.
-
-The task then uses Ethers.js to send a token transfer transaction to the TSS
-address. The transaction contains the following information:
-
-- `data`: the `data` field prepared by `prepareData`
-- `to`: the address of the TSS
-- `value`: the amount of tokens to transfer
-
-* `value`: the number of tokens to transfer
+```
+[ZetaChain]: Event from onCrossChainCall: {"_type":"log","address":"0x67d269191c92Caf3cD7723F116c85e6E9bf55933","blockHash":"0x978e67898c41511075417bcb219fe35f18d11ec992a2d7bac80ca0a28c72155f","blockNumber":41,"data":"0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001248656c6c6f206f6e205a657461436861696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005616c696365000000000000000000000000000000000000000000000000000000","index":0,"removed":false,"topics":["0x39f8c79736fed93bca390bb3d6ff7da07482edb61cd7dafcfba496821d6ab7a3"],"transactionHash":"0x8941f1f6015a43ce55bc1a55858a2a783c94108667197837f984ca2b0c9ba4a5","transactionIndex":0}
+```
-## Deploy the Contract
+## Simulating a Revert
-Compile the contract:
+To demonstrate how reverts are handled, we'll intentionally cause a revert by
+sending unexpected data. Instead of a `string`, we'll send a `uint256`.
```
-npx hardhat compile --force
+npx hardhat evm-call --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --network localhost --types '["uint256"]' 42
```
-Use the `--force` flag to clear the cache and artifacts.
-
-Deploy the contract to ZetaChain:
+This will cause the `abi.decode` function in the `onCrossChainCall` to fail,
+triggering a revert.
```
-npx hardhat deploy --network zeta_testnet
+[EVM]: Gateway: 'Called' event emitted
```
```
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-
-🚀 Successfully deployed contract on zeta_testnet.
-📜 Contract address: 0x2C0201B9DFdC6Dcc23524Ab29c51c38dcc8afF54
-🌍 ZetaScan: https://athens.explorer.zetachain.com/address/0x2C0201B9DFdC6Dcc23524Ab29c51c38dcc8afF54
-🌍 Blockcsout: https://zetachain-athens-3.blockscout.com/address/0x2C0201B9DFdC6Dcc23524Ab29c51c38dcc8afF54
+[ZetaChain]: Universal contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 executing onCrossChainCall (context: {"origin":"0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0","sender":"0x735b14BB79463307AAcBED86DAf3322B1e6226aB","chainID":1}), zrc20: 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921, amount: 0, message: 0x000000000000000000000000000000000000000000000000000000000000002a)
```
-## Interact with the Contract
-
-Use the `interact` task to send a transaction on Ethereum Sepolia testnet:
+You'll see output indicating that an error occurred:
```
-npx hardhat interact --name alice --contract 0x2C0201B9DFdC6Dcc23524Ab29c51c38dcc8afF54 --network sepolia_testnet --amount 0
+[ZetaChain]: Error executing onCrossChainCall: Error: transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": "1", "blobGasUsed": null, "blockHash": "0x18c7286736278b0fbb987115176dfa42cd77cf9ec224914a37f43694fc506189", "blockNumber": 48, "contractAddress": null, "cumulativeGasUsed": "36569", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "gasPrice": "10000000000", "gasUsed": "36569", "hash": "0x7367af3912dc16d52cf29bd7e7c005fe3bec090e360108c69db7b57a8aec4262", "index": 0, "logs": [ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0xbf1838bfa460082241895d67c8789e5f7ecc0729e88965abe1eaed1ed77ba66d", "status": 0, "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, code=CALL_EXCEPTION, version=6.13.2)
```
-```
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+Since we didn't specify a revert address, the gateway on the EVM chain cannot
+handle the revert properly:
-🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet network.
-📝 Transaction hash: 0x93b441dc2ddb751a60a2f4c0fc52dbbd447ed70eb962b1a01072328aa6872b73
+```
+[EVM]: Tx reverted without callOnRevert: Error: transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": "1", "blobGasUsed": null, "blockHash": "0x18c7286736278b0fbb987115176dfa42cd77cf9ec224914a37f43694fc506189", "blockNumber": 48, "contractAddress": null, "cumulativeGasUsed": "36569", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "gasPrice": "10000000000", "gasUsed": "36569", "hash": "0x7367af3912dc16d52cf29bd7e7c005fe3bec090e360108c69db7b57a8aec4262", "index": 0, "logs": [ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0xbf1838bfa460082241895d67c8789e5f7ecc0729e88965abe1eaed1ed77ba66d", "status": 0, "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, code=CALL_EXCEPTION, version=6.13.2)
```
-The `interact` task has sent a transaction to the TSS address on Sepolia.
+## Handling the Revert
-The `data` field of the transaction contains the following data:
+To handle the revert gracefully, we'll provide additional parameters to specify
+that the gateway should call the `Revert` contract on the source chain in case
+of a revert.
```
-0x2c0201b9dfdc6dcc23524ab29c51c38dcc8aff5400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005616c696365000000000000000000000000000000000000000000000000000000
+npx hardhat evm-call --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --call-on-revert --revert-address 0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E --network localhost --types '["uint256"]' 42
```
-The first 20 bytes is the universal app contract on ZetaChain, the rest are
-bytes that will be passed to the universal contract as `message`. In this
-example, the `message` is a string `alice`.
+Parameters:
-The `value` (amount) of the transaction is 0 because you're not sending any ETH
-in this example, you're just sending a message.
-
-Track the transaction:
+- `--call-on-revert`: Informs the gateway to handle reverts.
+- `--revert-address`: The address of the `Revert` contract on the source chain.
```
-npx hardhat cctx 0x1d32177e6cdedbabd5f587ed0de80e8b4734636b329d88d24d015676257c330d
+[EVM]: Gateway: 'Called' event emitted
```
```
-✓ CCTXs on ZetaChain found.
+[ZetaChain]: Universal contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 executing onCrossChainCall (context: {"origin":"0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0","sender":"0x735b14BB79463307AAcBED86DAf3322B1e6226aB","chainID":1}), zrc20: 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921, amount: 0, message: 0x000000000000000000000000000000000000000000000000000000000000002a)
+```
-✓ 0xb672ea88abd2c35e4ee4a094969799259773a89e5d93906e46829632837106a5: 11155111 → 7001: OutboundMined (Remote omnichain contract call completed)
+```
+[ZetaChain]: Error executing onCrossChainCall: Error: transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": "1", "blobGasUsed": null, "blockHash": "0xaa203c2d40f8c35f098542958a7f8268c222fc6d204968fe14f65e3b60036d7e", "blockNumber": 41, "contractAddress": null, "cumulativeGasUsed": "36569", "from": "0x735b14BB79463307AAcBED86DAf3322B1e6226aB", "gasPrice": "10000000000", "gasUsed": "36569", "hash": "0x2c339d4414b3691a749be036a0be8ce692d8b2ac0997069fc73e07cdf628d7fc", "index": 0, "logs": [ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0x8ae10541b4c9486d97e3e477295449ae80e3db3238ca0d19bf53483ca32119a6", "status": 0, "to": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, code=CALL_EXCEPTION, version=6.13.2)
```
-You can also track the cross-chain transaction on ZetaScan:
+You'll now see that the `Revert` contract's `onRevert` function is called:
-https://athens.explorer.zetachain.com/cc/tx/0xb672ea88abd2c35e4ee4a094969799259773a89e5d93906e46829632837106a5
+```
+[EVM]: Contract 0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E executing onRevert (context: {"asset":"0x0000000000000000000000000000000000000000","amount":0,"revertMessage":"0x3078"})
+```
-Once the transaction is finalized on ZetaChain, you should be able to see the
-event emitted by the contract on ZetaChain. Go to the "Logs tab" to see:
+```
+[EVM]: Gateway: successfully called onRevert
+```
+
+The `Revert` contract emits an event:
```
-Method id efdeaaf5
-Call Greet(string message)
-Name Type Indexed? Data
-message string false alice
+[EVM]: Event from onRevert: {"_type":"log","address":"0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E","blockHash":"0xa3778291eb4a9c8b352b0c251e8fb379ba88c80c624fcb9384a0b20e661321cb","blockNumber":42,"data":"0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000d526576657274206f6e2045564d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000","index":0,"removed":false,"topics":["0xd0ec07494fc6e006dfb6c9d8649b3ad7404ac3bf1d4bcd7741923c3937d84ff2"],"transactionHash":"0x78a33b424d066f9cbdaed683fd4609d6beeb36b936cce0e3111a8462b2189d64","transactionIndex":0}
```
-https://zetachain-athens-3.blockscout.com/address/0x2C0201B9DFdC6Dcc23524Ab29c51c38dcc8afF54
+## Conclusion
+
+In this tutorial, you:
+
+- Learned how to define a universal app contract that handles cross-chain
+ messages.
+- Deployed the `Hello` and `Revert` contracts to a local development network.
+- Interacted with the `Hello` contract by sending messages from a connected EVM
+ chain.
+- Simulated a revert scenario and handled it gracefully using the `Revert`
+ contract.
+
+By understanding how to manage cross-chain calls and handle reverts, you're well
+on your way to building robust universal applications on ZetaChain.
+
+## Source Code
+
+You can find the source code for the tutorial in the example contracts repo:
-Congratulations! 🎉 You've successfully created a universal app contract,
-deployed it on ZetaChain and made a call to it with a message sent from Ethereum
-testnet. Now you know the basics of how to handle calls and parse messages from
-connected chains and can move on to build more exciting apps!
+https://github.com/zeta-chain/example-contracts/tree/main/examples/hello
diff --git a/src/pages/developers/tutorials/multioutput.mdx b/src/pages/developers/tutorials/multioutput.mdx
deleted file mode 100644
index 6be99b17..00000000
--- a/src/pages/developers/tutorials/multioutput.mdx
+++ /dev/null
@@ -1,324 +0,0 @@
----
-title: Single Input Multiple Output
----
-
-## Overview
-
-If you already read the previous tutorials you already know how to use
-ZetaChain's EVM. A very common use case on ZetaChain's EVM is a smart contract
-with a single input from one chain, perform some logic, and then execute the
-output to another or multiple chains.
-
-The example in this tutorial does exactly that: the contract reads an address
-from the message, and then send some tokens to that address in several chains.
-
-This capability may be useful for applications like multichain asset managers or
-DeFi applications that need to distribute or manage assets on many chains from
-one place.
-
-This tutorial also demonstrates how a single inbound cross-chain transactions
-can result in more than one outbound cross-chain transactions.
-
-## Set up your environment
-
-```
-git clone https://github.com/zeta-chain/template
-```
-
-## Create the contract
-
-Run the following command to create a new omnichain contract called
-`MultiOutput` with one parameter in the message:
-
-```
-npx hardhat omnichain Multioutput recipient btcRecipient targetToken
-```
-
-## OnCrossChainCall
-
-Implement the `onCrossChainCall` function:
-
-```solidity filename="contracts/Multioutput.sol"
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
-
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
-import "@zetachain/toolkit/contracts/SwapHelperLib.sol";
-import "@openzeppelin/contracts/access/Ownable.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
-
-contract Multioutput is zContract, Ownable, OnlySystem {
- error NoAvailableTransfers();
- error InvalidRecipient();
- error FetchingBTCZRC20Failed();
-
- event Withdrawal(address, uint256, bytes);
-
- SystemContract public systemContract;
-
- uint256 constant BITCOIN = 18332;
-
- constructor(address systemContractAddress) {
- systemContract = SystemContract(systemContractAddress);
- }
-
- function onCrossChainCall(
- zContext calldata context,
- address zrc20,
- uint256 amount,
- bytes calldata message
- ) external virtual override onlySystem(systemContract) {
- if (systemContract.gasCoinZRC20ByChainId(BITCOIN) == address(0))
- revert FetchingBTCZRC20Failed();
-
- (
- address evmRecipient,
- bytes memory btcRecipient,
- address[] memory destinationTokens
- ) = parseMessage(context.chainID, message);
-
- uint256 totalTransfers = destinationTokens.length;
- if (totalTransfers == 0) revert NoAvailableTransfers();
-
- uint256 amountToTransfer = amount / totalTransfers;
- uint256 leftOver = amount - amountToTransfer * totalTransfers;
-
- uint256 lastTransferIndex = destinationTokens[
- destinationTokens.length - 1
- ] == zrc20
- ? destinationTokens.length - 2
- : destinationTokens.length - 1;
-
- for (uint256 i; i < destinationTokens.length; i++) {
- address targetZRC20 = destinationTokens[i];
- if (targetZRC20 == zrc20) continue;
-
- if (lastTransferIndex == i) {
- amountToTransfer += leftOver;
- }
-
- bytes memory recipient = abi.encodePacked(
- BytesHelperLib.addressToBytes(evmRecipient)
- );
-
- if (targetZRC20 == systemContract.gasCoinZRC20ByChainId(BITCOIN)) {
- if (btcRecipient.length == 0) revert InvalidRecipient();
- recipient = abi.encodePacked(btcRecipient);
- }
-
- _doSwapAndWithdraw(zrc20, amountToTransfer, targetZRC20, recipient);
- }
- }
-}
-```
-
-On a high level the `onCrossChainCall` function does the following:
-
-1. Parse the message to get the recipient address and the destination tokens.
-2. Calculate the amount to transfer to each destination token.
-3. Loop through the destination tokens and call the `_doSwapAndWithdraw`
- function to perform the transfer.
-
-When looping through the destination tokens, the contract checks that the
-destination token is not the same as the source token, and if it is, it skips
-the transfer.
-
-The contract also checks if the destination token is BTC, and if it is, it uses
-the BTC recipient address from the message.
-
-## Parse the message
-
-Next, implement the `parseMessage` function:
-
-```solidity filename="contracts/Multioutput.sol"
- function parseMessage(
- uint256 chainID,
- bytes calldata message
- ) public pure returns (address, bytes memory, address[] memory) {
- address evmRecipient;
- bytes memory btcRecipient;
- address[] memory destinationTokens;
- if (chainID == BITCOIN) {
- evmRecipient = BytesHelperLib.bytesToAddress(message, 0);
- uint256 numTokens = message.length / 20 - 1;
- destinationTokens = new address[](numTokens);
- for (uint256 i = 0; i < numTokens; i++) {
- destinationTokens[i] = BytesHelperLib.bytesToAddress(
- message,
- 20 + i * 20
- );
- }
- } else {
- (
- address evmAddress,
- bytes memory btcAddress,
- bytes memory targetTokens
- ) = abi.decode(message, (address, bytes, bytes));
-
- btcRecipient = btcAddress;
- evmRecipient = evmAddress;
-
- uint256 numTokens = targetTokens.length / 32;
- destinationTokens = new address[](numTokens);
- for (uint256 i = 0; i < numTokens; i++) {
- destinationTokens[i] = BytesHelperLib.bytesMemoryToAddress(
- targetTokens,
- i * 32
- );
- }
- }
-
- return (evmRecipient, btcRecipient, destinationTokens);
- }
-```
-
-`parseMessage` is a helper function that decodes the message to get the
-recipient address and the destination tokens.
-
-If the source chain is Bitcoin, the function assumes that the destination
-address is an EVM address and uses the helper function to manually decode the
-first 20 bytes as the recipient address. The rest of the message is a list of
-destination tokens. The function loops through the message and decodes each 20
-bytes as an address.
-
-If the source chain is not Bitcoin, the function expects both an EVM address and
-a BTC address in the message. The function decodes the message using the
-`abi.decode` function and returns the recipient address, the BTC recipient
-address, and the destination tokens. The function loops through the message and
-decodes token addresses.
-
-## Swap and Withdraw
-
-Finally, implement the `_doSwapAndWithdraw` function:
-
-```solidity filename="contracts/Multioutput.sol"
- function _doSwapAndWithdraw(
- address zrc20,
- uint256 amountToTransfer,
- address targetZRC20,
- bytes memory recipient
- ) internal {
- (address gasZRC20, uint256 gasFee) = IZRC20(targetZRC20)
- .withdrawGasFee();
-
- uint256 inputForGas = SwapHelperLib.swapTokensForExactTokens(
- systemContract,
- zrc20,
- gasFee,
- gasZRC20,
- amountToTransfer
- );
-
- uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
- systemContract,
- zrc20,
- amountToTransfer - inputForGas,
- targetZRC20,
- 0
- );
-
- IZRC20(gasZRC20).approve(targetZRC20, gasFee);
- IZRC20(targetZRC20).withdraw(recipient, outputAmount);
- }
-```
-
-`_doSwapAndWithdraw` mirrors the functionality of the omnichain swap example
-contract.
-
-It first calls the `withdrawGasFee` function to get the gas token and the gas
-fee.
-
-If `targetZRC20` is an ERC-20 token, `gasZRC20` will not be the same as
-`targetZRC20` and it’s important to have two swaps: the first swap is to swap
-the source token for the gas token, and the second swap is to swap the rest of
-the source token amount for the target token.
-
-If `targetZRC20` is a gas token, `gasZRC20` will be the same as `targetZRC20`.
-We could skip the first swap and just swap the source token for the target
-token, but for the sake of simplicity, we use the same logic for both cases.
-
-## Modify the Interact Task
-
-Modify the interact task to correctly handle BTC destination address and a list
-of destination token addresses. If BTC address is provided, it gets converted
-into bytes. Destination tokens are split into an array and encoded as addresses.
-
-```ts filename="tasks/interact.ts" {4-27, 32}
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const [signer] = await hre.ethers.getSigners();
-
- const destinationTokens = args.targetToken.split(",");
-
- let bitcoinAddress = "";
- let data;
- if (args.btcRecipient) {
- bitcoinAddress = args.btcRecipient;
- }
-
- const bitcoinAddressBytes = utils.solidityPack(["bytes"], [utils.toUtf8Bytes(bitcoinAddress)]);
-
- const tokensBytes = ethers.utils.concat(
- destinationTokens.map((address) => utils.defaultAbiCoder.encode(["address"], [address]))
- );
-
- data = prepareData(args.contract, ["address", "bytes", "bytes"], [args.recipient, bitcoinAddressBytes, tokensBytes]);
- //...
-};
-
-task("interact", "Interact with the contract", main).addOptionalParam("btcRecipient", "The bitcoin address to send to");
-```
-
-## Create an Account and Request Tokens from the Faucet
-
-Before proceeding with the next steps, make sure you have [created an account
-and requested ZETA
-tokens](/developers/tutorials/hello#create-an-account) from the
-faucet.
-
-## Deploy the Contract
-
-Clear the cache and artifacts, then compile the contract:
-
-```
-npx hardhat compile --force
-```
-
-```
-npx hardhat deploy --network zeta_testnet
-```
-
-```
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-
-🚀 Successfully deployed contract on ZetaChain.
-📜 Contract address: 0xa573Df1F0729FE6F1BD69b0a5dbFE393e6e09f47
-🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xa573Df1F0729FE6F1BD69b0a5dbFE393e6e09f47
-```
-
-## Interact with the Contract
-
-```
-npx hardhat interact --contract 0xa573Df1F0729FE6F1BD69b0a5dbFE393e6e09f47 --network bsc_testnet --target-token 0x05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0,0x65a45c57636f9BcCeD4fe193A602008578BcA90b --amount 5 --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 --btc-recipient tb1q8shzf7afc3rhw8n6w6ec32s8h6e2mrw077d0gg
-
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-
-🚀 Successfully broadcasted a token transfer transaction on bsc_testnet network.
-📝 Transaction hash: 0xf6895678629b99d756c8d4378d4d3e57e495c95edd39b2ab51cbd707bff75d59
-```
-
-```
-npx hardhat cctx 0xf6895678629b99d756c8d4378d4d3e57e495c95edd39b2ab51cbd707bff75d59
-✓ CCTXs on ZetaChain found.
-
-✓ 0x83dc34b68d36fbafb7a9c7eef51f3a19a5b842a39f1364f53091a236266d4ff1: 97 → 7001: OutboundMined (Remote omnichain contract call completed)
-✓ 0x25494fd66075c98a1ef1d13723fb0ca9029377580cf1c2847d0b786896d850cb: 7001 → 11155111: PendingOutbound → OutboundMined
-✓ 0xacc9d061b051c48b79b1dfec9e2ed3b7679af7ac225e8c995fab77b96d437bcc: 7001 → 18332: PendingOutbound → OutboundMined
-```
-
-## Source Code
-
-You can find the source code for the example in this tutorial here:
-
-https://github.com/zeta-chain/example-contracts/tree/main/omnichain/multioutput
diff --git a/src/pages/developers/tutorials/nft/_meta.json b/src/pages/developers/tutorials/nft/_meta.json
deleted file mode 100644
index eed76133..00000000
--- a/src/pages/developers/tutorials/nft/_meta.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "index": "NFT",
- "contract": "NFT Smart Contract",
- "frontend": "Frontend dApp"
-}
diff --git a/src/pages/developers/tutorials/nft/contract.mdx b/src/pages/developers/tutorials/nft/contract.mdx
deleted file mode 100644
index 519c519d..00000000
--- a/src/pages/developers/tutorials/nft/contract.mdx
+++ /dev/null
@@ -1,292 +0,0 @@
----
-title: NFT Smart Contract
----
-
-## Overview
-
-In this tutorial you will learn how to create an NFT omnichain smart contract
-that mints NFTs on ZetaChain in response to token deposits on connected chains.
-
-A user deposits a native gas token on one of the connected chains by sending it
-to the TSS address. This triggers an omnichain contract call on ZetaChain, and
-`onCrossChainCall` is called. The contract then mints an NFT with an `amount`
-property equal to the amount of tokens deposited, and a `chain` property equal
-to the chain ID of the chain that the deposit was made on. The NFT is sent to
-the user address on ZetaChain.
-
-A user may then send the NFT to another address on ZetaChain (as it is a regular
-ERC-721) or burn it.
-
-When an NFT is burned, the `amount` of tokens that it represents is withdrawn to
-a recipient (specified by the user when burning the NFT) on the `chain` from
-which the NFT was minted.
-
-## Set Up Your Environment
-
-Clone the Hardhat contract template:
-
-```
-git clone https://github.com/zeta-chain/template
-```
-
-Install dependencies:
-
-```
-cd template/contracts
-yarn
-```
-
-## Create the contract
-
-Run the following command to create a new omnichain contract called `NFT`.
-
-```
-npx hardhat omnichain NFT recipient:address
-```
-
-## Omnichain Contract
-
-```solidity filename="contracts/NFT.sol" {6-7,10,12,15-18,20,22,31-39}
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
-
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
-import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
-
-contract NFT is zContract, ERC721, OnlySystem {
- SystemContract public systemContract;
- error CallerNotOwnerNotApproved();
- uint256 constant BITCOIN = 18332;
-
- mapping(uint256 => uint256) public tokenAmounts;
- mapping(uint256 => uint256) public tokenChains;
-
- uint256 private _nextTokenId;
-
- constructor(address systemContractAddress) ERC721("MyNFT", "MNFT") {
- systemContract = SystemContract(systemContractAddress);
- _nextTokenId = 0;
- }
-
- function onCrossChainCall(
- zContext calldata context,
- address zrc20,
- uint256 amount,
- bytes calldata message
- ) external override onlySystem(systemContract) {
- address recipient;
-
- if (context.chainID == BITCOIN) {
- recipient = BytesHelperLib.bytesToAddress(message, 0);
- } else {
- recipient = abi.decode(message, (address));
- }
-
- _mintNFT(recipient, context.chainID, amount);
- }
-}
-```
-
-Import OpenZeppelin's ERC-721 implementation and the `Counters` library. You
-will use the `Counters` library to keep track of the token IDs. You will also
-import the `BytesHelperLib` from the ZetaChain toolkit. This library will be
-used for decoding data sent from other chains.
-
-The `NFT` contract inherits from `zContract` and `ERC721`. The `zContract`
-interface is required for omnichain contracts and the `ERC721` interface is
-required for NFTs.
-
-Create a new `BITCOIN` constant that is set to the chain ID of the Bitcoin
-testnet.
-
-Create two mappings: `tokenAmounts` that maps token IDs to the amount of tokens
-that the NFT represents and `tokenChains` that maps token IDs to the chain ID of
-the chain that the NFT was minted on.
-
-Create a `systemContract` variable that is set to the address of the
-`SystemContract` contract.
-
-Modify the constructor to call the `ERC721` constructor with the name and symbol
-of the NFT.
-
-Let's now take a look at the `onCrossChainCall` function. This function is
-called when a user deposits tokens from a connected chain.
-
-The only value that will be passed in the `message` parameter is the recipient
-address.
-
-For Bitcoin this is important, because NFTs are minted on ZetaChain, which uses
-hex addresses, but the `context.origin` contains a bech32 Bitcoin address. Since
-we cannot derive the hex address from the bech32 address, we pass the hex
-address as a parameter in the `message` parameter.
-
-For EVM chains, passing the recipient address in the `message` parameter is not
-strictly necessary, because the `context.origin` contains the hex address of the
-sender, but we will do it anyway for consistency. And it allows users to specify
-a different recipient address than the sender address.
-
-In the `onCrossChainCall` function, decode the `message` parameter to get the
-recipient address. Then call the `_mintNFT` function to mint the NFT.
-
-## Minting
-
-Create a new `_mintNFT` function that takes a `recipient` address, a `chainId`,
-and an `amount` as parameters. This function is private, because it is only
-called from the `onCrossChainCall` function.
-
-```solidity filename="contracts/NFT.sol"
- function _mintNFT(
- address recipient,
- uint256 chainId,
- uint256 amount
- ) private {
- uint256 tokenId = _nextTokenId;
- _safeMint(recipient, tokenId);
- tokenChains[tokenId] = chainId;
- tokenAmounts[tokenId] = amount;
- _nextTokenId++;
- }
-```
-
-The function mints a new NFT and stores the `chainId` and `amount` in the
-`tokenChains` and `tokenAmounts` mappings. It then increments the token ID
-counter.
-
-## Burning
-
-Create a new `burn` function that takes a `tokenId` and a `recipient` address as
-parameters. This function is public, because it is called by the user when they
-want to burn an NFT and withdraw the tokens that it represents.
-
-```solidity filename="contracts/NFT.sol"
- function burnNFT(uint256 tokenId, bytes memory recipient) public {
- if (!_isApprovedOrOwner(_msgSender(), tokenId)) {
- revert CallerNotOwnerNotApproved();
- }
- address zrc20 = systemContract.gasCoinZRC20ByChainId(
- tokenChains[tokenId]
- );
-
- (, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee();
-
- IZRC20(zrc20).approve(zrc20, gasFee);
- IZRC20(zrc20).withdraw(recipient, tokenAmounts[tokenId] - gasFee);
-
- _burn(tokenId);
- delete tokenAmounts[tokenId];
- delete tokenChains[tokenId];
- }
-```
-
-The function first checks that the caller is the owner of the NFT. It then
-retrieves the gas token ZRC-20 address for the chain that the NFT was minted on.
-It then withdraws the tokens that the NFT represents to the `recipient` address.
-The amount of tokens that the NFT represents minus the gas fee is withdrawn.
-
-## Compile and Deploy the Contract
-
-```
-npx hardhat compile --force
-```
-
-```
-npx hardhat deploy --network zeta_testnet
-```
-
-```
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
-
-🚀 Successfully deployed contract on ZetaChain.
-📜 Contract address: 0xb9647Fbb6562A0049CE3b425228dC59218F3b93c
-🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xb9647Fbb6562A0049CE3b425228dC59218F3b93c
-```
-
-## Configure Goldsky for Event Indexing
-
-When NFTs are minted, transferred or burned on ZetaChain, the ZetaChain protocol
-emits events. Since the contract cannot return all NFTs that belong to a user,
-you need to index these events to be able to display the NFTs. Goldsky is a
-subgraph indexer that indexes events.
-
-To configure Goldsky to index events for your contract, create a `goldsky.json`
-file in the root of your project:
-
-```json filename="goldsky.json
-{
- "version": "1",
- "name": "NFT",
- "abis": {
- "NFT": {
- "path": "artifacts/contracts/NFT.sol/NFT.json"
- }
- },
- "chains": ["zetachain-testnet"],
- "instances": [
- {
- "abi": "NFT",
- "address": "0x7a984BD3ce37257e0124A3c0d25857df5E258Be2", // Your contract address
- "chain": "zetachain-testnet",
- "startBlock": 3241788 // The block number that your contract was deployed on
- }
- ]
-}
-```
-
-Install Goldsky, login and deploy the subgraph:
-
-```
-curl https://goldsky.com | sh
-
-goldsky login
-
-goldsky subgraph deploy nft/v1 --from-abi goldsky.json
-```
-
-Copy the URL returned by the `goldsky subgraph deploy` command, you will need it
-in the next section when building the frontend.
-
-To learn more about setting up Goldsky, read [the
-guide](/developers/services/goldsky).
-
-## Mint an NFT
-
-Use the `interact` command to mint an NFT. The `--contract` parameter is the
-address of the contract that you just deployed. The `--amount` parameter is the
-amount of tokens that you want to deposit. The `--recipient` parameter is the
-address that you want to receive the NFT on ZetaChain.
-
-```
-npx hardhat interact --contract 0xb9647Fbb6562A0049CE3b425228dC59218F3b93c --amount 0.01 --network sepolia_testnet --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
-```
-
-```
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
-
-🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet network.
-📝 Transaction hash: 0x8e0c9edd2a570494b8610c99d9772cefd4fb3a5ebb42bb714f83ef898ff53881
-```
-
-Track the transaction using the `cctx` command:
-
-```
-npx hardhat cctx 0x8e0c9edd2a570494b8610c99d9772cefd4fb3a5ebb42bb714f83ef898ff53881
-
-✓ CCTXs on ZetaChain found.
-
-✓ 0xcd894e7299d80b6bf04c7a0b1589f0e5e1b4bcdbd691eb780429ceb359006d59: 11155111 → 7001: OutboundMined (Remote omnichain contract call completed)
-```
-
-## Limitations
-
-- Even though the minting process is initiated on connected chains, NFTs will be
- minted on ZetaChain.
-- During minting and burning only fungible tokens are transferred between
- blockchains. The NFT itself is not transferred.
-
-## Next Steps
-
-In the next tutorial you will learn how to build a user interface for your NFT
-omnichain contract that allows users to mint, burn and view NFTs.
diff --git a/src/pages/developers/tutorials/nft/frontend.mdx b/src/pages/developers/tutorials/nft/frontend.mdx
deleted file mode 100644
index a7a17cb4..00000000
--- a/src/pages/developers/tutorials/nft/frontend.mdx
+++ /dev/null
@@ -1,148 +0,0 @@
----
-title: Frontend dApp
----
-
-import { CodeBlock } from "~/components/shared";
-
-## Overview
-
-In this section, you will create a frontend dApp that allows users to deposit
-native tokens on connected chains to mint NFTs on ZetaChain, view their
-collection, burn NFTs to withdraw native tokens from ZetaChain and transfer NFTs
-to other addresses on ZetaChain.
-
-This UI is built as part of
-[the example frontend project](https://github.com/zeta-chain/example-frontend),
-but you can replicate it in your own project.
-
-![Frontend dApp](/img/docs/nft.png)
-
-## Prerequisites
-
-- Complete the
- [NFT Omnichain Contract](/developers/tutorials/nft/contract)
- tutorial. Make sure that you've configured Goldsky to index events from your
- contract.
-
-## Page Layout
-
-The entry point of the frontend dApp is the `NFTPage` component. It is
-responsible for rendering the page layout and the widgets that trigger
-broadcasting of transactions.
-
-The `NFTPage` component uses the `useNFT` hook to access the state of the
-frontend dApp. The `useNFT` hook is responsible for managing the state of the
-frontend dApp and for broadcasting transactions.
-
-On component load the `NFTPage` component calls the `fetchNFTs` function to
-fetch the NFTs owned by the connected address.
-
-
-
-## useNFT Hook
-
-`useNFT` is a simple component that stores the state of the frontend dApp.
-
-
-
-## Fetching NFTs
-
-The hook is responsible for fetching NFTs owned by the connected address.
-
-Set the constant to store the URL of the subgraph that you deployed in the
-previous section.
-
-Goldsky is a subgraph indexer that uses GraphQL to query events from your
-contract. Write a query that fetches all `Transfer` events. You're interested in
-transfers that were sent to or from a specific address. In the body of the query
-specify the fields your're interested in.
-
-The function to fetch NFT makes a GraphQL request to the subgraph to fetch the
-`transfer` events. The function then filters the events to find the NFTs that
-are currently owned by the address by checking which NFTs were sent to the
-address and which NFTs were sent from the address.
-
-Since you're using the default ERC-721 Transfer event, you only have the
-`tokenId` field that contains the ID of the NFT. You will need to query the
-contract to get the chain ID and the amount of the NFT.
-
-To correctly format the amount of tokens in NFTs, you to know the number of
-decimals of the corresponding ZRC-20 token. Query the foreign coins endpoints to
-find the decimals and use to correctly format the amounts.
-
-Finally, sort NFTs by ID in descending order.
-
-
-
-## Minting NFTs
-
-To mint an NFT on ZetaChain you need to deposit native tokens on a connected
-chain to the TSS address.
-
-For Bitcoin use the supported XDEFI wallet. Format the request correctly with
-the recipient (TSS address), amount (in sats) and memo (the address that will
-receive the NFT on ZetaChain), and call the `request` method.
-
-For EVM chains, just use `sendTransaction` to send a token transfer transaction
-to the TSS address with the amount of tokens that you want to deposit and the
-recipient address in the data field.
-
-Next, if the transaction was successful, use the transaction hash to track the
-progress of the cross-chain transaction.
-
-
-
-## Burning NFTs
-
-Burning NFTs happens on ZetaChain. To burn an NFT, you need to approve an NFT
-first, then call the `burn` method on the omnichain contract. The `burn` method
-takes the ID of the NFT as an argument.
-
-Due to fluctuating gas prices, a transaction might fail. To make sure that the
-user has best experience possible, implement a recursive check for approval.
-This function will check if a user has approved the omnichain contract to burn
-an NFT. If the user has not approved the contract, the function will ask for an
-approval. Implement a similar function to check for ownership of the NFT.
-
-In the body of the function call the approval function, once the approval is
-received, call burn the NFT and start checking for ownership. As soon as the
-user is no longer the owner of the NFT, update the frontend dApp state.
-
-
-
-## Transferring NFTs
-
-Transferring NFTS happens on ZetaChain and does not involve any cross-chain
-interactions. Simple call the contract's transfer method and pass the recipient,
-the sender and the ID of the NFT.
-
-
-
-## Congratulations!
-
-You've successfully created a frontend dApp that allows users to mint, burn and
-transfer NFTs on ZetaChain.
diff --git a/src/pages/developers/tutorials/nft/index.mdx b/src/pages/developers/tutorials/nft/index.mdx
deleted file mode 100644
index 0a9e2049..00000000
--- a/src/pages/developers/tutorials/nft/index.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: NFT
-pageType: sub-category
----
-
-import { CurrentPageNavigationSections } from "~/components/shared";
-
-
-
-
diff --git a/src/pages/developers/tutorials/staking.mdx b/src/pages/developers/tutorials/staking.mdx
deleted file mode 100644
index 15839209..00000000
--- a/src/pages/developers/tutorials/staking.mdx
+++ /dev/null
@@ -1,575 +0,0 @@
----
-title: Staking
----
-
-## Overview
-
-In this tutorial you will create an omnichain contract that will be capable of
-receiving tokens from connected chains and staking them on ZetaChain. Native
-tokens deposited to ZetaChain as ZRC-20s will be locked in the contract until
-withdrawn by the staker. Rewards will be accrued at a fixed rate proportionally
-to amount of tokens staked.
-
-The staker is the one depositing tokens to the contract. The staker is required
-to provide a beneficiary address to which the rewards will be sent (a staker is
-allowed to be its own beneficiary). Only the staker can withdraw the staked
-tokens from the contract and withdraw to the chain from which they originate.
-
-Only the beneficiary can claim the rewards from the contract.
-
-For simplicity this contract will be compatible with one of the connected chains
-at a time. The chain ID of the connected chain will be passed to the contract
-constructor and will be used to check that the contract is called from a
-compatible chain.
-
-This tutorial shows how to:
-
-- create an omnichain contract that can receive tokens from connected chains
-- use the parameters of the `onCrossChainCall` function to:
- - decode the staker address from the `context.origin` parameter
- - decode the contents of the `message` parameter
- - determine the chain ID from `context.chainID`
-- dispatch different logic using an action code
-- how to withdraw tokens correctly both to EVM-based chains and to Bitcoin
-
-## Prerequisites
-
-- [Node.js](https://nodejs.org/en/) (version 18 or above)
-- [Yarn](https://yarnpkg.com/)
-- [Git](https://git-scm.com/)
-
-## Set Up Your Environment
-
-Clone the Hardhat contract template:
-
-```
-git clone https://github.com/zeta-chain/template
-```
-
-Install dependencies:
-
-```
-cd template/contracts
-yarn
-```
-
-## Create the Contract
-
-To create a new omnichain contract you will use the `omnichain` Hardhat task and
-pass a contract name (`Staking`) to the task:
-
-```
-npx hardhat omnichain Staking
-```
-
-## Omnichain Contract
-
-### Design the Omnichain Contract
-
-To implement the functionality described in the introduction, you will need to
-handle the following actions.
-
-Called from a connected chain by the staker:
-
-- Staking tokens by depositing them into the staking omnichain contract on
- ZetaChain
-- Unstaking tokens by withdrawing them to the chain from which they originate
-- Setting the beneficiary address
-
-Called on ZetaChain:
-
-- Claiming rewards by the beneficiary
-- Querying the pending rewards
-
-Since the omnichain contract has only one function that gets called when the
-contract is triggered from a connected chain (`onCrossChainCall`), and you need
-to be able to execute different logic depending on the action, you will need to
-encode the action code into the `message` parameter of the `onCrossChainCall`.
-
-Even though the `message` value will be encoded differently in EVM-based chains
-and Bitcoin, the first bytes of the `message` will always be the action code
-encoded as `uint8`.
-
-```mermaid
-flowchart LR
- subgraph zetachain ["ZetaChain"]
- contract("Staking contract")
- system_contract("System contract")
- end
- subgraph connected_chain ["Connected chain"]
- tss("TSS address")
- end
- user("User")
- user -- stake --> tss
- user -- unstake --> tss
- user -- set beneficiary --> tss
- tss --> system_contract
- system_contract -- calls --> contract
- user -- claim rewards --> contract
- user -- query rewards --> contract
-```
-
-### Handle the Omnichain Contract Call
-
-```solidity filename="contracts/Staking.sol"
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
-
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
-
-contract Staking is ERC20, zContract, OnlySystem {
- SystemContract public systemContract;
- uint256 public immutable chainID;
- uint256 constant BITCOIN = 18332;
-
- uint256 public rewardRate = 1;
-
- error WrongChain(uint256 chainID);
- error UnknownAction(uint8 action);
- error Overflow();
- error Underflow();
- error WrongAmount();
- error NotAuthorized();
- error NoRewardsToClaim();
-
- mapping(bytes => uint256) public stakes;
- mapping(bytes => address) public beneficiary;
- mapping(bytes => uint256) public lastStakeTime;
-
- constructor(
- string memory name_,
- string memory symbol_,
- uint256 chainID_,
- address systemContractAddress
- ) ERC20(name_, symbol_) {
- systemContract = SystemContract(systemContractAddress);
- chainID = chainID_;
- }
-
- function onCrossChainCall(
- zContext calldata context,
- address zrc20,
- uint256 amount,
- bytes calldata message
- ) external virtual override onlySystem(systemContract) {
- if (chainID != context.chainID) {
- revert WrongChain(context.chainID);
- }
-
- uint8 action = chainID == BITCOIN
- ? uint8(message[0])
- : abi.decode(message, (uint8));
-
- if (action == 1) {
- stake(context.origin, amount, message);
- } else if (action == 2) {
- unstake(context.origin);
- } else if (action == 3) {
- updateBeneficiary(context.origin, message);
- } else {
- revert UnknownAction(action);
- }
- }
-}
-```
-
-First, import the `ERC20` contract from OpenZeppelin to manage our ERC20 staking
-reward token. Import `BytesHelperLib` from ZetaChain's toolkit for utility
-functions to convert bytes into addresses and vice versa.
-
-Add the `chainID` variable to store the ID of the connected chain. This variable
-will be set in the constructor and will be used to check that the contract is
-called from the correct chain.
-
-Add the `BITCOIN` constant to store the chain ID of Bitcoin. ZetaChain uses
-`18332` to represent Bitcoin's chain ID.
-
-The contract needs to store the following mappings:
-
-- `stakes` - stores the staker's staked balance
-- `beneficiary` - stores the staker's beneficiary address
-- `lastStakeTime` - stores the timestamp of the last staking action
-
-Modify the constructor to accept three additional arguments: `name_`, `symbol_`,
-and `chainID_`. The `name_` and `symbol_` arguments will be used to initialize
-the `ERC20` contract. The `chainID_` argument will be used to set the `chainID`
-variable.
-
-`onCrossChainCall` is the function that will be called by the system contract
-when a user triggers the omnichain contract from a connected chain.
-
-First, check that the omnichain contract is called from the same connected chain
-as the one specified in the constructor. You can use `context.chainID` to get
-the chain ID of the connected chain from which the omnichain contract was
-called.
-
-`context.origin` contains information about the address from which the
-transaction that triggered the omnichain contract was broadcasted.
-
-For EVM-based chains, `context.origin` is the actual address of the account
-which broadcasted the transaction. For example:
-
-```
-0x2cD3D070aE1BD365909dD859d29F387AA96911e1
-```
-
-For Bitcoin, `context.origin` is the hexadecimal representation of the Bitcoin
-address. For example, if the Bitcoin address is:
-
-```
-tb1q2dr85d57450xwde6560qyhj7zvzw9895hq25tx
-```
-
-The hexadecimal representation of the address is:
-
-```
-0x74623171326472383564353734353078776465363536307179686a377a767a7739383935687132357478
-```
-
-The `message` parameter contains the data that was passed to the omnichain
-contract when it was called from the connected chain. In our design the first
-value in the message is the `action` code. For Bitcoin take the first byte of
-the message and convert it to an unsigned 8-bit integer (`uint8`). For EVM-based
-chains, use `abi.decode` to decode the first value of the message as a `uint8`.
-
-Finally, based on the `action` code, call the corresponding function:
-
-`1` - stake ZRC-20 tokens
-
-`2` - unstake ZRC-20 tokens
-
-`3` - update beneficiary address
-
-### Update Beneficiary
-
-`updateBeneficiary` is a function that will be called by the staking function to
-set the beneficiary address or can be optionally called by the staker.
-
-The `message` is encoded differently in EVM-based chains and Bitcoin. For
-Bitcoin, the beneficiary address follows the `uint8` action code (1 byte long)
-and is 20 bytes long. Use `bytesToAddress` with an offset of `1` (byte) to
-decode the beneficiary address.
-
-For EVM-based chains, use `abi.decode` to get the beneficiary address from the
-`message`.
-
-```solidity filename="contracts/Staking.sol"
- function updateBeneficiary(
- bytes memory staker,
- bytes calldata message
- ) internal {
- address beneficiaryAddress;
- if (chainID == BITCOIN) {
- beneficiaryAddress = BytesHelperLib.bytesToAddress(message, 1);
- } else {
- (, beneficiaryAddress) = abi.decode(message, (uint8, address));
- }
- beneficiary[staker] = beneficiaryAddress;
- }
-```
-
-### Stake ZRC-20 Tokens
-
-`stake` is a function that will be called by `onCrossChainCall` to stake the
-deposited tokens.
-
-```solidity filename="contracts/Staking.sol"
- function stake(
- bytes memory staker,
- uint256 amount,
- bytes calldata message
- ) internal {
- updateBeneficiary(staker, message);
-
- stakes[staker] += amount;
- if (stakes[staker] < amount) revert Overflow();
-
- updateRewards(staker);
- }
-
- function updateRewards(bytes memory staker) internal {
- if (lastStakeTime[staker] == 0) lastStakeTime[staker] = block.timestamp;
- uint256 rewardAmount = queryRewards(staker);
-
- _mint(beneficiary[staker], rewardAmount);
- lastStakeTime[staker] = block.timestamp;
- }
-
- function queryRewards(bytes memory staker) public view returns (uint256) {
- uint256 timeDifference = block.timestamp - lastStakeTime[staker];
- uint256 rewardAmount = timeDifference * stakes[staker] * rewardRate;
- return rewardAmount;
- }
-```
-
-`stake` increases the staker's balance in the contract. The function also calls
-the `updateRewards` function to update the rewards for the staker.
-
-`updateRewards` calculates the rewards for the staker and mints them to the
-beneficiary address. The function also updates the timestamp of when the staking
-happened last.
-
-### Unstake ZRC-20 Tokens
-
-The `unstake` function begins by updating any outstanding rewards due to the
-user. It then checks that the user has a sufficient staked balance.
-Subsequently, it identifies the ZRC20 token associated with the contract's
-`chainID` and determines the gas fee for the unstaking operation. This fee is
-then approved. The user's tokens, minus the gas fee, are withdrawn to the
-encoded recipient address. Finally, the contract updates the user's staking
-balance and the timestamp of their last stake action.
-
-```solidity filename="contracts/Staking.sol"
- function unstake(bytes memory staker) internal {
- uint256 amount = stakes[staker];
-
- updateRewards(staker);
-
- address zrc20 = systemContract.gasCoinZRC20ByChainId(chainID);
- (, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee();
-
- if (amount < gasFee) revert WrongAmount();
-
- stakes[staker] = 0;
-
- IZRC20(zrc20).approve(zrc20, gasFee);
- IZRC20(zrc20).withdraw(staker, amount - gasFee);
-
- if (stakes[staker] > amount) revert Underflow();
-
- lastStakeTime[staker] = block.timestamp;
- }
-```
-
-### Claim Rewards
-
-`claimRewards` is a function that will be called by the beneficiary to claim the
-rewards. The function checks that the caller is the beneficiary and calls the
-`updateRewards` function to send rewards to the beneficiary.
-
-```solidity filename="contracts/Staking.sol"
- function claimRewards(bytes memory staker) external {
- if (beneficiary[staker] != msg.sender) revert NotAuthorized();
- uint256 rewardAmount = queryRewards(staker);
- if (rewardAmount <= 0) revert NoRewardsToClaim();
- updateRewards(staker);
- }
-```
-
-## Modify the Task to Deploy the Contract
-
-Modify the deploy task to accept an additional `--chain` flag that will specify
-the connected chain. The `--chain` flag is used to determine the chain ID of the
-connected chain and pass it as an argument to the constructor.
-
-It is also used to get the symbol of the ZRC-20 token on the connected chain.
-
-```ts filename="tasks/deploy.ts" {1,7-26,32-36}
-import ZRC20 from "@zetachain/protocol-contracts/abi/zevm/ZRC20.sol/ZRC20.json";
-
-// ...
-
-const factory = await hre.ethers.getContractFactory("Staking");
-
-let symbol, chainID;
-if (args.chain === "btc_testnet") {
- symbol = "BTC";
- chainID = 18332;
-} else {
- const zrc20 = getAddress("zrc20", args.chain);
- const contract = new hre.ethers.Contract(zrc20, ZRC20.abi, signer);
- symbol = await contract.symbol();
- chainID = hre.config.networks[args.chain]?.chainId;
- if (chainID === undefined) {
- throw new Error(`🚨 Chain ${args.chain} not found in hardhat config.`);
- }
-}
-
-const contract = await factory.deploy(
- `Staking rewards for ${symbol}`,
- `R${symbol.toUpperCase()}`,
- chainID,
- systemContract
-);
-
-await contract.deployed();
-
-// ...
-
-task("deploy", "Deploy the contract", main).addParam("chain", "Chain ID (use btc_testnet for Bitcoin Testnet)");
-```
-
-## Tasks to Interact with the Contract
-
-To make it easier to interact with the contract, create a few tasks that will:
-
-- stake tokens
-- unstake tokens
-- set beneficiary address
-
-You can find the source code for the tasks in the `tasks` directory of the
-project:
-
-https://github.com/zeta-chain/example-contracts/tree/main/omnichain/staking/tasks
-
-When copying these files make sure to also the helper function
-`convertToHexAddress.ts` in the `lib` directory.
-
-To perform the actions above, the tasks simply send transactions to the TSS
-address on the connected chain with the encoded data in the `data` field of the
-transactions.
-
-According to the design, the first byte of the `message` parameter is the action
-code. The rest of the `message` is the data that is required for the action.
-
-For example, to stake tokens, the `data` is just the `uint8` action code:
-
-```ts
-const data = prepareData(args.contract, ["uint8"], ["1"]);
-```
-
-To set the beneficiary address, the `data` is the `uint8` action code followed
-by the beneficiary address:
-
-```ts
-const data = prepareData(args.contract, ["uint8", "address"], ["3", args.beneficiary]);
-```
-
-Under the hood `prepareData` will also add the omnichain contract address to the
-beginning of the `data` to make sure the transaction is sent to the right
-contract on ZetaChain.
-
-The `stake` task requires you to send tokens specified by the `amount` argument.
-These tokens will be transferred to and locked in the TSS address, and your
-staking contract will receive the same amount of tokens as ZRC-20s.
-
-Other actions do not require you to send tokens, so the `amount` argument can be
-`"0"`.
-
-For Bitcoin, create an `addresses` task that will help you convert between
-bech32 address and a hexadecimal representation of the address.
-
-```ts filename="tasks/addresses.ts"
-import { task } from "hardhat/config";
-import { HardhatRuntimeEnvironment } from "hardhat/types";
-import { utils } from "ethers";
-
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const dataTypes = ["bytes"];
- const values = [utils.toUtf8Bytes(args.address)];
-
- const encodedData = utils.solidityPack(dataTypes, values);
- console.log(`Encoded: ${encodedData}`);
- console.log(`context.origin: ${encodedData.slice(0, 42)}`);
-};
-
-task("address", "Encode a Bitcoin bech32 address to hex", main).addPositionalParam("address");
-```
-
-Before proceeding make sure to import all newly created tasks in
-`hardhat.config.ts`.
-
-## Create an Account and Request Tokens from the Faucet
-
-Before proceeding with the next steps, make sure you have
-[created an account and requested ZETA tokens](/developers/tutorials/hello#create-an-account)
-from the faucet.
-
-## Interact with the Contract from an EVM-based Chain
-
-### Deploying the Contract
-
-Clear the cache and artifacts, then compile the contract:
-
-```
-npx hardhat compile --force
-```
-
-Use the `deploy` task to deploy the contract to ZetaChain with `--chain` flag
-specifying Sepolia testnet:
-
-```
-npx hardhat deploy --network zeta_testnet --chain sepolia_testnet
-```
-
-### Stake Tokens
-
-```
-npx hardhat stake --amount 0.1 --beneficiary ADDRESS --contract ADDRESS --network sepolia_testnet
-```
-
-### Unstake Tokens
-
-```
-npx hardhat unstake --contract ADDRESS --network sepolia_testnet
-```
-
-## Interact with the Contract from Bitcoin
-
-### Deploying the Contract
-
-Clear the cache and artifacts, then compile the contract:
-
-```
-npx hardhat compile --force
-```
-
-Use the `deploy` task to deploy the contract to ZetaChain with `--chain` flag
-specifying the Bitcoin testnet:
-
-```
-npx hardhat deploy --network zeta_testnet --chain btc_testnet
-
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
-
-🚀 Successfully deployed contract on ZetaChain.
-📜 Contract address: 0x57cafCe6802c45F682201b93529B09EfB9A492C3
-🌍 Explorer: https://athens3.explorer.zetachain.com/address/0x57cafCe6802c45F682201b93529B09EfB9A492C3
-```
-
-### Stake Tokens
-
-Pass your contract address (without the `0x` prefix), the action code for
-staking tokens (`01`), beneficiary address
-(`2cD3D070aE1BD365909dD859d29F387AA96911e1`) to the memo flag. Specify the
-`--amount` of tBTC you want to transfer to your omnichain contract.
-
-```
-npx hardhat send-btc --memo 57cafCe6802c45F682201b93529B09EfB9A492C3012cD3D070aE1BD365909dD859d29F387AA96911e1 --amount 0.01 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
-```
-
-### Unstake Tokens
-
-Pass your contract address (without the `0x` prefix) and the action code for
-unstaking tokens (`02`) to the memo flag.
-
-```
-npx hardhat send-btc --memo 57cafCe6802c45F682201b93529B09EfB9A492C302 --amount 0.0 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
-```
-
-## Summary
-
-Congratulations! 🎉 You have created an omnichain staking contract and learned:
-
-- how to make your contract compatible both with EVM-based chains and Bitcoin
-- how use the `context.chainID` to get chain ID and the `context.origin` to get
- an identifier for the sender
-- that the `context.origin` represents the actual sender address on EVM-based
- chains and the first 20 bytes of the hexadecimal representation of the Bitcoin
- address on Bitcoin
-- the pattern for encoding the action code into the `message` parameter of the
- `onCrossChainCall` function to execute different logic from the same function
-- how to use the `BytesHelperLib` from ZetaChain's toolkit to convert bytes into
- an address.
-- how to implement and use `bytesToBech32Bytes` to take the bytes corresponding
- to the hexadecimal representation of the Bitcoin address from the `message`.
-
-## Source Code
-
-You can find the source code for the example in this tutorial here:
-
-https://github.com/zeta-chain/example-contracts/tree/main/omnichain/staking
diff --git a/src/pages/developers/tutorials/swap-any.mdx b/src/pages/developers/tutorials/swap-any.mdx
index 525a59d5..7d4f3ce2 100644
--- a/src/pages/developers/tutorials/swap-any.mdx
+++ b/src/pages/developers/tutorials/swap-any.mdx
@@ -1,44 +1,77 @@
---
-title: Swap To Any Token
+title: Swap to Any Token
---
-In the previous [Swap](/developers/tutorials/swap) tutorial, you learned how to
-create universal swap contracts that enable users to exchange tokens from one
-connected blockchain for a token on another blockchain, with the target token
-always withdrawn to the destination chain.
+import { Alert } from "~/components/shared";
-In this tutorial, you will enhance the swap contract to support swapping tokens
-to any token (such as ZRC-20, ERC-20, or ZETA) and provide the flexibility to
-either withdraw the token to the destination chain or keep it on ZetaChain.
+In the previous [Swap](/developers/tutorials/swap) tutorial, you created a
+universal swap contract that allows users to exchange tokens from one connected
+blockchain for a token on another blockchain. In that implementation, the
+swapped token was always withdrawn to the destination chain. This tutorial
+expands on that by enhancing the contract to support swapping tokens to any
+token (such as ZRC-20, ERC-20, or ZETA) and offering users the flexibility to
+either withdraw the token to the destination chain or retain it on ZetaChain.
-Keeping swapped tokens on ZetaChain is useful if you want to use ZRC-20 in
-non-universal contracts that don't yet have the capacity to accept tokens from
-connected chains directly, or if the destination token is ZETA, which you want
-to keep on ZetaChain.
+The ability to keep swapped tokens on ZetaChain can be particularly useful if
+you intend to utilize ZRC-20 tokens in non-universal contracts that aren't yet
+equipped to accept tokens from connected chains. It is also useful if the
+destination token is ZETA, which you may want to keep on ZetaChain for further
+use.
-## Omnichain Contract
+In this enhanced version, you will modify the original swap contract to support
+this additional functionality. You will also deploy the modified contract to
+localnet and interact with it by swapping tokens from a connected EVM chain.
-Copy the existing swap example into a new file `SwapToAnyToken.sol` and make the
-necessary changes:
+
+ {" "}
+ This tutorial depends on the gateway, which is available on localnet but not yet deployed on testnet. It will be compatible
+ with testnet after the gateway is deployed. In other words, you cannot deploy this tutorial on testnet yet.{" "}
+
-```solidity filename="contracts/SwapToAnyToken.sol" {8,11,23,37,45-47,49-56,64,73,79,94,99-114,117-127}
+## Setting Up Your Environment
+
+To get started, clone the example contracts repository and install the
+dependencies by running the following commands:
+
+```
+git clone https://github.com/zeta-chain/example-contracts
+
+cd example-contracts/examples/swap
+
+yarn
+```
+
+## Understanding the SwapToAnyToken Contract
+
+The `SwapToAnyToken` contract builds on the previous swap contract by allowing
+users to swap tokens to any target token and giving them the option to either
+withdraw the swapped tokens to the destination chain or keep them on ZetaChain.
+This added flexibility makes the contract more versatile for a variety of use
+cases.
+
+```solidity
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
+pragma solidity 0.8.26;
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@zetachain/toolkit/contracts/SwapHelperLib.sol";
-import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
+import {SystemContract, IZRC20} from "@zetachain/toolkit/contracts/SystemContract.sol";
+import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol";
+import {BytesHelperLib} from "@zetachain/toolkit/contracts/BytesHelperLib.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IWZETA.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
+import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
-contract SwapToAnyToken is zContract, OnlySystem {
+contract SwapToAnyToken is UniversalContract {
SystemContract public systemContract;
-
+ GatewayZEVM public gateway;
uint256 constant BITCOIN = 18332;
- constructor(address systemContractAddress) {
+ constructor(address systemContractAddress, address payable gatewayAddress) {
systemContract = SystemContract(systemContractAddress);
+ gateway = GatewayZEVM(gatewayAddress);
}
struct Params {
@@ -47,14 +80,12 @@ contract SwapToAnyToken is zContract, OnlySystem {
bool withdraw;
}
- receive() external payable {}
-
function onCrossChainCall(
zContext calldata context,
address zrc20,
uint256 amount,
bytes calldata message
- ) external virtual override onlySystem(systemContract) {
+ ) external virtual override {
Params memory params = Params({
target: address(0),
to: bytes(""),
@@ -99,42 +130,60 @@ contract SwapToAnyToken is zContract, OnlySystem {
uint256 inputForGas;
address gasZRC20;
uint256 gasFee;
+ uint256 swapAmount = amount;
if (withdraw) {
(gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();
- inputForGas = SwapHelperLib.swapTokensForExactTokens(
- systemContract,
- inputToken,
- gasFee,
- gasZRC20,
- amount
- );
+ if (gasZRC20 == inputToken) {
+ swapAmount = amount - gasFee;
+ } else {
+ inputForGas = SwapHelperLib.swapTokensForExactTokens(
+ systemContract,
+ inputToken,
+ gasFee,
+ gasZRC20,
+ amount
+ );
+ swapAmount = amount - inputForGas;
+ }
}
uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
systemContract,
inputToken,
- withdraw ? amount - inputForGas : amount,
+ swapAmount,
targetToken,
0
);
if (withdraw) {
- IZRC20(gasZRC20).approve(targetToken, gasFee);
- IZRC20(targetToken).withdraw(recipient, outputAmount);
- } else {
- address wzeta = systemContract.wZetaContractAddress();
- if (targetToken == wzeta) {
- IWETH9(wzeta).withdraw(outputAmount);
- address payable recipientAddress = payable(
- address(uint160(bytes20(recipient)))
+ if (gasZRC20 == targetToken) {
+ IZRC20(gasZRC20).approve(
+ address(gateway),
+ outputAmount + gasFee
);
- recipientAddress.transfer(outputAmount);
} else {
- address recipientAddress = address(uint160(bytes20(recipient)));
- IWETH9(targetToken).transfer(recipientAddress, outputAmount);
+ IZRC20(gasZRC20).approve(address(gateway), gasFee);
+ IZRC20(targetToken).approve(address(gateway), outputAmount);
}
+ gateway.withdraw(
+ recipient,
+ outputAmount,
+ targetToken,
+ RevertOptions({
+ revertAddress: address(0),
+ callOnRevert: false,
+ abortAddress: address(0),
+ revertMessage: "",
+ onRevertGasLimit: 0
+ })
+ );
+ } else {
+ IWETH9(targetToken).transfer(
+ address(uint160(bytes20(recipient))),
+ outputAmount
+ );
}
}
@@ -149,192 +198,128 @@ contract SwapToAnyToken is zContract, OnlySystem {
swapAndWithdraw(inputToken, amount, targetToken, recipient, withdraw);
}
-}
-```
-
-Add the `WZETA` interface import, which allows interacting with ERC-20
-compatible tokens (such as ZRC-20) as well as unwrapping WZETA into native ZETA.
-
-Change the contract name from `Swap` to `SwapToAnyToken`.
-
-Add `bool withdraw` to the `Params` struct. This value determines if a user
-wants to withdraw the swapped token to the destination chain (`true`) or keep
-the token on ZetaChain (`false`). By default the value will be set to `true`.
-
-If the contract is being called from Bitcoin, use `bytesToBool` to decode the
-last value in the `message`, and set it as the value of `params.withdraw`.
-
-If the contract is being called from an EVM chain, use `abi.decode` to decode
-all values: target token, recipient and the withdraw flag.
-
-Next, add `params.withdraw` as the last argument to the `swapAndWithdraw`
-function call.
-### Swap and Withdraw Function
-
-Add `bool withdraw` as the last parameter to the function definition.
-
-If a user wants to withdraw the tokens, query the gas token and the gas fee.
-Since a user now has an option to not withdraw, this step has become optional.
-
-Modify the amount passed to `swapExactTokensForTokens`. If a user withdraws
-token, subtract the withdraw fee in input token amount.
-
-Finally, add a conditional to either withdraw ZRC-20 tokens to a connected chain
-or transfer the target token to the recipient on ZetaChain. If a user doesn't
-want to withdraw a token you need to consider two scenarios:
-
-- If the target token is WZETA, unwrap it and transfer native ZETA to the
- recipient.
-- If the target token is not WZETA, transfer it to the recipient as any other
- ERC-20-compatible token.
-
-### Swap Function
-
-Create a new public `swap` function to make it possible for users to call the
-"swap and withdraw" function. Compared to "swap and withdraw", which is internal
-and is not meant to be called directly, the "swap" function is public and is
-meant to be called from ZetaChain. The purpose of "swap" is to allow users to
-swap tokens they have on ZetaChain for other tokens and optionally also withdraw
-them. For example, when a user has a ZRC-20 ETH and they want to swap it for
-ZRC-20 BTC (without withdrawing), or swap it for ZRC-20 BNB and withdraw it to
-the BNB chain as a native BNB token.
-
-## Update the Interact Task
-
-```ts filename="tasks/interact.ts"
-let withdraw = true;
-if (args.withdraw != undefined) {
- withdraw = JSON.parse(args.withdraw);
+ function onRevert(RevertContext calldata revertContext) external override {}
}
-
-const data = prepareData(args.contract, ["address", "bytes", "bool"], [args.targetToken, recipient, withdraw]);
-
-//...
- .addOptionalParam("withdraw");
```
-Add an optional parameter `withdraw`, which determines if a user wants to
-withdraw the target token to the destination chain. By default set the value to
-`true`, and pass withdraw as the third value in the message.
-
-## Add a Swap Task
-
-While the interact task is meant to be called on a connected chain to trigger a
-universal contract, the swap task is meant to be called on ZetaChain directly to
-swap an asset already on ZetaChain for a different asset optionally withdrawing
-it.
+The contract introduces a key enhancement: a `withdraw` flag. This flag
+determines whether the swapped tokens should be withdrawn to a connected chain
+or remain on ZetaChain. Additionally, the contract supports both cross-chain
+calls and direct interactions on ZetaChain, making it useful for scenarios where
+tokens are already on ZetaChain and you don’t need to involve a connected chain.
+
+### Differences Between Swap and SwapToAnyToken Contracts
+
+In this new version, the core structure remains similar, but several key changes
+have been made to extend its functionality.
+
+First, the `Params` struct has been updated to include a `withdraw` flag. This
+allows users to specify whether they want the swapped tokens withdrawn to a
+connected chain or kept on ZetaChain. The `onCrossChainCall` function now
+decodes this additional flag from the incoming message. For EVM chains and
+Solana, the contract decodes the `withdraw` flag alongside other parameters. For
+Bitcoin, due to the smaller message size allowed by its OP_RETURN, the contract
+checks if the message length is sufficient before extracting the `withdraw`
+flag.
+
+The `swapAndWithdraw` function has also been modified to conditionally handle
+gas fees based on whether the tokens will be withdrawn. If the `withdraw` flag
+is set to `true`, the contract proceeds with the usual gas fee calculation and
+deduction. If the flag is `false`, it skips the gas fee handling and simply
+swaps the full amount of tokens.
+
+Once the tokens are swapped, the contract either withdraws them to the
+destination chain or transfers them directly on ZetaChain. When `withdraw` is
+`true`, it follows the same withdrawal process as the original contract, using
+the gateway to send tokens to the connected chain. However, if `withdraw` is
+`false`, it transfers the tokens directly to the recipient on ZetaChain without
+involving the gateway.
+
+Additionally, a new public `swap` function has been introduced, which allows
+users to interact with the contract directly on ZetaChain. This function is
+particularly useful if you already have tokens on ZetaChain and want to swap
+them without making a cross-chain call. It takes in parameters similar to those
+in `onCrossChainCall`, transfers the input tokens from the sender to the
+contract, and then calls `swapAndWithdraw` to perform the swap and handle
+withdrawal or direct transfer based on the `withdraw` flag.
+
+Finally, the contract now imports the `IWETH9` interface to handle direct token
+transfers when `withdraw` is `false`. This interface facilitates the transfer of
+wrapped tokens on ZetaChain.
+
+## Starting Localnet
+
+To simulate ZetaChain’s behavior locally, start the local development
+environment by running:
```
-import { task } from "hardhat/config";
-import { HardhatRuntimeEnvironment } from "hardhat/types";
-import { parseEther } from "@ethersproject/units";
-import { ethers } from "ethers";
-
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const [signer] = await hre.ethers.getSigners();
-
- if (!/zeta_(testnet|mainnet)/.test(hre.network.name)) {
- throw new Error('🚨 Please use either "zeta_testnet" or "zeta_mainnet".');
- }
-
- const factory = await hre.ethers.getContractFactory("SwapToAnyToken");
- const contract = factory.attach(args.contract);
-
- const amount = parseEther(args.amount);
- const inputToken = args.inputToken;
- const targetToken = args.targetToken;
- const recipient = ethers.utils.arrayify(args.recipient);
- const withdraw = JSON.parse(args.withdraw);
-
- const erc20Factory = await hre.ethers.getContractFactory("ERC20");
- const inputTokenContract = erc20Factory.attach(args.inputToken);
-
- const approval = await inputTokenContract.approve(args.contract, amount);
- await approval.wait();
-
- const tx = await contract.swap(
- inputToken,
- amount,
- targetToken,
- recipient,
- withdraw
- );
-
- await tx.wait();
- console.log(`Transaction hash: ${tx.hash}`);
-};
-
-task("swap", "Interact with the Swap contract from ZetaChain", main)
- .addFlag("json", "Output JSON")
- .addParam("contract", "Contract address")
- .addParam("amount", "Token amount to send")
- .addParam("inputToken", "Input token address")
- .addParam("targetToken", "Target token address")
- .addParam("recipient", "Recipient address")
- .addParam("withdraw", "Withdraw flag (true/false)");
+npx hardhat localnet
```
-## Compile and Deploy the Contract
+## Deploying the Contract
+
+Once your environment is set up, compile the contract and deploy it to localnet
+using the following command:
```
-npx hardhat compile --force
+yarn deploy --name SwapToAnyToken
```
-When deploying use the `--name` flag to specify which contract you want to
-deploy:
+After deployment, you should see an output similar to this:
```
-npx hardhat deploy --network zeta_testnet --name SwapToZeta
-```
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+🚀 Successfully deployed contract on localhost.
+📜 Contract address: 0x67d269191c92Caf3cD7723F116c85e6E9bf55933
```
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-🚀 Successfully deployed contract on zeta_testnet.
-📜 Contract address: 0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7
-🌍 ZetaScan: https://athens.explorer.zetachain.com/address/0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7
-🌍 Blockcsout: https://zetachain-athens-3.blockscout.com/address/0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7
-```
+Ensure that you provide the `systemContractAddress` and `gatewayAddress` when
+deploying the contract. For localnet, these addresses remain the same.
-## Swap Tokens Without Withdrawing
+## Swapping and Withdrawing Tokens to Connected Chain
-When using the `interact` task specify ZETA token contract address as the value
-of `--target-token`:
+To swap tokens from a connected EVM chain and withdraw them to the destination
+chain, use the following command:
```
-npx hardhat interact --contract 0x1767A93A96D339EeC8E0325D94B5d3E4454d542f --network bsc_testnet --amount 0.01 --target-token 0xcC683A782f4B30c138787CB5576a86AF66fdc31d --recipient 0x6093537Aa6C8C8bf4705bda40aC9193977208B39 --withdraw false
+npx hardhat evm-deposit-and-call --network localhost --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --amount 1 --types '["address", "bytes", "bool"]' 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 true
```
-Track the transaction:
+This command sends tokens to the `SwapToAnyToken` contract on ZetaChain and
+instructs it to withdraw the tokens to a connected chain. You’ll need to provide
+the receiver address (the contract), the amount of tokens to swap, the target
+token address on ZetaChain, the recipient address on the destination chain, and
+the `withdraw` flag set to `true`.
-```
-npx hardhat cctx 0x3f22de7a6d6669ce55ce2a95adaee46b8fd8a751b145c903c62300f9e7e44e4d
-```
+## Swapping Tokens Without Withdrawing
-```
-✓ CCTXs on ZetaChain found.
+If you want to swap tokens and keep them on ZetaChain rather than withdrawing
+them, set the `withdraw` flag to `false`. This can be done using the new `swap`
+function, which allows direct interaction with the contract on ZetaChain. Use
+the following command:
-✓ 0x7f7f7051dd9da2037b7fc01c43b18649b923c522e9ff3934e28647da59fffe79: 97 → 7001: OutboundMined (Remote omnichain contract call completed)
+```
+npx hardhat swap --network localhost --contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --inputToken 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921 --amount 1 --targetToken 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c --recipient 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --withdraw false
```
-Notice that an outbound CCTX is not created, because when swapping without
-withdrawing, target tokens remain on ZetaChain and are not withdrawn.
+This command will perform the token swap directly on ZetaChain without
+withdrawing the tokens. You need to provide the contract address, the input
+token address, the amount to swap, the target token address, and the recipient
+address on ZetaChain.
-## Swap Token And Withdraw
+## Conclusion
-```
-npx hardhat interact --contract 0x1767A93A96D339EeC8E0325D94B5d3E4454d542f --network bsc_testnet --amount 0.1 --target-token 0xcC683A782f4B30c138787CB5576a86AF66fdc31d --recipient 0x6093537Aa6C8C8bf4705bda40aC9193977208B39
-```
-
-```
-npx hardhat cctx 0x8323982f778ab14a5c37b10a80c5837da74ccdc8dba6ab4368f8b00612da8e1e
-```
+In this tutorial, you extended the functionality of the original swap contract
+by adding the ability to swap tokens to any token and decide whether to withdraw
+them to a connected chain or keep them on ZetaChain. You also learned how to
+deploy the contract and interact with it both via cross-chain calls and directly
+on ZetaChain, providing greater flexibility for a variety of use cases.
-```
-✓ CCTXs on ZetaChain found.
+## Source Code
+You can find the source code for this tutorial in the example contracts
+repository:
-✓ 0x86c66514209a23499d23fea4c2d7177e87bff5199d273a3592fb685d7d945da8: 97 → 7001: OutboundMined (Remote omnichain contract call completed)
-✓ 0x4a1d7df81e11c4273c0d105a23f049409d06f0bbc03e286014276adb247e208f: 7001 → 11155111: OutboundMined (ZRC20 withdrawal event setting to pending outbound directly : Outbound succeeded, mined)
-```
+https://github.com/zeta-chain/example-contracts/tree/main/examples/swap]
diff --git a/src/pages/developers/tutorials/swap-tss.mdx b/src/pages/developers/tutorials/swap-tss.mdx
new file mode 100644
index 00000000..e932358c
--- /dev/null
+++ b/src/pages/developers/tutorials/swap-tss.mdx
@@ -0,0 +1,381 @@
+---
+title: Swap on Testnet
+---
+
+import { Alert } from "~/components/shared";
+
+In this tutorial you will create a cross-chain swap contract. This contract will
+enable users to exchange a native gas token or a supported ERC-20 token from one
+connected blockchain for a token on another blockchain. For example, a user will
+be able to swap USDC from Ethereum to BTC on Bitcoin in a single transaction.
+
+
+ {" "}
+ This tutorial features architecture that is compatible with the current ZetaChain testnet, but will be phased out once
+ the [gateway](/developers/evm/gateway) is released. For a gateway compatible localnet-only example check out [Swap on Localnet](/developers/tutorials/swap)
+
+
+You will learn how to:
+
+- Decode incoming messages from both EVM chains and Bitcoin.
+- Work with the ZRC-20 representation of tokens transferred from connected
+ chains.
+- Use the swap helper function to swap tokens using Uniswap v2 pools.
+- Withdraw ZRC-20 tokens to a connected chain, accounting for cross-chain gas
+ fees.
+
+The swap contract will be implemented as a universal app and deployed on
+ZetaChain.
+
+Universal apps can accept token transfers and contract calls from connected
+chains. Tokens transferred from connected chains to a universal app contract are
+represented as [ZRC-20](/developers/tokens/zrc20). For example, ETH transferred
+from Ethereum is represented as ZRC-20 ETH. ZRC-20 tokens have the unique
+property of being able to be withdrawn back to their original chain as native
+assets.
+
+The swap contract will:
+
+- Accept a contract call from a connected chain containing native gas or
+ supported ERC-20 tokens and a message.
+- Decode the message, which should include:
+ - Target token address (represented as ZRC-20)
+ - Recipient address on the destination chain
+- Query withdraw gas fee of the target token.
+- Swap a fraction of the input token for a ZRC-20 gas tokens to cover the
+ withdrawal fee using the Uniswap v2 liquidity pools.
+- Swap the remaining input token amount for the target token ZRC-20.
+- Withdraw ZRC-20 tokens to the destination chain
+
+
+
+## Set Up Your Environment
+
+Clone the Hardhat contract template:
+
+```
+git clone https://github.com/zeta-chain/template
+
+cd template/contracts
+
+yarn
+```
+
+Make sure that you've followed the [Getting
+Started](/developers/tutorials/intro) tutorial to set up your development
+environment, create an account and request testnet tokens.
+
+## Create the contract
+
+Run the following command to create a new universal omnichain contract called
+`Swap` with two values in the message: target token address and recipient.
+
+```
+npx hardhat omnichain Swap targetToken:address recipient
+```
+
+## Universal App Contract
+
+```solidity filename="contracts/Swap.sol" {6-7,12,18-21,29-45,48-78}
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.7;
+
+import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
+import "@zetachain/toolkit/contracts/SwapHelperLib.sol";
+import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
+import "@zetachain/toolkit/contracts/OnlySystem.sol";
+
+contract Swap is zContract, OnlySystem {
+ SystemContract public systemContract;
+ uint256 constant BITCOIN = 18332;
+
+ constructor(address systemContractAddress) {
+ systemContract = SystemContract(systemContractAddress);
+ }
+
+ struct Params {
+ address target;
+ bytes to;
+ }
+
+ function onCrossChainCall(
+ zContext calldata context,
+ address zrc20,
+ uint256 amount,
+ bytes calldata message
+ ) external virtual override onlySystem(systemContract) {
+ Params memory params = Params({target: address(0), to: bytes("")});
+
+ if (context.chainID == BITCOIN) {
+ params.target = BytesHelperLib.bytesToAddress(message, 0);
+ params.to = abi.encodePacked(
+ BytesHelperLib.bytesToAddress(message, 20)
+ );
+ } else {
+ (address targetToken, bytes memory recipient) = abi.decode(
+ message,
+ (address, bytes)
+ );
+ params.target = targetToken;
+ params.to = recipient;
+ }
+
+ swapAndWithdraw(zrc20, amount, params.target, params.to);
+ }
+
+ function swapAndWithdraw(
+ address inputToken,
+ uint256 amount,
+ address targetToken,
+ bytes memory recipient
+ ) internal {
+ uint256 inputForGas;
+ address gasZRC20;
+ uint256 gasFee;
+
+ (gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();
+
+ inputForGas = SwapHelperLib.swapTokensForExactTokens(
+ systemContract,
+ inputToken,
+ gasFee,
+ gasZRC20,
+ amount
+ );
+
+ uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
+ systemContract,
+ inputToken,
+ amount - inputForGas,
+ targetToken,
+ 0
+ );
+
+ IZRC20(gasZRC20).approve(targetToken, gasFee);
+ IZRC20(targetToken).withdraw(recipient, outputAmount);
+ }
+}
+```
+
+### Decoding the Message
+
+Create a `Params` struct, which will hold two values:
+
+- `address target`: target token ZRC-20 address.
+- `bytes to`: recipient address on the destination chain. We're using `bytes`,
+ because the recipient can be either on EVM (like Ethereum or BNB) or on
+ Bitcoin.
+
+First, decode the incoming `message` to get the parameter values. The message
+might be encoded differently depending on the source chain. For example, on
+Bitcoin there is a upper limit of 80 bytes, so you might want to encode the
+message in the most efficient way possible. On EVM don't have this limit, so
+it's fine to use `abi.encode` to encode the message.
+
+Use `context.chainID` to determine the connected chain from which the contract
+is called.
+
+If it's Bitcoin, the first 20 bytes of the `message` are the `params.target`
+encoded as an `address`. Use `bytesToAddress` helper method to get the target
+token address. To get the recipient address, use the same helper method with an
+offset of 20 bytes and then use `abi.encodePacked` to convert the address to
+`bytes`.
+
+If it's an EVM chain, use `abi.decode` to decode the `message` into the
+`params.target` and `params.to`.
+
+### Swap and Withdraw Function
+
+#### Swapping for Gas Token
+
+Create a new function called `swapAndWithdraw`. Use the `withdrawGasFee` method
+of the target token ZRC-20 to get the gas fee token address and the gas fee
+amount. If the target token is the gas token of the destination chain (for
+example, BNB), `gasZRC20` will be the same `params.target`. However, if the
+target token is an ERC-20, like USDC on BNB, `gasZRC20` will tell you the
+address of the ZRC-20 of the destination chain.
+
+Use the `swapTokensForExactTokens` helper method to swap the incoming token for
+the gas coin using the internal liquidity pools. The method returns the amount
+of the incoming token that was used to pay for the gas.
+
+#### Swapping for Target Token
+
+Next, swap the incoming amount minus the amount spent swapping for a gas fee for
+the target token on the destination chain using the `swapExactTokensForTokens`
+helper method.
+
+#### Withdraw Target Token to Connected Chain
+
+At this point the contract has the required `gasFee` amount of `gasZRC20` token
+of the connected chain and an `outputAmount` amount of `params.target` token.
+
+To withdraw tokens to a connected chain you will be calling the `withdraw`
+method of ZRC-20. The `withdraw` method expects the caller (in our case the
+contract) to have the required amount of gas tokens ZRC-20. Approve the target
+token ZRC-20 contract to spend the `gasFee` amount. Finally, call the `withdraw`
+method of the target token ZRC-20 to send the tokens to the recipient on the
+connected chain.
+
+
+ {" "}
+ Note that you don't have to tell which chain to withdraw to because each ZRC-20 contract knows which connected chain it
+ is associated with. For example, ZRC-20 Ethereum USDC can only be withdrawn to Ethereum.
+
+
+## Update the Interact Task
+
+In the `interact` task generated for us by the contract template the recipient
+is encoded as string. Our contract, however, expects the recipient to be encoded
+as `bytes` to ensure that both EVM and Bitcoin addresses are supported.
+
+To support both EVM and Bitcoin addresses, we need to check if the recipient is
+a valid Bitcoin address. If it is, we need to encode it as `bytes` using
+`utils.solidityPack`.
+
+If it’s not a valid bech32 address, then we assume it's an EVM address and use
+`args.recipient` as the value for the recipient.
+
+Finally, update the `prepareData` function call to use the `bytes` type for the
+recipient.
+
+```ts filename="tasks/interact.ts" {1,6-22}
+import bech32 from "bech32";
+
+const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
+ const [signer] = await hre.ethers.getSigners();
+
+ let recipient;
+ try {
+ if (bech32.decode(args.recipient)) {
+ recipient = utils.solidityPack(["bytes"], [utils.toUtf8Bytes(args.recipient)]);
+ }
+ } catch (e) {
+ recipient = args.recipient;
+ }
+
+ const data = prepareData(args.contract, ["address", "bytes"], [args.targetToken, recipient]);
+ //...
+};
+```
+
+## Create an Account and Request Tokens from the Faucet
+
+Before proceeding with the next steps, make sure you have [created an account
+and requested ZETA tokens](/developers/tutorials/hello#create-an-account) from
+the faucet.
+
+## Compile and Deploy the Contract
+
+```
+npx hardhat compile --force
+```
+
+```
+npx hardhat deploy --network zeta_testnet
+```
+
+```
+🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+
+🚀 Successfully deployed contract on ZetaChain.
+📜 Contract address: 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
+🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
+```
+
+## Swap Native Gas Tokens Between EVM Chains
+
+Use the `interact` task to perform a cross-chain swap. In this example, we're
+swapping native sETH from Sepolia for BNB on BNB chain. The contract will
+deposit sETH to ZetaChain as ZRC-20, swap it for ZRC-20 BNB and then withdraw
+native BNB to the BNB chain. To get the value of the `--target-token` find the
+ZRC-20 contract address of the destination token in the [ZRC-20 section of the
+docs](/developers/tokens/zrc20).
+
+```
+npx hardhat interact --contract 0x175DeE06ca605674e49F1FADfC6B399D6ab31726 --amount 0.3 --network sepolia_testnet --target-token 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891 --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+```
+
+```
+🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+
+🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet
+network. 📝 Transaction hash:
+0xc4b2bbd3b3090e14797463af1965a00318cc39a50fce53a5d5856d09fe67410d
+```
+
+Track your cross-chain transaction:
+
+```
+npx hardhat cctx
+0xc4b2bbd3b3090e14797463af1965a00318cc39a50fce53a5d5856d09fe67410d
+```
+
+```
+✓ CCTXs on ZetaChain found.
+
+✓ 0xf6419c8d850314a436a3cfc7bc5cd487e29bad9c8fae0d8be9a913d622599980: 11155111 → 7001: OutboundMined (Remote omnich
+ain contract call completed)
+⠧ 0x5e533d781ddc9760784ba9c1887f77a80d3ca0d771ea41f02bc4d0a1c9412dc2: 7001 → 97: PendingOutbound (ZRC20 withdrawal
+event setting to pending outbound directly)
+```
+
+## Swap ERC-20 Tokens Between EVM Chains
+
+Now let's swap USDC from Sepolia to BNB on BNB chain. To send USDC specify the
+ERC-20 token contract address (on Sepolia) in the `--token` parameter. You can
+find the address of the token in the [ZRC-20 section of the
+docs](/developers/tokens/zrc20).
+
+```
+npx hardhat interact --contract 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E --amount 5 --token 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 --network sepolia_testnet --target-token 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891 --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+```
+
+```
+🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+
+🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet network.
+📝 Transaction hash: 0xce8832232639d29d40078e14d0a5b20c055123d6df1e1d39f90cfd130c33466d
+```
+
+```
+npx hardhat cctx 0xce8832232639d29d40078e14d0a5b20c055123d6df1e1d39f90cfd130c33466d
+```
+
+```
+✓ CCTXs on ZetaChain found.
+
+✓ 0x1ae1436358ef755c1c782d0a249ae99e857b0aecb91dcd8da4a4e7171f5d9459: 11155111 → 7001: OutboundMined (Remote omnichain contract call completed)
+✓ 0xbefe99d3e17d16fc88762f85b1becd1396b01956c04b5ec037abc2c63d821caa: 7001 → 97: OutboundMined (ZRC20 withdrawal event setting to pending outbound directly : Outbound succeeded, mined)
+```
+
+## Swap from Bitcoin
+
+Use the `send-btc` task to send Bitcoin to the TSS address with a memo. The memo
+should contain the following:
+
+- Omnichain contract address on ZetaChain:
+ `175DeE06ca605674e49F1FADfC6B399D6ab31726`
+- Target token address: `05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0`
+- Recipient address: `4955a3F38ff86ae92A914445099caa8eA2B9bA32`
+
+```
+npx hardhat send-btc --amount 0.001 --memo 175DeE06ca605674e49F1FADfC6B399D6ab3172605BA149A7bd6dC1F937fA9046A9e05C05f3b18b04955a3F38ff86ae92A914445099caa8eA2B9bA32 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
+```
+
+```
+npx hardhat cctx 29d6a0af11aa6164e83c17d9f129e4ec504d327fb94429732d95c16ddfcce999
+```
+
+## Source Code
+
+You can find the source code for the example in this tutorial here:
+
+https://github.com/zeta-chain/example-contracts/tree/main/omnichain/swap
diff --git a/src/pages/developers/tutorials/swap.mdx b/src/pages/developers/tutorials/swap.mdx
index 25baf126..5b47afb7 100644
--- a/src/pages/developers/tutorials/swap.mdx
+++ b/src/pages/developers/tutorials/swap.mdx
@@ -4,19 +4,18 @@ title: Swap
import { Alert } from "~/components/shared";
-In this tutorial you will create a cross-chain swap contract. This contract will
-enable users to exchange a native gas token or a supported ERC-20 token from one
-connected blockchain for a token on another blockchain. For example, a user will
-be able to swap USDC from Ethereum to BTC on Bitcoin in a single transaction.
+In this tutorial, you will create a cross-chain swap contract. This contract
+will enable users to exchange a native gas token or a supported ERC-20 token
+from one connected blockchain for a token on another blockchain. For example, a
+user will be able to swap USDC from Ethereum to BTC on Bitcoin in a single
+transaction.
You will learn how to:
-- Decode incoming messages from both EVM chains and Bitcoin.
-- Work with the ZRC-20 representation of tokens transferred from connected
- chains.
-- Use the swap helper function to swap tokens using Uniswap v2 pools.
-- Withdraw ZRC-20 tokens to a connected chain, accounting for cross-chain gas
- fees.
+- Define a universal app contract that performs token swaps across chains.
+- Deploy the contract to localnet.
+- Interact with the contract by swapping tokens from a connected EVM blockchain
+ in localnet.
The swap contract will be implemented as a universal app and deployed on
ZetaChain.
@@ -36,62 +35,61 @@ The swap contract will:
- Target token address (represented as ZRC-20)
- Recipient address on the destination chain
- Query withdraw gas fee of the target token.
-- Swap a fraction of the input token for a ZRC-20 gas tokens to cover the
+- Swap a fraction of the input token for a ZRC-20 gas token to cover the
withdrawal fee using the Uniswap v2 liquidity pools.
- Swap the remaining input token amount for the target token ZRC-20.
-- Withdraw ZRC-20 tokens to the destination chain
+- Withdraw ZRC-20 tokens to the destination chain.
-
+
+ {" "}
+ This tutorial depends on the gateway, which is available on localnet but not yet deployed on testnet. It will be compatible
+ with testnet after the gateway is deployed. In other words, you can't deploy this tutorial on testnet yet. For a tutorial
+ that works with the current testnet, check out the [Swap on Testnet tutorial](/developers/tutorials/swap-tss).{" "}
+
-## Set Up Your Environment
+## Setting Up Your Environment
-Clone the Hardhat contract template:
+To set up your environment, clone the example contracts repository and install
+the dependencies by running the following commands:
```
-git clone https://github.com/zeta-chain/template
+git clone https://github.com/zeta-chain/example-contracts
-cd template/contracts
+cd example-contracts/examples/swap
yarn
```
-Make sure that you've followed the [Getting
-Started](/developers/tutorials/intro) tutorial to set up your development
-environment, create an account and request testnet tokens.
-
-## Create the contract
+## Understanding the Swap Contract
-Run the following command to create a new universal omnichain contract called
-`Swap` with two values in the message: target token address and recipient.
-
-```
-npx hardhat omnichain Swap targetToken:address recipient
-```
+The `Swap` contract is a universal application that facilitates cross-chain
+token swaps on ZetaChain. It inherits from the `UniversalContract` interface and
+handles incoming cross-chain calls, processes token swaps using ZetaChain's
+liquidity pools, and sends the swapped tokens to the recipient on the target
+chain.
-## Universal App Contract
-
-```solidity filename="contracts/Swap.sol" {6-7,12,18-21,29-45,48-78}
+```solidity
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.7;
+pragma solidity 0.8.26;
+
+import {SystemContract, IZRC20} from "@zetachain/toolkit/contracts/SystemContract.sol";
+import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol";
+import {BytesHelperLib} from "@zetachain/toolkit/contracts/BytesHelperLib.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
-import "@zetachain/toolkit/contracts/SwapHelperLib.sol";
-import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
-import "@zetachain/toolkit/contracts/OnlySystem.sol";
+import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
+import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
-contract Swap is zContract, OnlySystem {
+contract Swap is UniversalContract {
SystemContract public systemContract;
+ GatewayZEVM public gateway;
uint256 constant BITCOIN = 18332;
- constructor(address systemContractAddress) {
+ constructor(address systemContractAddress, address payable gatewayAddress) {
systemContract = SystemContract(systemContractAddress);
+ gateway = GatewayZEVM(gatewayAddress);
}
struct Params {
@@ -104,9 +102,8 @@ contract Swap is zContract, OnlySystem {
address zrc20,
uint256 amount,
bytes calldata message
- ) external virtual override onlySystem(systemContract) {
+ ) external override {
Params memory params = Params({target: address(0), to: bytes("")});
-
if (context.chainID == BITCOIN) {
params.target = BytesHelperLib.bytesToAddress(message, 0);
params.to = abi.encodePacked(
@@ -133,243 +130,277 @@ contract Swap is zContract, OnlySystem {
uint256 inputForGas;
address gasZRC20;
uint256 gasFee;
+ uint256 swapAmount;
(gasZRC20, gasFee) = IZRC20(targetToken).withdrawGasFee();
- inputForGas = SwapHelperLib.swapTokensForExactTokens(
- systemContract,
- inputToken,
- gasFee,
- gasZRC20,
- amount
- );
+ if (gasZRC20 == inputToken) {
+ swapAmount = amount - gasFee;
+ } else {
+ inputForGas = SwapHelperLib.swapTokensForExactTokens(
+ systemContract,
+ inputToken,
+ gasFee,
+ gasZRC20,
+ amount
+ );
+ swapAmount = amount - inputForGas;
+ }
uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
systemContract,
inputToken,
- amount - inputForGas,
+ swapAmount,
targetToken,
0
);
- IZRC20(gasZRC20).approve(targetToken, gasFee);
- IZRC20(targetToken).withdraw(recipient, outputAmount);
+ if (gasZRC20 == targetToken) {
+ IZRC20(gasZRC20).approve(address(gateway), outputAmount + gasFee);
+ } else {
+ IZRC20(gasZRC20).approve(address(gateway), gasFee);
+ IZRC20(targetToken).approve(address(gateway), outputAmount);
+ }
+
+ gateway.withdraw(
+ recipient,
+ outputAmount,
+ targetToken,
+ RevertOptions({
+ revertAddress: address(0),
+ callOnRevert: false,
+ abortAddress: address(0),
+ revertMessage: "",
+ onRevertGasLimit: 0
+ })
+ );
}
+
+ function onRevert(RevertContext calldata revertContext) external override {}
}
```
### Decoding the Message
-Create a `Params` struct, which will hold two values:
+The contract defines a `Params` struct to store two crucial pieces of
+information:
+
+- **`address target`**: The ZRC-20 address of the target token on ZetaChain.
+- **`bytes to`**: The recipient's address on the destination chain, stored as
+ `bytes` because the recipient could be on an EVM chain (like Ethereum or BNB)
+ or on a non-EVM chain like Bitcoin.
-- `address target`: target token ZRC-20 address.
-- `bytes to`: recipient address on the destination chain. We're using `bytes`,
- because the recipient can be either on EVM (like Ethereum or BNB) or on
- Bitcoin.
+When the `onCrossChainCall` function is invoked, it receives a `message`
+parameter that needs to be decoded to extract the swap details. The encoding of
+this message varies depending on the source chain due to different limitations
+and requirements.
-First, decode the incoming `message` to get the parameter values. The message
-might be encoded differently depending on the source chain. For example, on
-Bitcoin there is a upper limit of 80 bytes, so you might want to encode the
-message in the most efficient way possible. On EVM don't have this limit, so
-it's fine to use `abi.encode` to encode the message.
+- **For Bitcoin**: Since Bitcoin has an upper limit of 80 bytes for OP_RETURN
+ messages, the contract uses a more efficient encoding. It extracts the
+ `params.target` by reading the first 20 bytes of the `message` and converting
+ it to an `address` using the `bytesToAddress` helper method. The recipient's
+ address is then obtained by reading the next 20 bytes and packing it into
+ `bytes` using `abi.encodePacked`.
-Use `context.chainID` to determine the connected chain from which the contract
-is called.
+- **For EVM Chains And Solana**: EVM chains don't have strict message size
+ limits, so the contract uses `abi.decode` to extract the `params.target` and
+ `params.to` directly from the `message`.
-If it's Bitcoin, the first 20 bytes of the `message` are the `params.target`
-encoded as an `address`. Use `bytesToAddress` helper method to get the target
-token address. To get the recipient address, use the same helper method with an
-offset of 20 bytes and then use `abi.encodePacked` to convert the address to
-`bytes`.
+The `context.chainID` is utilized to determine the source chain and apply the
+appropriate decoding logic.
-If it's an EVM chain, use `abi.decode` to decode the `message` into the
-`params.target` and `params.to`.
+After decoding the message, the contract proceeds to handle the token swap and
+withdrawal process by calling the `swapAndWithdraw` function with the
+appropriate parameters.
-### Swap and Withdraw Function
+### Swapping and Withdrawing Tokens
+
+The `swapAndWithdraw` function encapsulates the logic for swapping tokens and
+withdrawing them to the connected chain. By separating this logic into its own
+function, the code becomes cleaner and easier to maintain.
#### Swapping for Gas Token
-Create a new function called `swapAndWithdraw`. Use the `withdrawGasFee` method
-of the target token ZRC-20 to get the gas fee token address and the gas fee
-amount. If the target token is the gas token of the destination chain (for
-example, BNB), `gasZRC20` will be the same `params.target`. However, if the
-target token is an ERC-20, like USDC on BNB, `gasZRC20` will tell you the
-address of the ZRC-20 of the destination chain.
+The contract first addresses the gas fee required for the withdrawal on the
+destination chain. It uses the `withdrawGasFee` method of the target token's
+ZRC-20 contract to obtain the gas fee amount (`gasFee`) and the gas fee token
+address (`gasZRC20`).
-Use the `swapTokensForExactTokens` helper method to swap the incoming token for
-the gas coin using the internal liquidity pools. The method returns the amount
-of the incoming token that was used to pay for the gas.
+If the incoming token (`inputToken`) is the same as the gas fee token
+(`gasZRC20`), it deducts the gas fee directly from the incoming amount.
+Otherwise, it swaps a portion of the incoming tokens for the required gas fee
+using the `swapTokensForExactTokens` helper method. This ensures that the
+contract has enough gas tokens to cover the withdrawal fee on the destination
+chain.
#### Swapping for Target Token
-Next, swap the incoming amount minus the amount spent swapping for a gas fee for
-the target token on the destination chain using the `swapExactTokensForTokens`
-helper method.
+Next, the contract swaps the remaining tokens (`swapAmount`) for the target
+token specified in `targetToken`. It uses the `swapExactTokensForTokens` helper
+method to perform this swap through ZetaChain's internal liquidity pools. This
+method returns the amount of the target token received (`outputAmount`).
-#### Withdraw Target Token to Connected Chain
+#### Withdrawing Target Token to Connected Chain
-At this point the contract has the required `gasFee` amount of `gasZRC20` token
-of the connected chain and an `outputAmount` amount of `params.target` token.
+At this stage, the contract holds the required gas fee in `gasZRC20` tokens and
+the swapped target tokens in `targetToken`. It needs to approve the
+`GatewayZEVM` contract to spend these tokens before initiating the withdrawal.
+If the gas fee token is the same as the target token, it approves the total
+amount (gas fee plus output amount) for the gateway to spend. If they are
+different, it approves each token separately—the gas fee token (`gasZRC20`) and
+the target token (`targetToken`).
-To withdraw tokens to a connected chain you will be calling the `withdraw`
-method of ZRC-20. The `withdraw` method expects the caller (in our case the
-contract) to have the required amount of gas tokens ZRC-20. Approve the target
-token ZRC-20 contract to spend the `gasFee` amount. Finally, call the `withdraw`
-method of the target token ZRC-20 to send the tokens to the recipient on the
-connected chain.
+Finally, the contract calls the `gateway.withdraw` method to send the tokens to
+the recipient on the connected chain. The `withdraw` method handles the
+cross-chain transfer, ensuring that the recipient receives the swapped tokens on
+their native chain, whether it's an EVM chain or Bitcoin.
{" "}
- Note that you don't have to tell which chain to withdraw to because each ZRC-20 contract knows which connected chain it
- is associated with. For example, ZRC-20 Ethereum USDC can only be withdrawn to Ethereum.
+ Note that you don't have to specify which chain to withdraw to because each ZRC-20 contract knows which connected chain
+ it is associated with. For example, ZRC-20 Ethereum USDC can only be withdrawn to Ethereum.
-## Update the Interact Task
-
-In the `interact` task generated for us by the contract template the recipient
-is encoded as string. Our contract, however, expects the recipient to be encoded
-as `bytes` to ensure that both EVM and Bitcoin addresses are supported.
-
-To support both EVM and Bitcoin addresses, we need to check if the recipient is
-a valid Bitcoin address. If it is, we need to encode it as `bytes` using
-`utils.solidityPack`.
+## Deploying the Contract
-If it’s not a valid bech32 address, then we assume it's an EVM address and use
-`args.recipient` as the value for the recipient.
+Compile the contract and deploy it to localnet by running:
-Finally, update the `prepareData` function call to use the `bytes` type for the
-recipient.
-
-```ts filename="tasks/interact.ts" {1,6-22}
-import bech32 from "bech32";
+```
+yarn deploy
+```
-const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
- const [signer] = await hre.ethers.getSigners();
+You should see output similar to:
- let recipient;
- try {
- if (bech32.decode(args.recipient)) {
- recipient = utils.solidityPack(["bytes"], [utils.toUtf8Bytes(args.recipient)]);
- }
- } catch (e) {
- recipient = args.recipient;
- }
-
- const data = prepareData(args.contract, ["address", "bytes"], [args.targetToken, recipient]);
- //...
-};
```
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
-## Create an Account and Request Tokens from the Faucet
+🚀 Successfully deployed contract on localhost.
+📜 Contract address: 0x67d269191c92Caf3cD7723F116c85e6E9bf55933
+```
-Before proceeding with the next steps, make sure you have [created an account
-and requested ZETA tokens](/developers/tutorials/hello#create-an-account) from
-the faucet.
+## Starting Localnet
-## Compile and Deploy the Contract
+Start the local development environment to simulate ZetaChain's behavior by
+running:
```
-npx hardhat compile --force
+npx hardhat localnet
```
-```
-npx hardhat deploy --network zeta_testnet
-```
+## Swapping Gas Tokens for ERC-20 Tokens
-```
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+To swap gas tokens (such as ETH) for ERC-20 tokens, run the following command:
-🚀 Successfully deployed contract on ZetaChain.
-📜 Contract address: 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
-🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
+```
+npx hardhat evm-deposit-and-call --network localhost --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --amount 1 --types '["address", "bytes"]' 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
```
-## Swap Native Gas Tokens Between EVM Chains
+This script deposits tokens into the ZetaChain gateway and sends a message to
+the Swap contract on ZetaChain to execute the swap logic.
-Use the `interact` task to perform a cross-chain swap. In this example, we're
-swapping native sETH from Sepolia for BNB on BNB chain. The contract will
-deposit sETH to ZetaChain as ZRC-20, swap it for ZRC-20 BNB and then withdraw
-native BNB to the BNB chain. To get the value of the `--target-token` find the
-ZRC-20 contract address of the destination token in the [ZRC-20 section of the
-docs](/developers/tokens/zrc20).
+In this command, the `--receiver` parameter is the address of the Swap contract
+on ZetaChain (`0x67d269191c92Caf3cD7723F116c85e6E9bf55933`) that will handle the
+swap. The `--amount 1` option indicates that you want to swap 1 ETH. The
+`--types '["address", "bytes"]'` parameter defines the ABI types of the message
+parameters being sent to the `onCrossChainCall` function in the Swap contract.
+The two addresses that follow are the target ERC-20 token address on the
+destination chain (`0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c`) and the
+recipient address (`0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`) who will
+receive the swapped tokens.
+
+When you execute this command, the script calls the `gateway.depositAndCall`
+method on the connected EVM chain, depositing 1 ETH and sending a message to the
+Swap contract on ZetaChain. The EVM gateway processes the deposit and emits a
+`Deposited` event:
```
-npx hardhat interact --contract 0x175DeE06ca605674e49F1FADfC6B399D6ab31726 --amount 0.3 --network sepolia_testnet --target-token 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891 --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+[EVM]: Gateway: 'Deposited' event emitted
```
-```
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+ZetaChain then picks up the event and executes the `onCrossChainCall` function
+of the Swap contract with the provided message. The execution log might look
+like this:
-🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet
-network. 📝 Transaction hash:
-0xc4b2bbd3b3090e14797463af1965a00318cc39a50fce53a5d5856d09fe67410d
+```
+[ZetaChain]: Universal contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 executing onCrossChainCall (context: {"origin":"0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0","sender":"0x735b14BB79463307AAcBED86DAf3322B1e6226aB","chainID":1}), zrc20: 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921, amount: 1000000000000000000, message: 0x0000000000000000000000009fd96203f7b22bcf72d9dcb40ff98302376ce09c00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000014f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000)
```
-Track your cross-chain transaction:
+In this context, `origin` refers to the original sender's address, `sender` is
+the address that initiated the cross-chain call, and `chainID` identifies the
+source chain. The `zrc20` field shows the ZRC-20 representation of the deposited
+token on ZetaChain, and `amount` is the number of tokens received. The `message`
+contains the encoded parameters sent to `onCrossChainCall`.
+
+The Swap contract decodes the message, identifies the target ERC-20 token and
+recipient, and initiates the swap logic. After processing, the ZetaChain gateway
+emits a `Withdrawn` event:
```
-npx hardhat cctx
-0xc4b2bbd3b3090e14797463af1965a00318cc39a50fce53a5d5856d09fe67410d
+[ZetaChain]: Gateway: 'Withdrawn' event emitted
```
-```
-✓ CCTXs on ZetaChain found.
+Finally, the EVM chain receives the withdrawal request, and the swapped ERC-20
+tokens are transferred to the recipient's address:
-✓ 0xf6419c8d850314a436a3cfc7bc5cd487e29bad9c8fae0d8be9a913d622599980: 11155111 → 7001: OutboundMined (Remote omnich
-ain contract call completed)
-⠧ 0x5e533d781ddc9760784ba9c1887f77a80d3ca0d771ea41f02bc4d0a1c9412dc2: 7001 → 97: PendingOutbound (ZRC20 withdrawal
-event setting to pending outbound directly)
+```
+[EVM]: Transferred 1.013466046281196713 ERC-20 tokens from Custody to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
```
-## Swap ERC-20 Tokens Between EVM Chains
+### Swapping ERC-20 Tokens for Gas Tokens
-Now let's swap USDC from Sepolia to BNB on BNB chain. To send USDC specify the
-ERC-20 token contract address (on Sepolia) in the `--token` parameter. You can
-find the address of the token in the [ZRC-20 section of the
-docs](/developers/tokens/zrc20).
+To swap ERC-20 tokens for gas tokens, adjust the command by specifying the
+ERC-20 token you're swapping from using the `--erc20` parameter:
```
-npx hardhat interact --contract 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E --amount 5 --token 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 --network sepolia_testnet --target-token 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891 --recipient 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+npx hardhat evm-deposit-and-call --network localhost --receiver 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 --amount 1 --erc20 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82 --types '["address", "bytes"]' 0x91d18e54DAf4F677cB28167158d6dd21F6aB3921 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
```
-```
-🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+Here, the `--erc20` option specifies the ERC-20 token address you're swapping
+from on the source chain (`0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82`). The
+other parameters remain the same as in the previous command.
-🚀 Successfully broadcasted a token transfer transaction on sepolia_testnet network.
-📝 Transaction hash: 0xce8832232639d29d40078e14d0a5b20c055123d6df1e1d39f90cfd130c33466d
-```
+When you run the command, the script calls the `gateway.depositAndCall` method
+with the specified ERC-20 token and amount, sending a message to the Swap
+contract on ZetaChain. The EVM gateway processes the deposit of the ERC-20
+tokens and emits a `Deposited` event:
```
-npx hardhat cctx 0xce8832232639d29d40078e14d0a5b20c055123d6df1e1d39f90cfd130c33466d
+[EVM]: Gateway: 'Deposited' event emitted
```
-```
-✓ CCTXs on ZetaChain found.
+ZetaChain picks up the event and executes the `onCrossChainCall` function of the
+Swap contract:
-✓ 0x1ae1436358ef755c1c782d0a249ae99e857b0aecb91dcd8da4a4e7171f5d9459: 11155111 → 7001: OutboundMined (Remote omnichain contract call completed)
-✓ 0xbefe99d3e17d16fc88762f85b1becd1396b01956c04b5ec037abc2c63d821caa: 7001 → 97: OutboundMined (ZRC20 withdrawal event setting to pending outbound directly : Outbound succeeded, mined)
+```
+[ZetaChain]: Universal contract 0x67d269191c92Caf3cD7723F116c85e6E9bf55933 executing onCrossChainCall (context: {"origin":"0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0","sender":"0x735b14BB79463307AAcBED86DAf3322B1e6226aB","chainID":1}), zrc20: 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c, amount: 1000000000000000000, message: 0x00000000000000000000000091d18e54daf4f677cb28167158d6dd21f6ab392100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000014f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000)
```
-## Swap from Bitcoin
-
-Use the `send-btc` task to send Bitcoin to the TSS address with a memo. The memo
-should contain the following:
-
-- Omnichain contract address on ZetaChain:
- `175DeE06ca605674e49F1FADfC6B399D6ab31726`
-- Target token address: `05BA149A7bd6dC1F937fA9046A9e05C05f3b18b0`
-- Recipient address: `4955a3F38ff86ae92A914445099caa8eA2B9bA32`
+The Swap contract decodes the message, identifies the target gas token and
+recipient, and initiates the swap logic. After processing, the ZetaChain gateway
+emits a `Withdrawn` event:
```
-npx hardhat send-btc --amount 0.001 --memo 175DeE06ca605674e49F1FADfC6B399D6ab3172605BA149A7bd6dC1F937fA9046A9e05C05f3b18b04955a3F38ff86ae92A914445099caa8eA2B9bA32 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
+[ZetaChain]: Gateway: 'Withdrawn' event emitted
```
+The EVM chain then receives the withdrawal request, and the swapped gas tokens
+are transferred to the recipient's address:
+
```
-npx hardhat cctx 29d6a0af11aa6164e83c17d9f129e4ec504d327fb94429732d95c16ddfcce999
+[EVM]: Transferred 0.974604535974342599 native gas tokens from TSS to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
```
+## Conclusion
+
+In this tutorial, you learned how to define a universal app contract that
+performs cross-chain token swaps. You deployed the `Swap` contract to a local
+development network and interacted with the contract by swapping tokens from a
+connected EVM chain. You also understood the mechanics of handling gas fees and
+token approvals in cross-chain swaps.
+
## Source Code
-You can find the source code for the example in this tutorial here:
+You can find the source code for the tutorial in the example contracts
+repository:
-https://github.com/zeta-chain/example-contracts/tree/main/omnichain/swap
+[https://github.com/zeta-chain/example-contracts/tree/main/examples/swap](https://github.com/zeta-chain/example-contracts/tree/main/examples/swap)