From c58d7d1ee993e1f6abeb6639b32ea2e5060422b9 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 10 Oct 2023 17:30:59 +0300 Subject: [PATCH] Update tutorials to the latest version of toolkit (#173) --- .../examples/cross-chain-counter.md | 49 ++- .../examples/cross-chain-nft.md | 78 ++--- .../examples/multi-chain-value.md | 172 ++++++---- .../examples/your-first-message.md | 9 +- .../omnichain/tutorials/bitcoin-frontend.md | 4 +- .../developers/omnichain/tutorials/bitcoin.md | 191 ----------- docs/developers/omnichain/tutorials/curve.md | 2 +- docs/developers/omnichain/tutorials/hello.md | 78 ++--- .../tutorials/single-input-multiple-output.md | 65 ++-- docs/developers/omnichain/tutorials/swap.md | 300 ++++++++++-------- .../omnichain/tutorials/withdraw.md | 218 ------------- docs/developers/omnichain/zrc-20.md | 13 +- docs/reference/wallets.md | 2 +- 13 files changed, 426 insertions(+), 755 deletions(-) delete mode 100644 docs/developers/omnichain/tutorials/bitcoin.md delete mode 100644 docs/developers/omnichain/tutorials/withdraw.md diff --git a/docs/developers/cross-chain-messaging/examples/cross-chain-counter.md b/docs/developers/cross-chain-messaging/examples/cross-chain-counter.md index e2ee1e6d..83293347 100644 --- a/docs/developers/cross-chain-messaging/examples/cross-chain-counter.md +++ b/docs/developers/cross-chain-messaging/examples/cross-chain-counter.md @@ -17,12 +17,8 @@ This is an example app of cross-chain counter using ``` git clone https://github.com/zeta-chain/template -``` - -Install the dependencies: - -``` -yarn add --dev @openzeppelin/contracts +cd template +yarn ``` ## Create a new contract @@ -42,20 +38,18 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; -interface CounterErrors { +contract Counter is ZetaInteractor, ZetaReceiver { error InvalidMessageType(); // highlight-next-line error DecrementOverflow(); -} - -contract Counter is ZetaInteractor, ZetaReceiver, CounterErrors { - bytes32 public constant COUNTER_MESSAGE_TYPE = - keccak256("CROSS_CHAIN_COUNTER"); event CounterEvent(address); event CounterRevertedEvent(address); + // highlight-next-line mapping(address => uint256) public counter; + bytes32 public constant COUNTER_MESSAGE_TYPE = + keccak256("CROSS_CHAIN_COUNTER"); ZetaTokenConsumer private immutable _zetaConsumer; IERC20 internal immutable _zetaToken; @@ -79,7 +73,6 @@ contract Counter is ZetaInteractor, ZetaReceiver, CounterErrors { }(address(this), crossChainGas); _zetaToken.approve(address(connector), zetaValueAndGas); - counter[msg.sender]++; connector.send( ZetaInterfaces.SendInput({ destinationChainId: destinationChainId, @@ -129,7 +122,7 @@ contract Counter is ZetaInteractor, ZetaReceiver, CounterErrors { ## Create a task to get the counter value -```solidity title="tasks/counter_show.ts" +```ts title="tasks/counter_show.ts" import { task } from "hardhat/config"; import { HardhatRuntimeEnvironment } from "hardhat/types"; @@ -162,22 +155,44 @@ import "./tasks/counter_show.ts"; ## Create a task to increment the counter value ```ts title="tasks/interact.ts" +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { parseEther } from "@ethersproject/units"; + +const main = async (args: any, hre: HardhatRuntimeEnvironment) => { + const [signer] = await hre.ethers.getSigners(); + + const factory = await hre.ethers.getContractFactory("Counter"); + const contract = factory.attach(args.contract); + + const destination = hre.config.networks[args.destination]?.chainId; + if (destination === undefined) { + throw new Error(`${args.destination} is not a valid destination chain`); + } + // remove-next-line const paramFrom = hre.ethers.utils.getAddress(args.from); + const value = parseEther(args.amount); + const tx = await contract .connect(signer) // highlight-next-line - .sendMessage(destination, { value: parseEther(args.amount) }); + .sendMessage(destination, { value }); const receipt = await tx.wait(); - console.log(`✅ The transaction has been broadcasted to ${hre.network.name} + if (args.json) { + console.log(JSON.stringify(tx, null, 2)); + } else { + console.log(`🔑 Using account: ${signer.address}\n`); + console.log(`✅ The transaction has been broadcasted to ${hre.network.name} 📝 Transaction hash: ${receipt.transactionHash} `); - await trackCCTX(tx.hash); + } }; task("interact", "Sends a message from one chain to another.", main) + .addFlag("json", "Output JSON") .addParam("contract", "Contract address") .addParam("amount", "Token amount to send") .addParam("destination", "Destination chain") diff --git a/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md b/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md index 55b76f72..ee2f37a0 100644 --- a/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md +++ b/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md @@ -14,17 +14,12 @@ capabilities using ![Cross-chain NFT transfer](/img/graphs/cross-chain-nft-transfer.svg) -## Set Up Your Environment +## Set up your environment ``` git clone https://github.com/zeta-chain/template -``` - -Install dependencies: - -``` cd template -yarn add --dev @openzeppelin/contracts +yarn ``` ## Create a new contract @@ -47,41 +42,38 @@ pragma solidity 0.8.7; import "@openzeppelin/contracts/interfaces/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; +import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; // highlight-start import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; // highlight-end -import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; -import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; - -interface CrossChainWarriorsErrors { - error InvalidMessageType(); -} contract CrossChainWarriors is ZetaInteractor, ZetaReceiver, - CrossChainWarriorsErrors, // highlight-next-line ERC721("CrossChainWarriors", "CCWAR") { - // highlight-next-line - using Counters for Counters.Counter; - bytes32 public constant CROSS_CHAIN_WARRIORS_MESSAGE_TYPE = - keccak256("CROSS_CHAIN_CROSS_CHAIN_WARRIORS"); + error InvalidMessageType(); event CrossChainWarriorsEvent(uint256, address, address); event CrossChainWarriorsRevertedEvent(uint256, address, address); + // highlight-start + using Counters for Counters.Counter; + Counters.Counter public tokenIds; + // highlight-end + bytes32 public constant CROSS_CHAIN_WARRIORS_MESSAGE_TYPE = + keccak256("CROSS_CHAIN_CROSS_CHAIN_WARRIORS"); ZetaTokenConsumer private immutable _zetaConsumer; IERC20 internal immutable _zetaToken; - // highlight-next-line - Counters.Counter public tokenIds; constructor( address connectorAddress, address zetaTokenAddress, address zetaConsumerAddress, + // highlight-next-line bool useEven ) ZetaInteractor(connectorAddress) { _zetaToken = IERC20(zetaTokenAddress); @@ -111,16 +103,12 @@ contract CrossChainWarriors is function _burnWarrior(uint256 burnedWarriorId) internal { _burn(burnedWarriorId); } - // highlight-end function sendMessage( uint256 destinationChainId, uint256 token, - // remove-next-line - address sender, address to - ) external payable { if (!_isValidChainId(destinationChainId)) revert InvalidDestinationChainId(); @@ -138,7 +126,7 @@ contract CrossChainWarriors is ZetaInterfaces.SendInput({ destinationChainId: destinationChainId, destinationAddress: interactorsByChainId[destinationChainId], - destinationGasLimit: 500000, + destinationGasLimit: 300000, message: abi.encode( CROSS_CHAIN_WARRIORS_MESSAGE_TYPE, token, @@ -197,14 +185,15 @@ Introduce a new state variable by leveraging the `Counters.Counter` data structure. Name this variable `tokenIds`. This state variable will be used to manage unique IDs for the ERC721 tokens that the contract will mint. -Modify the constructor of the contract to incorporate the changes. It's -important that the `tokenIds` counter is incremented twice when the contract is -deployed. This action guarantees unique IDs for the initial tokens. +Modify the constructor of the contract to accept a new parameter `bool useEven`. +This parameter will be used to determine the parity of NFT IDs on different +chains: even IDs on one chain and odd IDs on another. This action guarantees +unique IDs for the initial tokens. Furthermore, incorporate a series of new functions to extend the contract's functionalities: -- Introduce a `mint(address to)`` function, a public-facing method that allows +- Introduce a `mint(address to)` function, a public-facing method that allows minting a new ERC721 token to a specified address and returns the ID of the newly minted token. Remember to increment the tokenIds counter twice within this function to ensure unique IDs. @@ -240,7 +229,7 @@ function on it, searches the events for a "Transfer" event and prints out the token ID. ```ts title="tasks/mint.ts" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/messaging/warriors/tasks/mint.ts +https://github.com/zeta-chain/example-contracts/blob/main/messaging/warriors/tasks/mint.ts ``` ```ts title="hardhat.config.ts" @@ -259,13 +248,12 @@ const paramSender = hre.ethers.utils.getAddress(args.sender); const tx = await contract .connect(signer) // highlight-next-line - .sendMessage(destination, paramToken, paramTo, { - value: parseEther(args.amount), - }); + .sendMessage(destination, paramToken, paramTo, { value }); //... task("interact", "Sends a message from one chain to another.", main) + .addFlag("json", "Output JSON") .addParam("contract", "Contract address") .addParam("amount", "Token amount to send") .addParam("destination", "Destination chain") @@ -275,10 +263,6 @@ task("interact", "Sends a message from one chain to another.", main) .addParam("to", "address"); ``` -``` -npx hardhat transfer --network goerli --contract 0xFeAF74733B6f046F3d609e663F667Ba61B19A148 --address 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 --destination 97 --token 2 --amount 0.4 -``` - ## Update the Deploy Task Modify the deploy task by adding a new argument `parity` and passing it to the @@ -288,27 +272,32 @@ of NFT IDs on different chains: even IDs on one chain and odd IDs on another. ```ts title="tasks/deploy.ts" const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const networks = args.networks.split(","); - // A mapping between network names and deployed contract addresses. const contracts: { [key: string]: string } = {}; await Promise.all( // highlight-start networks.map(async (networkName: string, i: number) => { const parity = i % 2 == 0; - contracts[networkName] = await deployContract(hre, networkName, parity); + contracts[networkName] = await deployContract( + hre, + networkName, + parity, + args.json, + args.gasLimit + ); }) // highlight-end ); - for (const source in contracts) { - await setInteractors(hre, source, contracts); - } + // ... }; const deployContract = async ( hre: HardhatRuntimeEnvironment, networkName: string, // highlight-next-line - parity: boolean + parity: boolean, + json: boolean = false, + gasLimit: number ) => { //... const contract = await factory.deploy( @@ -316,7 +305,8 @@ const deployContract = async ( zetaToken, zetaTokenConsumerUniV2 || zetaTokenConsumerUniV3, // highlight-next-line - parity + parity, + { gasLimit } ); //... }; diff --git a/docs/developers/cross-chain-messaging/examples/multi-chain-value.md b/docs/developers/cross-chain-messaging/examples/multi-chain-value.md index c51def99..a20140dc 100644 --- a/docs/developers/cross-chain-messaging/examples/multi-chain-value.md +++ b/docs/developers/cross-chain-messaging/examples/multi-chain-value.md @@ -8,65 +8,125 @@ title: Multichain Value Transfer # Multichain Value Transfer -This is an example contract that shows how you can send value across chains via -the ZetaChain API. +In this tutorial you will learn how to send ZETA tokens between connected +blockchains using ZetaChain. -From this rudimentary example, you could easily extend it to support arbitrary -asset exchanges via a swap to/from ZETA on source and destination. - -# Multichain Value Transfer - -In this tutorial we will create a contract that allows sending value from one -chain to another using the -[Connector API](/developers/cross-chain-messaging/connector/). +In this example you will only be sending ZETA tokens without any associated +message. ## Set up your environment ``` git clone https://github.com/zeta-chain/template +cd template +yarn ``` -Install the dependencies: +## Create the Contract + +To create a new cross-chain messaging contract you will use the `messaging` +Hardhat task available by default in the template. ``` -yarn add --dev @openzeppelin/contracts +npx hardhat messaging Value --fees zetaRevert ``` -## Create a new contract - -```solidity title="contracts/MultiChainValue.sol" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/messaging/value/contracts/MultiChainValue.sol +Use the `--fees` flag to specify that you want your contract to accept ZETA +tokens as fees. Since the purpose of this contract is to send ZETA tokens, it +makes sense to also use ZETA tokens as fees. + +## Modify the Contract + +```solidity title="contracts/Value.sol" +// SPDX-License-Identifier: MIT +pragma solidity 0.8.7; + +import "@openzeppelin/contracts/interfaces/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; +import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; +import "@zetachain/protocol-contracts/contracts/evm/Zeta.eth.sol"; + +// highlight-next-line +contract Value is ZetaInteractor { + error ErrorTransferringZeta(); + // remove-start + error InvalidMessageType(); + + event ValueEvent(); + event ValueRevertedEvent(); + + bytes32 public constant VALUE_MESSAGE_TYPE = keccak256("CROSS_CHAIN_VALUE"); + // remove-end + IERC20 internal immutable _zetaToken; + + constructor( + address connectorAddress, + address zetaTokenAddress + ) ZetaInteractor(connectorAddress) { + _zetaToken = IERC20(zetaTokenAddress); + } + + function sendMessage( + uint256 destinationChainId, + uint256 zetaValueAndGas + ) external payable { + if (!_isValidChainId(destinationChainId)) + revert InvalidDestinationChainId(); + + bool success1 = _zetaToken.approve(address(connector), zetaValueAndGas); + bool success2 = _zetaToken.transferFrom( + msg.sender, + address(this), + zetaValueAndGas + ); + if (!(success1 && success2)) revert ErrorTransferringZeta(); + + connector.send( + ZetaInterfaces.SendInput({ + destinationChainId: destinationChainId, + destinationAddress: interactorsByChainId[destinationChainId], + destinationGasLimit: 300000, + // highlight-next-line + message: abi.encode(), + zetaValueAndGas: zetaValueAndGas, + zetaParams: abi.encode("") + }) + ); + } + + // remove-start + function onZetaMessage( + ZetaInterfaces.ZetaMessage calldata zetaMessage + ) external override isValidMessageCall(zetaMessage) { + bytes32 messageType = abi.decode(zetaMessage.message, (bytes32)); + + if (messageType != VALUE_MESSAGE_TYPE) revert InvalidMessageType(); + + emit ValueEvent(); + } + + function onZetaRevert( + ZetaInterfaces.ZetaRevert calldata zetaRevert + ) external override isValidRevertCall(zetaRevert) { + bytes32 messageType = abi.decode(zetaRevert.message, (bytes32)); + + if (messageType != VALUE_MESSAGE_TYPE) revert InvalidMessageType(); + + emit ValueRevertedEvent(); + } + // remove-end +} ``` -The contract's main functionality is implemented in the `sendValue` function. - -The `send` function first checks if the destination chain ID is valid and if the -Zeta value and gas are not zero. - -Next, it attempts to approve and transfer the specified amount of Zeta tokens -from the sender's address to the contract's address. +Modify the contract so that it only inherits from `ZetaInteractor`. Since the +purpose of the contract is to only send ZETA tokens (and not messages), it +doesn't need to inherit from `ZetaMessageReceiver` and implement the +`onZetaMessage` and `onZetaRevert` functions. -Finally, the function calls the "send" function of a connector contract, -providing the necessary inputs such as the destination chain ID, destination -address, gas limit, and other parameters. The function encodes these inputs into -a message and sends it to the connector contract for further processing. +You can also remove the message type from the `connector.send` call. -The contract also uses a notion of "available chains". Before calling the send -function and transferring value between chains you need to call the -`addAvailableChainId` function on the source chain and add the destination chain -ID to the list of available chains. In this example this logic is implemented in -the deploy task. - -## Create a deployment task - -The deploy task is fairly standard. It deploys the contract to two or more -chains and sets the interactors on each chain. Additionally, for this example, -the script also calls the `addAvailableChainId` function on each chain to add -the other chain to the list of available chains. - -```ts title="tasks/deploy.ts" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/messaging/value/tasks/deploy.ts -``` +## Deploy the Contract Clear the cache and artifacts, then compile the contract: @@ -77,34 +137,28 @@ npx hardhat compile --force Run the following command to deploy the contract to two networks: ``` -npx hardhat deploy --networks polygon-mumbai,bsc-testnet +npx hardhat deploy --networks goerli_testnet,mumbai_testnet ``` ## Send a message -Create a new task to send tokens from one chain to another. The task accepts the -following parameters: contract address, recipient address, amount, destination -chain ID, and the source network name. - -Send a message from Polygon Mumbai testnet to BSC testnet (chain ID: 97) using -the contract address (see the output of the `deploy` task). Make sure to submit -enough native tokens with `--amount` to pay for the transaction fees. +Run the following command to send ZETA tokens from Goerli to Mumbai. Please, +note that since the contract expect ZETA tokens as fees, the value of the +`--amount` param is denomited in ZETA tokens. A fraction of the amount will be +deducted as a cross-chain fee, the rest will be sent to the recipient on the +destination chain. ``` -npx hardhat send --contract 0x042AF09ae20f924Ce18Dc3daBFa1866B114aFa89 --address 0xF5a522092F8E4041F038a6d30131192945478Af0 --amount 20 --destination 97 --network polygon-mumbai +npx hardhat interact --contract 0xe6DE62328677C80084b07eF25637EC83A53d69E1 --network goerli_testnet --destination mumbai_testnet --amount 3 🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 -✅ The transaction has been broadcasted to polygon-mumbai -📝 Transaction hash: 0x2748882492a565627e4726658744f443fb993943f25ba73f93dba42ae314e689 - -Please, refer to ZetaChain's explorer for updates on the progress of the cross-chain transaction. - -🌍 Explorer: https://explorer.zetachain.com/address/0x042AF09ae20f924Ce18Dc3daBFa1866B114aFa89 +✅ The transaction has been broadcasted to goerli_testnet +📝 Transaction hash: 0x4996283c199fafe4c15f33a8ef6d4a41d00545b0736bac0e5a74d72fb342b4c7 ``` ## Source Code You can find the source code for the example in this tutorial here: -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/messaging/value +https://github.com/zeta-chain/example-contracts/tree/main/messaging/value diff --git a/docs/developers/cross-chain-messaging/examples/your-first-message.md b/docs/developers/cross-chain-messaging/examples/your-first-message.md index 902400cd..46cdcb2f 100644 --- a/docs/developers/cross-chain-messaging/examples/your-first-message.md +++ b/docs/developers/cross-chain-messaging/examples/your-first-message.md @@ -18,17 +18,12 @@ from one chain to another using the - [Yarn](https://yarnpkg.com/) v1 or later - [Git](https://git-scm.com/) -## Set Up Your Environment +## Set up your environment ``` git clone https://github.com/zeta-chain/template -``` - -Install dependencies: - -``` cd template -yarn add --dev @openzeppelin/contracts +yarn ``` ## Create the Contract diff --git a/docs/developers/omnichain/tutorials/bitcoin-frontend.md b/docs/developers/omnichain/tutorials/bitcoin-frontend.md index f7f9d806..1415301a 100644 --- a/docs/developers/omnichain/tutorials/bitcoin-frontend.md +++ b/docs/developers/omnichain/tutorials/bitcoin-frontend.md @@ -183,8 +183,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 -[Minter](http://localhost:3001/developers/omnichain/tutorials/bitcoin/) tutorial -to deploy a contract that you can call from Bitcoin. +[Staking](http://localhost:3001/developers/omnichain/tutorials/staking/) +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 in XDEFI. If you filled out the "contract" field, diff --git a/docs/developers/omnichain/tutorials/bitcoin.md b/docs/developers/omnichain/tutorials/bitcoin.md deleted file mode 100644 index 100593a0..00000000 --- a/docs/developers/omnichain/tutorials/bitcoin.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Call a Contract From Bitcoin - -In this tutorial you will create an omnichain contract for minting ERC-20 tokens -on ZetaChain. Omnichain contracts can be called from any connected blockchain, -but in this tutorial you will call the contract from Bitcoin. - -Overview: - -- A user sends tBTC to a TSS address on Bitcoin with a memo containing an - omnichain contract address and the recipient's address. -- ZetaChain detects the transaction and calls the `onCrossChainCall` function of - the omnichain contract. -- The `onCrossChainCall` - - - checks if the cross-chain call was made from Bitcoin. If not, it reverts. - - mints ERC-20 tokens (the same amount as tBTC sent in the first step) on - ZetaChain and sends them to the recipient. - -## Set Up Your Environment - -Clone the Hardhat contract template: - -``` -git clone https://github.com/zeta-chain/template -``` - -Install dependencies: - -``` -cd template -yarn add --dev @openzeppelin/contracts -``` - -## Create the Contract - -Create a new omnichain contract `Minter` that expects to see a `recipient` -address in the message: - -``` -npx hardhat omnichain Minter recipient:address -``` - -```solidity title="contracts/Minter.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"; -// highlight-next-line -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -// highlight-next-line -contract Minter is zContract, ERC20 { - error SenderNotSystemContract(); - // highlight-next-line - error WrongChain(); - - SystemContract public immutable systemContract; - // highlight-next-line - uint256 public immutable chain; - - // highlight-start - constructor( - string memory name, - string memory symbol, - uint256 chainID, - address systemContractAddress - ) ERC20(name, symbol) { - // highlight-end - systemContract = SystemContract(systemContractAddress); - // highlight-next-line - chain = chainID; - } - - function onCrossChainCall( - zContext calldata context, - address zrc20, - uint256 amount, - bytes calldata message - ) external virtual override { - if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); - } - address recipient = abi.decode(message, (address)); - // highlight-start - address acceptedZRC20 = systemContract.gasCoinZRC20ByChainId(chain); - if (zrc20 != acceptedZRC20) revert WrongChain(); - - _mint(recipient, amount); - // highlight-end - } -} -``` - -Contract's constructor accepts a name of the token, a symbol, a chain ID of the -chain from which the contract is allowed to be called (in our example, we will -provide Bitcoin Testnet's chain ID) and a system contract address. - -When `onCrossChainCall` is called, the contract checks if the call was made from -the allowed chain. If not, it reverts. If the call was made from the allowed -chain, the contract converts the `message` from bytes into the `recipient` -address and mints new ERC-20 tokens to that address. - -## Modify the Deploy Task - -```ts title="tasks/deploy.ts" -const main = async (args: any, hre: HardhatRuntimeEnvironment) => { - // ... - // highlight-start - const contract = await factory.deploy( - "Wrapped tBTC", - "WTBTC", - 18332, - systemContract - ); - // highlight-end - // ... -}; - -task("deploy", "Deploy the contract", main); -``` - -## 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/omnichain/tutorials/hello#create-an-account) -from the faucet. - -## Deploy the Contract - -Clear the cache and artifacts, then compile the contract: - -``` -npx hardhat compile --force -``` - -Deploy the contract to ZetaChain: - -``` -npx hardhat deploy --network zeta_testnet -``` - -``` -🔑 Using account: 0x1bE17D79b60182D7F3573576B7807F6C20Ae7C99 - -🚀 Successfully deployed contract on ZetaChain. -📜 Contract address: 0xE26F2e102E2f3267777F288389435d3037D14bb3 -🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xE26F2e102E2f3267777F288389435d3037D14bb3 -``` - -## Calling the contract from Bitcoin Testnet - -Ensure that you have an account and tBTC on the Bitcoin Testnet. You can get -some from a faucet. - -Use the `send-btc` command to send tBTC to the [TSS address](/reference/testnet) -on Bitcoin Testnet set as the `--recipient`: - -``` -npx hardhat send-btc --amount 0.001 --memo 629eEe97B95Bd6e04B0885De58eF016177a709Ae2cD3D070aE1BD365909dD859d29F387AA96911e1 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur -``` - -The memo contains the following: - -- the address of the omnichain contract on ZetaChain that will be called once - the cross-chain transaction is processed without the `0x` prefix: - `629eEe97B95Bd6e04B0885De58eF016177a709Ae`. -- a list of arguments that will be passed to the contracts as the `message` - bytes. In our case, it's a single value, the recipient's address without the - `0x` prefix: `2cD3D070aE1BD365909dD859d29F387AA96911e1`. - -You can learn more about how to construct the memo in the -[Bitcoin section](/developers/omnichain/bitcoin). - -The `send-btc` command will return the transaction hash. You can use the `cctx` -command to track the status of the cross-chain transaction: - -``` -npx hardhat cctx --tx TX_HASH -``` - -Add the token contract address `0x629eEe97B95Bd6e04B0885De58eF016177a709Ae` to -Metamask (or any other wallet) to be able to check the balance of the ERC-20 -token minted by the contract. - -Once the transaction is processed, you will be able to see the udpated balance -of the WTBTC token in Metamask. diff --git a/docs/developers/omnichain/tutorials/curve.md b/docs/developers/omnichain/tutorials/curve.md index 32385dd6..11f9c84c 100644 --- a/docs/developers/omnichain/tutorials/curve.md +++ b/docs/developers/omnichain/tutorials/curve.md @@ -35,7 +35,7 @@ Now that you have Curve and the pool you want deployed, swapping would look just like this: ```solidity reference -https://github.com/zeta-chain/zetachain/blob/main/packages/zevm-example-contracts/contracts/zeta-swap/ZetaCurveSwapDemo.sol +https://github.com/zeta-chain/zetachain/blob/main/packages/zevm-app-contracts/contracts/zeta-swap/ZetaCurveSwapDemo.sol ``` In this example `crvZRC20s` is an array of three ZRC20 tokens, for example gETH, diff --git a/docs/developers/omnichain/tutorials/hello.md b/docs/developers/omnichain/tutorials/hello.md index 5c25ed2b..e0e9f985 100644 --- a/docs/developers/omnichain/tutorials/hello.md +++ b/docs/developers/omnichain/tutorials/hello.md @@ -63,23 +63,26 @@ import "@zetachain/protocol-contracts/contracts/zevm/SystemContract.sol"; import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol"; contract MyContract is zContract { - error SenderNotSystemContract(); - SystemContract public immutable systemContract; constructor(address systemContractAddress) { systemContract = SystemContract(systemContractAddress); } + modifier onlySystem() { + require( + msg.sender == address(systemContract), + "Only system contract can call this function" + ); + _; + } + function onCrossChainCall( zContext calldata context, address zrc20, uint256 amount, bytes calldata message - ) external virtual override { - if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); - } + ) external virtual override onlySystem { // TODO: implement the logic } } @@ -113,15 +116,9 @@ function receives the following inputs: 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 following check ensures that the function is -called only as a response to a token transfer transaction sent to the TSS -address: - -```solidity -if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); -} -``` +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. By default, the `onCrossChainCall` function doesn't do anything else. You will implement the logic yourself based on your use case. @@ -143,7 +140,11 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { } const [signer] = await hre.ethers.getSigners(); - console.log(`🔑 Using account: ${signer.address}\n`); + 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)` + ); + } const systemContract = getAddress("systemContract", "zeta_testnet"); @@ -151,13 +152,19 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const contract = await factory.deploy(systemContract); await contract.deployed(); - console.log(`🚀 Successfully deployed contract on ZetaChain. + if (args.json) { + console.log(JSON.stringify(contract)); + } else { + console.log(`🔑 Using account: ${signer.address} + +🚀 Successfully deployed contract on ZetaChain. 📜 Contract address: ${contract.address} 🌍 Explorer: https://athens3.explorer.zetachain.com/address/${contract.address} `); + } }; -task("deploy", "Deploy the contract", main); +task("deploy", "Deploy the contract", main).addFlag("json", "Output in JSON"); ``` Omnichain contracts are supposed to be deployed to ZetaChain, so the task checks @@ -178,11 +185,10 @@ import { task } from "hardhat/config"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { parseEther } from "@ethersproject/units"; import { getAddress } from "@zetachain/protocol-contracts"; -import { prepareData, trackCCTX } from "@zetachain/toolkit/helpers"; +import { prepareData } from "@zetachain/toolkit/helpers"; const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const [signer] = await hre.ethers.getSigners(); - console.log(`🔑 Using account: ${signer.address}\n`); const data = prepareData(args.contract, [], []); const to = getAddress("tss", hre.network.name); @@ -190,16 +196,21 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => { const tx = await signer.sendTransaction({ data, to, value }); - console.log(` -🚀 Successfully broadcasted a token transfer transaction on ${hre.network.name} network. + 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} `); - await trackCCTX(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"); + .addParam("amount", "Amount of tokens to send") + .addFlag("json", "Output in JSON"); ``` The task uses the `prepareData` function from `@zetachain/toolkit/helpers` to @@ -224,12 +235,6 @@ address. The transaction contains the following information: - `to`: the address of the TSS - `value`: the amount of tokens to transfer -Finally, the task uses the `trackCCTX` function from -`@zetachain/toolkit/helpers` to track the token transfer transaction. The -function waits for the transaction to appear on ZetaChain and tracks the status -of the transaction. Transaction tracking is optional, but helpful to know when -the transaction has been processed by ZetaChain. - ## Create an Account To deploy and interact with the contract you will need a wallet with tokens. @@ -261,7 +266,7 @@ tokens on connected chains. You will, however, need to request native tokens on connected chains from one of the -[publicly available faucets](http://localhost:3001/reference/get-testnet-zeta/#getting-additional-testnet-gas-assets). +[publicly available faucets](/reference/get-testnet-zeta/#getting-additional-testnet-gas-assets). ## Check Token Balances @@ -272,6 +277,9 @@ the connected chains: npx hardhat balances ``` +Learn more about these and other ZetaChain toolkit commands +[avaialble in the template](/developers/template/). + ## Deploy the Contract Clear the cache and artifacts, then compile the contract: @@ -307,18 +315,12 @@ npx hardhat interact --contract 0xE26F2e102E2f3267777F288389435d3037D14bb3 --amo 🚀 Successfully broadcasted a token transfer transaction on goerli_testnet network. 📝 Transaction hash: 0x93b441dc2ddb751a60a2f4c0fc52dbbd447ed70eb962b1a01072328aa6872b73 - -✔ CCTX hash found: 0x31310706ac4b33aa468e62a77d5db358e52a60dad3854210db8fc06c870186b6 - -ℹ Status updated to "OutboundMined": Remote omnichain contract call completed - -✔ CCTX has been finalized on ZetaChain ``` Once the transaction is finalized on ZetaChain, you should be able to review the transaction on the ZetaChain explorer: -https://athens3.explorer.zetachain.com/cc/tx/0x31310706ac4b33aa468e62a77d5db358e52a60dad3854210db8fc06c870186b6 +https://explorer.zetachain.com/cc/tx/0x31310706ac4b33aa468e62a77d5db358e52a60dad3854210db8fc06c870186b6 The `interact` task has sent a token transfer transaction to the TSS address on Goerli. The transaction contains the address of the contract on ZetaChain in the diff --git a/docs/developers/omnichain/tutorials/single-input-multiple-output.md b/docs/developers/omnichain/tutorials/single-input-multiple-output.md index 8fb07396..9d81a285 100644 --- a/docs/developers/omnichain/tutorials/single-input-multiple-output.md +++ b/docs/developers/omnichain/tutorials/single-input-multiple-output.md @@ -18,18 +18,15 @@ 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 ``` -Install the dependencies: - -``` -yarn add --dev @openzeppelin/contracts -``` - ## Create the contract Run the following command to create a new omnichain contract called @@ -69,6 +66,14 @@ contract MultiOutput is zContract, Ownable { systemContract = SystemContract(systemContractAddress); } + modifier onlySystem() { + require( + msg.sender == address(systemContract), + "Only system contract can call this function" + ); + _; + } + // highlight-start function registerDestinationToken( address destinationToken @@ -93,10 +98,7 @@ contract MultiOutput is zContract, Ownable { address zrc20, uint256 amount, bytes calldata message - ) external virtual override { - if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); - } + ) external virtual override onlySystem { address recipient = abi.decode(message, (address)); // highlight-start if (_getTotalTransfers(zrc20) == 0) revert NoAvailableTransfers(); @@ -147,6 +149,16 @@ Before proceeding with the next steps, make sure you have [created an account and requested ZETA tokens](/developers/omnichain/tutorials/hello#create-an-account) from the faucet. +## Create a task to set destination chain + +```ts title="tasks/destination.ts" reference +https://github.com/zeta-chain/example-contracts/blob/main/omnichain/multioutput/tasks/destination.ts +``` + +```ts title="hardhat.config.ts" +import "./tasks/destination"; +``` + ## Deploy the Contract Clear the cache and artifacts, then compile the contract: @@ -167,39 +179,6 @@ npx hardhat deploy --network zeta_testnet 🌍 Explorer: https://athens3.explorer.zetachain.com/address/0x040FDDE34d07e1FBA155DCCe829a250317985d83 ``` -## Create a task to set destination chain - -```ts title="tasks/destination.ts" -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { getAddress } from "@zetachain/protocol-contracts"; - -const main = async (args: any, hre: HardhatRuntimeEnvironment) => { - const [signer] = await hre.ethers.getSigners(); - console.log(`🔑 Using account: ${signer.address}\n`); - - const destinationToken = getAddress("zrc20" as any, args.destination as any); - const ZetaMultiOutput = await hre.ethers.getContractAt( - "MultiOutput", - args.contract - ); - - const tx = await ZetaMultiOutput.registerDestinationToken(destinationToken); - - await tx.wait(); - - console.log( - `Registered token ${destinationToken} as a destination in the contract ${args.contract}` - ); -}; - -task("destination", "", main).addParam("contract").addParam("destination"); -``` - -```ts title="hardhat.config.ts" -import "./tasks/destination"; -``` - ## Interact with the Contract Set the destination chain to Mumbai: diff --git a/docs/developers/omnichain/tutorials/swap.md b/docs/developers/omnichain/tutorials/swap.md index bc3fc666..5c95b4c1 100644 --- a/docs/developers/omnichain/tutorials/swap.md +++ b/docs/developers/omnichain/tutorials/swap.md @@ -2,35 +2,18 @@ sidebar_position: 4 --- -# Zeta Swap +# Swap ## Overview -In this tutorial you will write a contract that allows users to swap native -tokens from one connected chain to another through ZetaChain. - -High-level overview: - -1. A `ZetaSwapV2.sol` contract is created and deployed to ZetaChain. -2. A user wants to swap tMATIC from Polygon Mumbai to gETH on Goerli. -3. A user transfers a native gas token (in this example, tMATIC) to a specific - address (called TSS) on Mumbai. The `data` value of the token transfer - transaction contains the following information: - 1. address of the ZetaSwapV2 contract on Zetachain - 2. recipients address (defaults to the sender's address) - 3. destination token address - 4. minimal output amount (not covered in this tutorial, set to 0) -4. ZetaChain detects the token transfer transaction and triggers the - `onCrossChainCall()` function of the ZetaSwapV2 contract. -5. `onCrossChainCall()` does the following: - 1. calls the UniswapV2 router contract (Uniswap contracts have already been - deployed to ZetaChain), specifically `swapExactTokensForTokens` to swap - tMATIC represented on ZetaChain as a ZRC20 for gETH also represented as a - ZRC20. - 2. calls ZetaChain's `withdraw` to withdraw native gas token (gETH) on the - destination chain (Goerli). - -## Set up your environment +In this tutorial you will write a cross-chain swap contract that allows users to +transfer native tokens from one of the connected chains to ZetaChain, swap them +for a ZRC-20 representation of a token on another chain, and withdraw the tokens +to the recipient address on the target chain. + +## Set Up Your Environment + +Clone the Hardhat contract template: ``` git clone https://github.com/zeta-chain/template @@ -40,7 +23,7 @@ Install dependencies: ``` cd template -yarn add --dev @uniswap/v2-periphery @uniswap/v2-core +yarn ``` ## Create the contract @@ -48,29 +31,71 @@ yarn add --dev @uniswap/v2-periphery @uniswap/v2-core Run the following command to create a new omnichain contract called `Swap`. ``` -npx hardhat omnichain Swap targetZRC20:address recipient:bytes32 minAmountOut:uint256 +npx hardhat omnichain Swap ``` -Modify the `onCrossChainCall` function to perform a swap: +## Omnichain Contract ```solidity title="contracts/Swap.sol" -// highlight-next-line +// 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"; +// highlight-start import "@zetachain/toolkit/contracts/SwapHelperLib.sol"; +import "@zetachain/toolkit/contracts/BytesHelperLib.sol"; +// highlight-end contract Swap is zContract { - //... + SystemContract public immutable systemContract; + // highlight-start + uint256 constant BITCOIN = 18332; + error WrongGasContract(); + error NotEnoughToPayGasFee(); + // highlight-end + + constructor(address systemContractAddress) { + systemContract = SystemContract(systemContractAddress); + } + + modifier onlySystem() { + require( + msg.sender == address(systemContract), + "Only system contract can call this function" + ); + _; + } + function onCrossChainCall( zContext calldata context, address zrc20, uint256 amount, bytes calldata message - ) external virtual override { - if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); - } - (address targetZRC20, bytes32 recipient, uint256 minAmountOut) = abi - .decode(message, (address, bytes32, uint256)); + ) external virtual override onlySystem { // highlight-start + uint32 targetChainID; + address recipient; + uint256 minAmountOut; + + if (context.chainID == BITCOIN) { + targetChainID = BytesHelperLib.bytesToUint32(message, 0); + recipient = BytesHelperLib.bytesToAddress(message, 4); + } else { + ( + uint32 targetChainID_, + address recipient_, + uint256 minAmountOut_ + ) = abi.decode(message, (uint32, address, uint256)); + targetChainID = targetChainID_; + recipient = recipient_; + minAmountOut = minAmountOut_; + } + + address targetZRC20 = systemContract.gasCoinZRC20ByChainId( + targetChainID + ); + uint256 outputAmount = SwapHelperLib._doSwap( systemContract.wZetaContractAddress(), systemContract.uniswapv2FactoryAddress(), @@ -80,151 +105,176 @@ contract Swap is zContract { targetZRC20, minAmountOut ); - SwapHelperLib._doWithdrawal(targetZRC20, outputAmount, recipient); + + (address gasZRC20, uint256 gasFee) = IZRC20(targetZRC20) + .withdrawGasFee(); + + if (gasZRC20 != targetZRC20) revert WrongGasContract(); + if (gasFee >= outputAmount) revert NotEnoughToPayGasFee(); + + IZRC20(targetZRC20).approve(targetZRC20, gasFee); + IZRC20(targetZRC20).withdraw( + abi.encodePacked(recipient), + outputAmount - gasFee + ); // highlight-end } } ``` +The contract expects to receive three values in the `message`: + +- `targetChainID`: the ID of the destination chain +- `recipient`: the recipient address on the destination chain +- `minAmountOut`: and the minimum amount of tokens to receive on the destination + chain. For the purposes of this tutorial, we will set this value to 0. + +When the contract is called from an EVM chain, the `message` is encoded as a +`bytes` array using the ABI encoding. + +When the contract is called from Bitcoin it's up to us to encode and then decode +the message. + +Use `context.chainID` to determine the connected chain from which the contract +is called. + +If it's Bitcoin, the first 4 bytes of the `message` are the `targetChainID` +encoded as a `uint32`. Use `bytesToUint32` helper method to get the target chain +ID. To get the recipient address, use `bytesToAddress` helper method with an +offset of 4 bytes. + +If it's an EVM chain, use `abi.decode` to decode the `message` into the +`targetChainID`, `recipient` and `minAmountOut` variables. + +Use the `systemContract` to get the address of the ZRC-20 representation of the +gas coin on the destination chain. + +Next, swap the incoming token for the gas coin on the destination chain. . +ZetaChain has liquidity pools with the ZRC-20 representation of the gas coin on +all connected chains. The `SwapHelperLib._doSwap` helper method to swap the +tokens. + +Finally, withdraw the tokens to the recipient address on the destination chain. + ## Modify the Interact Task +Modify the interact task to convert chain label (for example, `goerli_testnet`) +to chain ID (for example, `5`). Update the `prepareData` method with the values +which will be used to encode the `message`. + ```ts title="tasks/interact.ts" +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { parseEther } from "@ethersproject/units"; +import { getAddress } from "@zetachain/protocol-contracts"; +import { prepareData } from "@zetachain/toolkit/helpers"; // highlight-next-line import { BigNumber } from "@ethersproject/bignumber"; const main = async (args: any, hre: HardhatRuntimeEnvironment) => { - //... + const [signer] = await hre.ethers.getSigners(); + // highlight-start - const targetZRC20 = getAddress("zrc20" as any, args.destination as any); + const targetChainID = hre.config.networks[args.destination]?.chainId; + if (targetChainID === undefined) { + throw new Error("Invalid destination network"); + } const minAmountOut = BigNumber.from("0"); // highlight-end const data = prepareData( args.contract, - ["address", "bytes32", "uint256"], - // highlight-next-line - [targetZRC20, args.recipient, minAmountOut] + // highlight-start + ["uint32", "address", "uint256"], + [targetChainID, args.recipient, minAmountOut] + // highlight-end ); - //... + const to = getAddress("tss", hre.network.name); + const value = parseEther(args.amount); + + const 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") + .addFlag("json", "Output in JSON") + // highlight-start .addParam("recipient") - // remove-start - .addParam("minAmountOut") - .addParam("targetZRC20") - // remove-end - // highlight-next-line .addParam("destination"); +// highlight-end ``` -The code generation command automatically created all three parameters for the -`interact` task. Instead of asking the user to provide the `targetZRC20` and the -`minAmountOut` parameters, you can define them in the task itself. Use the -`getAddress` to fetch the right ZRC-20 address for the destination chain and add -a `"destination"` parameter. Use a hard-coded value of 0 for the `minAmountOut` -parameter. - -## Write a test for the contract - -```ts title="test/swap.ts" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/omnichain/swap/test/swap.ts -``` +## Create an Account and Request Tokens from the Faucet -### Import contracts +Before proceeding with the next steps, make sure you have +[created an account and requested ZETA tokens](/developers/omnichain/tutorials/hello#create-an-account) +from the faucet. -The test depends on types from external contracts. Import these contracts to -enable Hardhat to compile their types. +## Deploy the Contract -```solidity title="contracts/TestContracts.sol" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/omnichain/swap/contracts/TestContracts.sol ``` - -```solidity title="contracts/TestUniswap.sol" reference -https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/omnichain/swap/contracts/TestUniswap.sol +npx hardhat deploy --network zeta_testnet ``` -### Configure the Hardhat environment - -Take note that the Uniswap contracts require a specific version of Solidity, -which differs from the version used for the remaining contracts. Update the -Hardhat configuration to include both versions of Solidity. - -```ts title="hardhat.config.ts" -const config: HardhatUserConfig = { - // highlight-start - solidity: { - compilers: [ - { version: "0.6.6" /** For uniswap v2 */ }, - { version: "0.8.7" }, - ], - }, - // highlight-end -}; ``` +🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 -### Run the test - -``` -npx hardhat test +🚀 Successfully deployed contract on ZetaChain. +📜 Contract address: 0x458bCAF5d95025cdd00f946f1C5F09623E856579 +🌍 Explorer: https://athens3.explorer.zetachain.com/address/0x458bCAF5d95025cdd00f946f1C5F09623E856579 ``` -``` - ZetaSwap tests - zetaSwapV2 -Getting uniswapV2Router02 address from mainnet: eth-mainnet. -Getting uniswapV2Factory address from mainnet: eth-mainnet. -Getting weth9 address from mainnet: eth-mainnet. - ✔ Should do swap (60ms) +## Swap from an EVM Chain +Use the `interact` task to perform a cross-chain swap: - 1 passing (9s) ``` - -## 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/omnichain/tutorials/hello#create-an-account) -from the faucet. - -## Deploying the contract - -Clear the cache and artifacts, then compile the contract: +px hardhat interact --contract 0x458bCAF5d95025cdd00f946f1C5F09623E856579 --amount 0.05 --network goerli_testnet --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 --destination mumbai_testnet +``` ``` -npx hardhat compile --force +🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 + +🚀 Successfully broadcasted a token transfer transaction on goerli_testnet network. +📝 Transaction hash: 0x47262aade728b9c8897bf4c8932c26774caf3b9ebd085cbc048dd9f6522ccf00 ``` -Use the `deploy` task to deploy the contract to ZetaChain: +Track your cross-chain transaction: ``` -npx hardhat deploy --network zeta_testnet +npx hardhat cctx 0x47262aade728b9c8897bf4c8932c26774caf3b9ebd085cbc048dd9f6522ccf00 ``` ``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 +✓ CCTXs on ZetaChain found. -🚀 Successfully deployed contract on ZetaChain. -📜 Contract address: 0xd6FB957c64f5197C2e630Cb5D995C0845505957C -🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xd6FB957c64f5197C2e630Cb5D995C0845505957C +✓ 0xee7ae0367ee5fa52e17ae0c01d8c929b009d0601a5078ddcb319910ddda493e0: 5 → 7001: OutboundMined (Remote omnichain contract call completed) +✓ 0xaa120bee04c86b1c5d16b1d47d69fd9a3c49fbed91ee15f4f59ef9eeef48ca88: 7001 → 80001: OutboundMined ``` -## Execute a swap +## Swap from Bitcoin -Use the `interact` task to swap 30 tMATIC from Mumbai for an amount of gETH and -withdraw the tokens to Goerli transferring them to a specific recipient address: +Use the `send-btc` task to send Bitcoin to the TSS address with a memo. The memo +should contain the following: -``` -npx hardhat interact --contract 0xd6FB957c64f5197C2e630Cb5D995C0845505957C --amount 30 --network mumbai_testnet --destination goerli_testnet --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 -``` +- Omnichain contract address on ZetaChain: + `458bCAF5d95025cdd00f946f1C5F09623E856579` +- Target chain ID: `00000005` +- Recipient address: `2cD3D070aE1BD365909dD859d29F387AA96911e1` ``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -🚀 Successfully broadcasted a token transfer transaction on mumbai_testnet network. -📝 Transaction hash: 0x808a9524c5ab6012b24cbf1c8417a6b7c36c407e9d7d22273f2797f81b892afe +npx hardhat send-btc --amount 0.001 --memo 458bCAF5d95025cdd00f946f1C5F09623E856579000000052cD3D070aE1BD365909dD859d29F387AA96911e1 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur ``` ## Source Code diff --git a/docs/developers/omnichain/tutorials/withdraw.md b/docs/developers/omnichain/tutorials/withdraw.md deleted file mode 100644 index 9f548051..00000000 --- a/docs/developers/omnichain/tutorials/withdraw.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Withdraw - -In this tutorial you will write an omnichain contract that accepts an argument -and uses the `withdraw` functionality of ZRC-20. The purpose of this tutorial is -to demonstrate how you can pass a value to an omnichain contract, send a native -token to the contract, and withdraw the token back to the source network. - -High-level overview: - -1. A `Withdraw.sol` contract is created and deployed to ZetaChain. -2. A user transfers tokens to a specific address (called TSS) on the source - network (Goerli, in this example). The `data` value of the token transfer - transaction contains the address of the Withdraw contract on Zetachain and - the recipient address back on the source network (back to Goerli in this - example). -3. ZetaChain detects the token transfer transaction and triggers the - `onCrossChainCall()` function of the Withdraw contract. -4. `onCrossChainCall()` uses ZRC20's `withdraw()` to initiate the process of - transferring the tokens to the recipient address on the source network. - -Note that the TSS address is a special address that is used to trigger -cross-chain transactions. The ZetaChain network has a TSS address on each -connected chain (like Goerli or Polygon Mumbai), and token transfers to this TSS -address can call any ZetaChain contract (which contract is called is determined -by the payload of the data field of the transaction). - -## Set up your environment - -``` -git clone https://github.com/zeta-chain/template -``` - -Install dependencies: - -``` -cd template -yarn -``` - -## Creating the contract - -Run the following command to create a new omnichain contract called `Withdraw`. - -``` -npx hardhat omnichain Withdraw recipient:bytes32 -``` - -You're passing a `recipient` parameter of type `bytes32` to the command to -specify the arguments to the cross-chain call. These parameters will be used to -generate the `onCrossChainCall` function and the `interact` task. - -Modify the `onCrossChainCall` function to withdraw the tokens to the recipient: - -```solidity title="contracts/Withdraw.sol" - error SenderNotSystemContract(); - // highlight-start - error WrongGasContract(); - error NotEnoughToPayGasFee(); - error InvalidZRC20Address(); - error ZeroAmount(); - // highlight-end - - function onCrossChainCall( - address zrc20, - uint256 amount, - bytes calldata message - ) external virtual override { - if (msg.sender != address(systemContract)) { - revert SenderNotSystemContract(); - } - bytes32 recipient = abi.decode(message, (bytes32)); - // highlight-start - if (zrc20 == address(0)) revert InvalidZRC20Address(); - if (amount == 0) revert ZeroAmount(); - - (address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee(); - - if (gasZRC20 != zrc20) revert WrongGasContract(); - if (gasFee >= amount) revert NotEnoughToPayGasFee(); - - IZRC20(zrc20).approve(zrc20, gasFee); - IZRC20(zrc20).withdraw(abi.encodePacked(recipient), amount - gasFee); - // highlight-end - } -``` - -First, define error types. These types, `WrongGasContract`, -`NotEnoughToPayGasFee`, `InvalidZRC20Address`, and `ZeroAmount`, replace general -reverts with detailed error messages, offering insights into what causes a -transaction to be reverted. By using custom error types, it becomes easier for -both developers and users to debug transactions that fail due to these specific -issues. - -In the `onCrossChainCall` function, the first two conditions you need to set up -validate that the `zrc20` address isn't a null address and the amount being -transferred isn't zero. If either of these conditions isn't met, make the -contract execution revert with a specific error message (`InvalidZRC20Address` -or `ZeroAmount`, respectively). - -Next, handle the gas fees for the transaction with a pair of conditions. -Retrieve the `gasZRC20` address and `gasFee` amount using the `withdrawGasFee` -method from the `IZRC20` interface. Then, check if the contract address for -withdrawing the gas fee matches the `zrc20` address. If it doesn't, an -`WrongGasContract` error is raised. - -Also, verify that the `gasFee` isn't greater than or equal to the amount being -transacted. If it is, this suggests that the user doesn't have sufficient funds -to cover the gas fee for the transaction, resulting in a `NotEnoughToPayGasFee` -error. - -Finally, approve the gas fee to be withdrawn from the `zrc20` address and -proceed with the withdrawal, passing the recipient address and the amount to be -sent, after the gas fee has been deducted. This step ensures the gas fee is -deducted before the cross chain transfer, safeguarding the contract from users -who might not have enough funds to cover the gas fee. - -## Passing Arguments to the Contract - -In the `interact.ts` you will find the following code: - -```ts title="tasks/interact.ts" -const data = prepareData(args.contract, ["bytes32"], [args.recipient]); -const to = getAddress("tss", hre.network.name); -const value = parseEther(args.amount); - -const tx = await signer.sendTransaction({ data, to, value }); -``` - -The `interact` task uses the `prepareData` helper function to encode the -arguments to the `onCrossChainCall` function. The `prepareData` function takes -the contract address, the types of the arguments, and the values of the -arguments as parameters. It returns the encoded data that can be passed to the -`data` field of the transaction. - -The value of the `data` field contains the address of the Withdraw contract on -ZetaChain and the arguments to the `onCrossChainCall` function (in this example, -the only argument is the recipient address). These arguments are passed to the -`onCrossChainCall` function in the `message` parameter when the function is -called. - -Use `abi.decode` to decode the arguments from the `message` parameter: - -```solidity title="contracts/Withdraw.sol" -function onCrossChainCall( address zrc20, uint256 amount, bytes calldata message ) external virtual override { - bytes32 recipient = abi.decode(message, (bytes32)); - //... -} -``` - -## 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/omnichain/tutorials/hello#create-an-account) -from the faucet. - -## 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: - -``` -npx hardhat deploy --network zeta_testnet -``` - -``` -🔑 Using account: 0x1e89d05e9917e18c198b30d6728399FD63E236bB - -🚀 Successfully deployed contract on ZetaChain. -📜 Contract address: 0x5E3229522BeFD3D9CD422Fd91f4273ed4EB2639a -``` - -## Interacting with the contract - -Use the `interact` task to call the omnichain contract: - -``` -npx hardhat interact --network goerli_testnet --amount 0.5 --contract 0x5E3229522BeFD3D9CD422Fd91f4273ed4EB2639a --recipient 0x2c2d7aF555156eFB8Bfc3eBB0C41fE57D4D1C7c4 -``` - -Where `--contract` is the contract address from the output of the `deploy` task -and `--recipient` is any address on the source network. You're using the -`goerli_testnet` network in this example, but you can use any other supported -testnet like `mumbai_testnet`. - -``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -🚀 Successfully broadcasted a token transfer transaction on goerli_testnet network. -📝 Transaction hash: 0xc6b72c5cc7b7ec68e0853827eab8cead9664b951bfe66340bd2711e2abdf0013 -``` - -You should be able to see the progress of the transaction being tracked on -ZetaChain. Once the transaction is finalized on ZetaChain, you should see a -token transaction to the recipient address on the source network. - -Congratulations! You have successfully created and deployed the Withdraw -contract to ZetaChain, and executed a cross-chain transaction by sending tokens -to a TSS address on Goerli, which triggered a Withdraw contract call on -ZetaChain and withdrew the tokens to the recipient address on Goerli. - -In this tutorial you learned how the `withdraw` of ZRC-20 can be used to -transfer tokens from ZetaChain to the source network. You also learned how to -pass arguments to an omnichain contract. - -## 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/withdraw/ diff --git a/docs/developers/omnichain/zrc-20.md b/docs/developers/omnichain/zrc-20.md index edd0c0fb..99f6d8c3 100644 --- a/docs/developers/omnichain/zrc-20.md +++ b/docs/developers/omnichain/zrc-20.md @@ -154,15 +154,10 @@ tx. In this example, it uses an existing Uniswap deployment with a pool for 2 given tokens. When `onCrossChainCall` is called, it performs a swap to a target ZRC-20 token and withdraws it to an address on a native chain. -Check out the [Withdraw tutorial](/developers/omnichain/tutorials/withdraw) to -see how to deposit tokens to ZetaChain as ZRC-20 and use the `withdraw` function -to withdraw them back to their native chain. - -Or check out a more advanced -[Swap tutorial](/developers/omnichain/tutorials/swap) to see how to deposit -tokens (like tMATIC), swap them to another token (like gETH) using the internal -liquidity pools on ZetaChain, and withdraw them to their native chain (like -Goerli). +Check out the [Swap tutorial](/developers/omnichain/tutorials/swap) to see how +to deposit tokens (like tMATIC), swap them to another token (like gETH) using +the internal liquidity pools on ZetaChain, and withdraw them to their native +chain (like Goerli). ## Building on ZRC-20 diff --git a/docs/reference/wallets.md b/docs/reference/wallets.md index 59dafad0..cd349d93 100644 --- a/docs/reference/wallets.md +++ b/docs/reference/wallets.md @@ -47,7 +47,7 @@ criteria and you will be able to use it to interact with ZetaChain. To test ZetaChain's interoperability with Bitcoin, you can use the `npx hardhat send-btc` available in the smart contract template. Learn more -about it in the [Bitcoin tutorial](/developers/omnichain/tutorials/bitcoin/). +about it in the [Staking](/developers/omnichain/tutorials/staking). ## Cosmos Wallets