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)