From 068440f2605d1db96429d45fa4906d5a683e2de5 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 7 May 2024 20:48:45 +0300 Subject: [PATCH] Update CCM tutorials (#318) --- .../examples/cross-chain-counter.md | 273 ----------- .../examples/cross-chain-nft.md | 377 --------------- .../cross-chain-messaging/examples/erc20.md | 227 +++++++++ .../cross-chain-messaging/examples/message.md | 238 ++++++++++ .../examples/multi-chain-value.md | 164 ------- .../cross-chain-messaging/examples/nft.md | 341 ++++++++++++++ .../examples/your-first-message.md | 287 ------------ .../cross-chain-messaging/examples/zeta.md | 437 ++++++++++++++++++ docusaurus.config.js | 12 + 9 files changed, 1255 insertions(+), 1101 deletions(-) delete mode 100644 docs/developers/cross-chain-messaging/examples/cross-chain-counter.md delete mode 100644 docs/developers/cross-chain-messaging/examples/cross-chain-nft.md create mode 100644 docs/developers/cross-chain-messaging/examples/erc20.md create mode 100644 docs/developers/cross-chain-messaging/examples/message.md delete mode 100644 docs/developers/cross-chain-messaging/examples/multi-chain-value.md create mode 100644 docs/developers/cross-chain-messaging/examples/nft.md delete mode 100644 docs/developers/cross-chain-messaging/examples/your-first-message.md create mode 100644 docs/developers/cross-chain-messaging/examples/zeta.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 deleted file mode 100644 index 2d7320ae..00000000 --- a/docs/developers/cross-chain-messaging/examples/cross-chain-counter.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Cross-Chain Counter -hide_title: true -id: cross-chain-counter -title: Build a Cross-Chain Counter ---- - -# Build a cross-chain counter - -This is an example app of cross-chain counter using -[Zeta Connector](/developers/cross-chain-messaging/connector). - -![Cross-chain counter](/img/graphs/cross-chain-counter.svg) - -## Set up your environment - -``` -git clone https://github.com/zeta-chain/template -cd template -yarn -``` - -## Create a new contract - -``` -npx hardhat messaging Counter from:address -``` - -- `from`: address of the sender - -```solidity title="contracts/Counter.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"; - -contract Counter is ZetaInteractor, ZetaReceiver { - error InvalidMessageType(); - // highlight-next-line - error DecrementOverflow(); - - 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; - - constructor( - address connectorAddress, - address zetaTokenAddress, - address zetaConsumerAddress - ) ZetaInteractor(connectorAddress) { - _zetaToken = IERC20(zetaTokenAddress); - _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); - } - - // highlight-next-line - function sendMessage(uint256 destinationChainId) external payable { - if (!_isValidChainId(destinationChainId)) - revert InvalidDestinationChainId(); - - uint256 crossChainGas = 2 * (10 ** 18); - uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ - value: msg.value - }(address(this), crossChainGas); - _zetaToken.approve(address(connector), zetaValueAndGas); - - connector.send( - ZetaInterfaces.SendInput({ - destinationChainId: destinationChainId, - destinationAddress: interactorsByChainId[destinationChainId], - destinationGasLimit: 300000, - // highlight-next-line - message: abi.encode(COUNTER_MESSAGE_TYPE, msg.sender), - zetaValueAndGas: zetaValueAndGas, - zetaParams: abi.encode("") - }) - ); - } - - function onZetaMessage( - ZetaInterfaces.ZetaMessage calldata zetaMessage - ) external override isValidMessageCall(zetaMessage) { - (bytes32 messageType, address from) = abi.decode( - zetaMessage.message, - (bytes32, address) - ); - - if (messageType != COUNTER_MESSAGE_TYPE) revert InvalidMessageType(); - - // highlight-next-line - counter[from]++; - emit CounterEvent(from); - } - - function onZetaRevert( - ZetaInterfaces.ZetaRevert calldata zetaRevert - ) external override isValidRevertCall(zetaRevert) { - (bytes32 messageType, address from) = abi.decode( - zetaRevert.message, - (bytes32, address) - ); - - if (messageType != COUNTER_MESSAGE_TYPE) revert InvalidMessageType(); - - // highlight-start - if (counter[from] <= 0) revert DecrementOverflow(); - counter[from]--; - // highlight-end - emit CounterRevertedEvent(from); - } -} -``` - -## Create a task to get the counter value - -```ts title="tasks/counter_show.ts" -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; - -const contractName = "CrossChainCounter"; - -const main = async (args: any, hre: HardhatRuntimeEnvironment) => { - const [signer] = await hre.ethers.getSigners(); - console.log(`🔑 Using account: ${signer.address}\n`); - - const factory = await hre.ethers.getContractFactory(contractName); - const contract = factory.attach(args.contract); - - const counter = await contract.counter(signer.address); - - console.log(`🔢 The counter for ${signer.address} is: ${counter.toString()} -`); -}; - -task( - "counter:show", - "Sends a message from one chain to another.", - main -).addParam("contract", "Contract address"); -``` - -```ts title="hardhat.config.ts" -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 }); - - const receipt = await tx.wait(); - 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} -`); - } -}; - -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") - // remove-next-line - .addParam("from", "address"); -``` - -## Deploy the contract - -Clear the cache and artifacts, then compile the contract: - -``` -npx hardhat compile --force -``` - -``` -npx hardhat deploy --networks sepolia_testnet,mumbai_testnet - -🚀 Successfully deployed contract on mumbai_testnet. -📜 Contract address: 0xbe58130dcD7db27f7b79AE27F91d2D74324c5999 - -🚀 Successfully deployed contract on sepolia_testnet. -📜 Contract address: 0x0e10dF07DCA39Ae5e09bC37897E846b281A68A6C - -🔗 Setting interactors for a contract on mumbai_testnet -✅ Interactor address for 5 (sepolia_testnet) is set to 0x0e10df07dca39ae5e09bc37897e846b281a68a6c - -🔗 Setting interactors for a contract on sepolia_testnet -✅ Interactor address for 80001 (mumbai_testnet) is set to 0xbe58130dcd7db27f7b79ae27f91d2d74324c5999 -``` - -## Increment the counter value - -### Show initial counter value on both chains - -``` -npx hardhat counter:show --network sepolia_testnet --contract 0x0e10dF07DCA39Ae5e09bC37897E846b281A68A6C - -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -🔢 The counter for 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 is: 0 -``` - -``` -npx hardhat counter:show --network mumbai_testnet --contract 0xbe58130dcD7db27f7b79AE27F91d2D74324c5999 - -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -🔢 The counter for 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 is: 0 -``` - -### Increment the counter value - -``` -npx hardhat interact --network sepolia_testnet --contract 0x0e10dF07DCA39Ae5e09bC37897E846b281A68A6C ---amount 0.3 --destination mumbai_testnet - -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -✅ The transaction has been broadcasted to sepolia_testnet -📝 Transaction hash: 0xd0e5adadda20236fd1f50c2e3290e823744015e3227242fb22c78f27b46a63db -``` - -### Show the counter value after increment - -``` -npx hardhat counter:show --network mumbai_testnet --contract 0xbe58130dcD7db27f7b79AE27F91d2D74324c5999 - -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -🔢 The counter for 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 is: 1 -``` - -## Source Code - -You can find the source code for the example in this tutorial here: - -https://github.com/zeta-chain/example-contracts/tree/main/messaging/counter diff --git a/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md b/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md deleted file mode 100644 index a9474fce..00000000 --- a/docs/developers/cross-chain-messaging/examples/cross-chain-nft.md +++ /dev/null @@ -1,377 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Cross-Chain NFT -hide_title: true -id: cross-chain-nft -title: Build a Cross-Chain NFT ---- - -# Build a cross-chain NFT - -In this tutorial you will create an NFT collection with cross-chain transfer -capabilities using -[Zeta Connector](/developers/cross-chain-messaging/connector). - -![Cross-chain NFT transfer](/img/graphs/cross-chain-nft-transfer.svg) - -## Set up your environment - -``` -git clone https://github.com/zeta-chain/template -cd template -yarn -``` - -## Create a new contract - -To create a new cross-chain messaging contract, use the `messaging` command: - -``` -npx hardhat messaging CrossChainWarriors token:uint256 sender:address to:address -``` - -- `token` - NFT ID -- `sender`: address of the sender -- `to`: address of the recipient - -Modify the contract to implement NFT logic: - -```solidity title="contracts/CrossChainWarriors.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"; -// highlight-start -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; -// highlight-end - -contract CrossChainWarriors is - ZetaInteractor, - ZetaReceiver, - // highlight-next-line - ERC721("CrossChainWarriors", "CCWAR") -{ - 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; - - constructor( - address connectorAddress, - address zetaTokenAddress, - address zetaConsumerAddress, - // highlight-next-line - bool useEven - ) ZetaInteractor(connectorAddress) { - _zetaToken = IERC20(zetaTokenAddress); - _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); - - // highlight-start - tokenIds.increment(); - if (useEven) tokenIds.increment(); - // highlight-end - } - - // highlight-start - function mint(address to) public returns (uint256) { - uint256 newWarriorId = tokenIds.current(); - - tokenIds.increment(); - tokenIds.increment(); - - _safeMint(to, newWarriorId); - return newWarriorId; - } - - function _mintId(address to, uint256 tokenId) internal { - _safeMint(to, tokenId); - } - - function _burnWarrior(uint256 burnedWarriorId) internal { - _burn(burnedWarriorId); - } - // highlight-end - - function sendMessage( - uint256 destinationChainId, - uint256 token, - address to - ) external payable { - if (!_isValidChainId(destinationChainId)) - revert InvalidDestinationChainId(); - - uint256 crossChainGas = 2 * (10 ** 18); - uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ - value: msg.value - }(address(this), crossChainGas); - _zetaToken.approve(address(connector), zetaValueAndGas); - - // highlight-next-line - _burnWarrior(token); - - connector.send( - ZetaInterfaces.SendInput({ - destinationChainId: destinationChainId, - destinationAddress: interactorsByChainId[destinationChainId], - destinationGasLimit: 300000, - message: abi.encode( - CROSS_CHAIN_WARRIORS_MESSAGE_TYPE, - token, - // highlight-next-line - msg.sender, - to - ), - zetaValueAndGas: zetaValueAndGas, - zetaParams: abi.encode("") - }) - ); - } - - function onZetaMessage( - ZetaInterfaces.ZetaMessage calldata zetaMessage - ) external override isValidMessageCall(zetaMessage) { - (bytes32 messageType, uint256 token, address sender, address to) = abi - .decode(zetaMessage.message, (bytes32, uint256, address, address)); - - if (messageType != CROSS_CHAIN_WARRIORS_MESSAGE_TYPE) - revert InvalidMessageType(); - - // highlight-next-line - _mintId(to, token); - - emit CrossChainWarriorsEvent(token, sender, to); - } - - function onZetaRevert( - ZetaInterfaces.ZetaRevert calldata zetaRevert - ) external override isValidRevertCall(zetaRevert) { - (bytes32 messageType, uint256 token, address sender, address to) = abi - .decode(zetaRevert.message, (bytes32, uint256, address, address)); - - if (messageType != CROSS_CHAIN_WARRIORS_MESSAGE_TYPE) - revert InvalidMessageType(); - - // highlight-next-line - _mintId(to, token); - - emit CrossChainWarriorsRevertedEvent(token, sender, to); - } -} -``` - -Firstly, import the `ERC721` contract from the OpenZeppelin library. This will -enable the contract to adopt the ERC721 NFT standard. Import the `Counters` -utility from the OpenZeppelin library. The `Counters` utility provides a secure -mechanism to increment or decrement a counter. - -Next, ensure that the contract also inherits from ERC721 and initializes it with -the name "CrossChainWarriors" and the symbol "CCWAR". You can choose your own -name and symbol. - -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 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 - 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. -- Add an internal function `_mintId(address to, uint256 tokenId)`. This function - should be designed to mint a specific ERC721 token with a pre-determined ID to - a specified address. -- Introduce another internal function `_burnWarrior(uint256 burnedWarriorId)`. - This function should facilitate the burning (destruction) of a specific ERC721 - token using its provided ID. - -Having done that, make necessary modifications to existing functions: - -- Amend the `sendMessage(...)` function. As part of its operations, ensure that - the function burns an ERC721 token using the `_burnWarrior(token)` method. - This change ties the act of sending a message to burning an ERC721 token. - Within the `sendMessage` function, you should notice that the address sender - argument has been removed from the function's signature. Also, ensure that the - message encoding captures `msg.sender` as part of its structure. -- Update the `onZetaMessage(...)` function. When a Zeta message is received and - validated, the contract should now mint an ERC721 token to a specified address - using the `_mintId(to, token)` function. -- Similarly, modify the `onZetaRevert(...)` function. On the reception and - validation of a Zeta revert message, the contract should mint an ERC721 token - to a specific address. - -After thsese change the `CrossChainWarriors` contract is now able to mint and -burn non-fungible tokens using the ERC721 standard. - -## Create a Mint Task - -The mint task accepts a contract address as an argument, calls the `mint` -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/main/messaging/warriors/tasks/mint.ts -``` - -```ts title="hardhat.config.ts" -import "./tasks/mint"; -``` - -## Update the Interact Task - -Remove the `sender` argument. In the contract we're using the `msg.sender` -value, instead. - -```ts title="tasks/interact.ts" -// remove-next-line -const paramSender = hre.ethers.utils.getAddress(args.sender); - -const tx = await contract - .connect(signer) - // highlight-next-line - .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") - .addParam("token", "uint256") - // remove-next-line - .addParam("sender", "address") - .addParam("to", "address"); -``` - -## Update the Deploy Task - -Modify the deploy task by adding a new argument `parity` and passing it to the -`deployContract` function. The `parity` argument is used to determine the parity -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(","); - 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, - args.json, - args.gasLimit - ); - }) - // highlight-end - ); - - // ... -}; - -const deployContract = async ( - hre: HardhatRuntimeEnvironment, - networkName: string, - // highlight-next-line - parity: boolean, - json: boolean = false, - gasLimit: number -) => { - //... - const contract = await factory.deploy( - connector, - zetaToken, - zetaTokenConsumerUniV2 || zetaTokenConsumerUniV3, - // highlight-next-line - parity, - { gasLimit } - ); - //... -}; -``` - -## Deploy the Contract - -Clear the cache and artifacts, then compile the contract: - -``` -npx hardhat compile --force -``` - -Run the following command to deploy the contract to two networks: - -``` -npx hardhat deploy --networks sepolia_testnet,mumbai_testnet -``` - -``` -🚀 Successfully deployed contract on mumbai_testnet. -📜 Contract address: 0xe6663Ea61512630438ADC89dB7fD9aE5Ccb28D7B - -🚀 Successfully deployed contract on sepolia_testnet. -📜 Contract address: 0x834313e0C221A5507C3fD62d825FD5182b94c68D - -🔗 Setting interactors for a contract on mumbai_testnet -✅ Interactor address for 511155111 (sepolia_testnet) is set to 0x834313e0c221a5507c3fd62d825fd5182b94c68d - -🔗 Setting interactors for a contract on sepolia_testnet -✅ Interactor address for 80001 (mumbai_testnet) is set to 0xe6663ea61512630438adc89db7fd9ae5ccb28d7b -``` - -## Mint an NFT - -``` -npx hardhat mint --contract 0xe6663Ea61512630438ADC89dB7fD9aE5Ccb28D7B --network mumbai_testnet -``` - -``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -✅ "mint" transaction has been broadcasted to mumbai_testnet -📝 Transaction hash: 0x9b0ed3d360aa7d42ed9e5a366caa9a71b3e85f8ed2041cb8572f6ccd60348cda -🌠 Minted NFT ID: 2 -``` - -## Send the NFT to the Destination Chain - -``` -npx hardhat interact --contract 0xe6663Ea61512630438ADC89dB7fD9aE5Ccb28D7B --network mumbai_testnet --destination sepolia_testnet --token 2 --amount 1.5 --to 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 -``` - -``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -✅ The transaction has been broadcasted to mumbai_testnet -📝 Transaction hash: 0x3814bf4d75009a694435f2a0512be31750491c2d3a4fdee30f695e10392b345f -``` - -After the transfer transaction is confirmed, you will be able to see the NFT on -the recipient address page on the destination chain. - -## Source Code - -You can find the source code for the example in this tutorial here: - -https://github.com/zeta-chain/example-contracts/tree/main/messaging/warriors diff --git a/docs/developers/cross-chain-messaging/examples/erc20.md b/docs/developers/cross-chain-messaging/examples/erc20.md new file mode 100644 index 00000000..0fae2818 --- /dev/null +++ b/docs/developers/cross-chain-messaging/examples/erc20.md @@ -0,0 +1,227 @@ +--- +sidebar_position: 3 +--- + +# Cross-Chain ERC-20 + +In this tutorial you will learn how to create a contract capable of minting +ERC-20 tokens and sending them to a contract on a different chain using +cross-chain messaging. + +## Set up your environment + +``` +git clone https://github.com/zeta-chain/template +cd template +yarn +``` + +## Create the Contract + +To create a new cross-chain messaging contract you will use the `messaging` +Hardhat task available by default in the template. + +``` +npx hardhat messaging CrossChainERC20 to:address value:uint256 +``` + +- `to`: address of the recipient on the destination chain +- `value`: the amount of ERC-20 tokens transferred + +## Cross-Chain Messaging Contract + +Let's review and make changes to the contract: + +```solidity title="contracts/CrossChainERC20.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"; +// highlight-start +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +// highlight-end + +// highlight-next-line +contract CrossChainERC20 is ERC20, ERC20Burnable, ZetaInteractor, ZetaReceiver { + error InsufficientBalance(); + + event CrossChainERC20Event(address, uint256); + event CrossChainERC20RevertedEvent(address, uint256); + + ZetaTokenConsumer private immutable _zetaConsumer; + IERC20 internal immutable _zetaToken; + + constructor( + address connectorAddress, + address zetaTokenAddress, + address zetaConsumerAddress + // highlight-next-line + ) ERC20("CrossChain Token", "CCT") ZetaInteractor(connectorAddress) { + _zetaToken = IERC20(zetaTokenAddress); + _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); + + // highlight-next-line + _mint(msg.sender, 1000000 * 10 ** decimals()); + } + + function sendMessage( + uint256 destinationChainId, + address to, + uint256 value + ) external payable { + if (!_isValidChainId(destinationChainId)) + revert InvalidDestinationChainId(); + + uint256 crossChainGas = 2 * (10 ** 18); + uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ + value: msg.value + }(address(this), crossChainGas); + _zetaToken.approve(address(connector), zetaValueAndGas); + + // highlight-start + if (balanceOf(msg.sender) < value) revert InsufficientBalance(); + _burn(msg.sender, value); + // highlight-end + + connector.send( + ZetaInterfaces.SendInput({ + destinationChainId: destinationChainId, + destinationAddress: interactorsByChainId[destinationChainId], + destinationGasLimit: 300000, + // highlight-next-line + message: abi.encode(to, value, msg.sender), + zetaValueAndGas: zetaValueAndGas, + zetaParams: abi.encode("") + }) + ); + } + + function onZetaMessage( + ZetaInterfaces.ZetaMessage calldata zetaMessage + ) external override isValidMessageCall(zetaMessage) { + (address to, uint256 value) = abi.decode( + zetaMessage.message, + (address, uint256) + ); + + // highlight-next-line + _mint(to, value); + + emit CrossChainERC20Event(to, value); + } + + function onZetaRevert( + ZetaInterfaces.ZetaRevert calldata zetaRevert + ) external override isValidRevertCall(zetaRevert) { + // highlight-start + (address to, uint256 value, address from) = abi.decode( + zetaRevert.message, + (address, uint256, address) + ); + // highlight-end + + // highlight-next-line + _mint(from, value); + + emit CrossChainERC20RevertedEvent(to, value); + } +} +``` + +By default the generated contract is a standard cross-chain messaging contract +with functions to send a message, message handler and revert handler. + +Let's modify the contract to make it ERC-20 compatible. + +To enable ERC-20 token features, import the necessary OpenZeppelin contracts: +`ERC20` for standard token functionality and `ERC20Burnable` to enable burning +tokens. + +In the contract's constructor, initialize the ERC-20 token by setting a name and +a symbol using the `ERC20` constructor. Then, pre-mint an initial supply of +tokens to the deploying address by calling the `_mint` function. + +In the `sendMessage` function, ensure the sender has sufficient tokens to send +by checking their balance. If they have enough, burn the required amount from +their balance using `_burn`. Add `msg.sender` to the message by passing it as a +third argument to `abi.decode`. This is necessary in case the transaction is +reverted, so the sender can be reimbursed. + +To handle the cross-chain message, decode the incoming message in +`onZetaMessage` and mint tokens to the intended recipient using the `_mint` +function. + +In the event of a message revert, decode the original message in `onZetaRevert` +to identify the sender and the amount. Reimburse the sender by minting the +tokens back to their account with `_mint`. Use the `from` address, as this is +the original sender who should receive the reimbursement. + +## Deploy the Contract + +Clear the cache and artifacts, then compile the contract: + +``` +npx hardhat compile --force +``` + +Run the following command to deploy the contract to two networks: + +``` +npx hardhat deploy --networks sepolia_testnet,bsc_testnet +``` + +``` +🚀 Successfully deployed contract on sepolia_testnet +📜 Contract address: 0x18B6f0aB98429F00eDD44D5900090D71e3747e10 + +🚀 Successfully deployed contract on bsc_testnet +📜 Contract address: 0x7eA3054e5086FeE43D745975d171d3a850A94304 + +🔗 Setting interactors for a contract on sepolia_testnet +✅ Interactor address for 97 (bsc_testnet) is set to 0x7ea3054e5086fee43d745975d171d3a850a94304 + +🔗 Setting interactors for a contract on bsc_testnet +✅ Interactor address for 11155111 (sepolia_testnet) is set to 0x18b6f0ab98429f00edd44d5900090d71e3747e10 +``` + +## Send an ERC-20 Token + +Use the interact task to call the `sendMessage` method of the contract and send +ERC-20 tokens from Sepolia to BSC Testnet + +``` +npx hardhat interact --to 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 --value 1000000000 --contract 0x18B6f0aB98429F00eDD44D5900090D71e3747e10 --network sepolia_testnet --amount 0.01 --destination bsc_testnet +``` + +``` +🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 + +✅ The transaction has been broadcasted to sepolia_testnet +📝 Transaction hash: 0x25cfda972e255480e15b47472ae6c8d8842201855776fedaa446f75cab7a0e86 +``` + +You can check the broadcasted transaction on Sepolia's Etherscan: + +https://sepolia.etherscan.io/tx/0x25cfda972e255480e15b47472ae6c8d8842201855776fedaa446f75cab7a0e86 + +Next, you can track the progress of the cross-chain transaction: + +``` +npx hardhat cctx 0x25cfda972e255480e15b47472ae6c8d8842201855776fedaa446f75cab7a0e86 +``` + +``` +✓ CCTXs on ZetaChain found. + +✓ 0x258d3ad9adf92ac5b7d59354435adfadac0af6430737f388ebf2664c22c71158: 11155111 → 97: 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/messaging/erc20 diff --git a/docs/developers/cross-chain-messaging/examples/message.md b/docs/developers/cross-chain-messaging/examples/message.md new file mode 100644 index 00000000..36532cab --- /dev/null +++ b/docs/developers/cross-chain-messaging/examples/message.md @@ -0,0 +1,238 @@ +--- +sidebar_position: 2 +--- + +# Cross-Chain Message + +In this tutorial you will learn how to create a contract capable of sending a +message with arbitrary data between contracts on connected chains using +cross-chain messaging. + +
+ +
+ +## Set up your environment + +``` +git clone https://github.com/zeta-chain/template +cd template +yarn +``` + +## Create the Contract + +To create a new cross-chain messaging contract you will use the `messaging` +Hardhat task available by default in the template. + +``` +npx hardhat messaging CrossChainMessage message:string +``` + +The `messaging` task accepts one or more arguments: the name of the contract and +a list of arguments (optionally with types). The arguments define the contents +of the message that will be sent across chains. + +In the example above the message will have only one field: `message` of type +`string`. If the type is not specified it is assumed to be `string`. + +The optional `--fees` flag is missing, which means this contract will accept +native gas tokens as input and swap it for ZETA. + +## Cross-Chain Messaging Contract + +Let's review the contract: + +```solidity title="contracts/CrossChainMessage.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"; + +contract CrossChainMessage is ZetaInteractor, ZetaReceiver { + + event CrossChainMessageEvent(string); + event CrossChainMessageRevertedEvent(string); + + ZetaTokenConsumer private immutable _zetaConsumer; + IERC20 internal immutable _zetaToken; + + constructor(address connectorAddress, address zetaTokenAddress, address zetaConsumerAddress) ZetaInteractor(connectorAddress) { + _zetaToken = IERC20(zetaTokenAddress); + _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); + } + + function sendMessage(uint256 destinationChainId, string memory message) external payable { + if (!_isValidChainId(destinationChainId)) + revert InvalidDestinationChainId(); + + uint256 crossChainGas = 2 * (10 ** 18); + uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ + value: msg.value + }(address(this), crossChainGas); + _zetaToken.approve(address(connector), zetaValueAndGas); + + connector.send( + ZetaInterfaces.SendInput({ + destinationChainId: destinationChainId, + destinationAddress: interactorsByChainId[destinationChainId], + destinationGasLimit: 300000, + message: abi.encode(message), + zetaValueAndGas: zetaValueAndGas, + zetaParams: abi.encode("") + }) + ); + } + + + function onZetaMessage( + ZetaInterfaces.ZetaMessage calldata zetaMessage + ) external override isValidMessageCall(zetaMessage) { + (string memory message ) = abi.decode( + zetaMessage.message, (string) + ); + + emit CrossChainMessageEvent(message); + } + + function onZetaRevert( + ZetaInterfaces.ZetaRevert calldata zetaRevert + ) external override isValidRevertCall(zetaRevert) { + (string memory message) = abi.decode( + zetaRevert.message, + (string) + ); + + emit CrossChainMessageRevertedEvent(message); + } + +} +``` + +The contract: + +- inherits from + [`ZetaInteractor`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/evm/tools/ZetaInteractor.sol), + which provides two modifiers that are used to validate the message and revert + calls: `isValidMessageCall` and `isValidRevertCall`. +- implements + [`ZetaReceiver`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/evm/interfaces/ZetaInterfaces.sol), + which defines two functions: `onZetaMessage` and `onZetaRevert`. + +State Variables: + +- `_zetaConsumer`: a private immutable state variable that stores the address of + `ZetaTokenConsumer`, which is used amond other things for getting ZETA tokens + from native tokens to pay for gas when sending a message. +- `_zetaToken`: an internal immutable state variable that stores the address of + the ZETA token contract. + +The contract defines two events: `CrossChainMessageEvent` emitted when a message +is processed on the destination chain and `CrossChainMessageRevertedEvent` +emitted when a message is reverted on the destination chain. + +The constructor passes `connectorAddress` to the `ZetaInteractor` constructor +and initializes both `_zetaToken` and `_zetaConsumer` state variables. + +The `sendMessage` function is used to send a message to a recipient contract on +the destination chain. It first checks that the destination chain ID is valid. +Then it uses ZETA consumer to get the needed amount of ZETA tokens from the +provided `msg.value` (amount of native gas assets sent with the function call), +and approves the `ZetaConnector` to spend the `zetaValueAndGas` amount of ZETA +tokens. + +The `sendMessage` function uses `connector.send` to send a crosss-chain message +with the following arguments wrapped in a struct: + +- `destinationChainId`: chain ID of the destination chain +- `destinationAddress`: address of the contract receiving the message on the + destination chain (expressed in bytes since it can be non-EVM) +- `destinationGasLimit`: gas limit for the destination chain's transaction +- `message`: arbitrary message to be parsed by the receiving contract on the + destination chain +- `zetaValueAndGas`: amount of ZETA tokens to be sent to the destination chain, + ZetaChain gas fees, and destination chain gas fees (expressed in ZETA tokens) +- `zetaParams`: optional ZetaChain parameters. + +The `onZetaMessage` function processes incoming cross-chain messages. The +function uses `abi.decode` to decode the contents of the `message`. After +decoding the functions emits `CrossChainMessageEvent` event with the message +content. + +The `onZetaRevert` function handles the reverts of cross-chain messages. This +function is triggered on the source chain if the message passing failed. + +Both `onZetaMessage` and `onZetaRevert` use the `isValidRevertCall` modifier to +ensure that the revert message is genuine and originates from the trusted +source. + +## Deploy the Contract + +Clear the cache and artifacts, then compile the contract: + +``` +npx hardhat compile --force +``` + +Run the following command to deploy the contract to two networks: + +``` +npx hardhat deploy --networks sepolia_testnet,bsc_testnet +``` + +``` +🚀 Successfully deployed contract on bsc_testnet +📜 Contract address: 0x4036009aa206a5c4d3bDABaC7242b18ACc5655D5 + +🚀 Successfully deployed contract on sepolia_testnet +📜 Contract address: 0x5d5c88B669337686af75f97C817365164786C88a + +🔗 Setting interactors for a contract on bsc_testnet +✅ Interactor address for 11155111 (sepolia_testnet) is set to 0x5d5c88b669337686af75f97c817365164786c88a + +🔗 Setting interactors for a contract on sepolia_testnet +✅ Interactor address for 97 (bsc_testnet) is set to 0x4036009aa206a5c4d3bdabac7242b18acc5655d5 +``` + +## Send a Message + +Send a message from Sepolia to BSC testnet using the contract address (see the +output of the `deploy` task). Make sure to submit enough native gas tokens with +`--amount` to pay for the transaction fees. + +``` +npx hardhat interact --message hello --contract 0x5d5c88B669337686af75f97C817365164786C88a --network sepolia_testnet --amount 0.01 --destination bsc_testnet + +``` + +``` +🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 + +✅ The transaction has been broadcasted to sepolia_testnet +📝 Transaction hash: 0x798f15cc214e8d5d595ba1944099134330439a4a30b76cc791e1ca60cd85d696 +``` + +You can check the broadcasted transaction on Sepolia's Etherscan: + +https://sepolia.etherscan.io/tx/0x798f15cc214e8d5d595ba1944099134330439a4a30b76cc791e1ca60cd85d696 + +Next, you can track the progress of the cross-chain transaction: + +``` +npx hardhat cctx 0x798f15cc214e8d5d595ba1944099134330439a4a30b76cc791e1ca60cd85d696 +``` + +``` +✓ CCTXs on ZetaChain found. + +✓ 0x9f3dfff6b1373a6717ce6a0e20d3e6e1591cf13e75634f385550d3e1a226c604: 11155111 → 97: 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/messaging/message diff --git a/docs/developers/cross-chain-messaging/examples/multi-chain-value.md b/docs/developers/cross-chain-messaging/examples/multi-chain-value.md deleted file mode 100644 index 350be455..00000000 --- a/docs/developers/cross-chain-messaging/examples/multi-chain-value.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -sidebar_position: 2 -sidebar_label: Value Transfer -hide_title: true -id: multichain-value-transfer -title: Multichain Value Transfer ---- - -# Multichain Value Transfer - -In this tutorial you will learn how to send ZETA tokens between connected -blockchains using ZetaChain. - -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 -``` - -## Create the Contract - -To create a new cross-chain messaging contract you will use the `messaging` -Hardhat task available by default in the template. - -``` -npx hardhat messaging Value --fees zetaRevert -``` - -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 -} -``` - -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. - -You can also remove the message type from the `connector.send` call. - -## Deploy the Contract - -Clear the cache and artifacts, then compile the contract: - -``` -npx hardhat compile --force -``` - -Run the following command to deploy the contract to two networks: - -``` -npx hardhat deploy --networks sepolia_testnet,mumbai_testnet -``` - -## Send a message - -Run the following command to send ZETA tokens from Sepolia 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 interact --contract 0xe6DE62328677C80084b07eF25637EC83A53d69E1 --network sepolia_testnet --destination mumbai_testnet --amount 3 - -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -✅ The transaction has been broadcasted to sepolia_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/tree/main/messaging/value diff --git a/docs/developers/cross-chain-messaging/examples/nft.md b/docs/developers/cross-chain-messaging/examples/nft.md new file mode 100644 index 00000000..d1b39bf1 --- /dev/null +++ b/docs/developers/cross-chain-messaging/examples/nft.md @@ -0,0 +1,341 @@ +--- +sidebar_position: 4 +--- + +# Cross-Chain NFT + +In this tutorial you will learn how to create a contract capable of minting NFTs +and sending them to a contract on a different chain using cross-chain messaging. + +## Set up your environment + +``` +git clone https://github.com/zeta-chain/template +cd template +yarn +``` + +## Create the Contract + +To create a new cross-chain messaging contract you will use the `messaging` +Hardhat task available by default in the template. + +``` +npx hardhat messaging CrossChainNFT to:address token:uint256 +``` + +- `to`: address of the recipient on the destination chain +- `token` - an ID of the NFT being transferred + +Modify the contract to implement NFT logic: + +```solidity title="contracts/CrossChainNFT.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"; +// highlight-next-line +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract CrossChainNFT is + // highlight-next-line + ERC721("CrossChainNFT", "CCNFT"), + ZetaInteractor, + ZetaReceiver +{ + event CrossChainNFTEvent(address, uint256); + event CrossChainNFTRevertedEvent(address, uint256); + + ZetaTokenConsumer private immutable _zetaConsumer; + IERC20 internal immutable _zetaToken; + // highlight-next-line + uint256 private _tokenIds; + + constructor( + address connectorAddress, + address zetaTokenAddress, + address zetaConsumerAddress, + // highlight-next-line + bool useEven + ) ZetaInteractor(connectorAddress) { + _zetaToken = IERC20(zetaTokenAddress); + _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); + + // highlight-start + _tokenIds++; + if (useEven) _tokenIds++; + // highlight-end + } + + function sendMessage( + uint256 destinationChainId, + address to, + uint256 token + ) external payable { + if (!_isValidChainId(destinationChainId)) + revert InvalidDestinationChainId(); + + uint256 crossChainGas = 2 * (10 ** 18); + uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ + value: msg.value + }(address(this), crossChainGas); + _zetaToken.approve(address(connector), zetaValueAndGas); + + // highlight-next-line + _burn(token); + + connector.send( + ZetaInterfaces.SendInput({ + destinationChainId: destinationChainId, + destinationAddress: interactorsByChainId[destinationChainId], + destinationGasLimit: 300000, + // highlight-next-line + message: abi.encode(to, token, msg.sender), + zetaValueAndGas: zetaValueAndGas, + zetaParams: abi.encode("") + }) + ); + } + + function onZetaMessage( + ZetaInterfaces.ZetaMessage calldata zetaMessage + ) external override isValidMessageCall(zetaMessage) { + (address to, uint256 token) = abi.decode( + zetaMessage.message, + (address, uint256) + ); + + // highlight-next-line + _safeMint(to, token); + + emit CrossChainNFTEvent(to, token); + } + + function onZetaRevert( + ZetaInterfaces.ZetaRevert calldata zetaRevert + ) external override isValidRevertCall(zetaRevert) { + // highlight-start + (address to, uint256 token, address from) = abi.decode( + zetaRevert.message, + (address, uint256, address) + ); + // highlight-end + + // highlight-next-line + _safeMint(from, token); + + emit CrossChainNFTRevertedEvent(to, token); + } + + // highlight-start + function mint(address to) public returns (uint256) { + _tokenIds++; + _tokenIds++; + + _safeMint(to, _tokenIds); + return _tokenIds; + } + // highlight-end +} +``` + +To integrate NFT functionality, import the `ERC721` contract from OpenZeppelin. + +Initialize the ERC-721 token by setting a name and a symbol using the `ERC721` +constructor. + +Initialize a `_tokenIds` counter for managing NFT IDs. + +Add a `bool useEven` parameter to the constructor. The contract will be deployed +on two chains (in this example) and you need to ensure that IDs of tokens minted +on different chains do not clash. Using this constructor parameter on one chain +IDs will be odd, on the other chain IDs will be even. Inside the constructor +increment the initial token IDs. + +In the sendMessage function, burn the specified NFT by calling `_burn`. Add +`msg.sender` to the message by passing it as a third argument to `abi.decode`. +This is necessary in case the transaction is reverted, so the same NFT can be +minted and transferred back to the original sender. + +In the `onZetaMessage` function, decode the incoming message to retrieve the +recipient address and NFT ID. Mint the specified NFT to the recipient using +`_safeMint`. + +In the `onZetaRevert` function, decode the revert message to obtain the +recipient, NFT ID, and the original sender. Reimburse the original sender by +minting the NFT back to their account using `_safeMint`. + +## Create a Mint Task + +The mint task accepts a contract address as an argument, calls the `mint` +function on it, searches the events for a "Transfer" event and prints out the +token ID. + +```ts title="tasks/mint.ts" +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +const contractName = "CrossChainNFT"; + +const main = async (args: any, hre: HardhatRuntimeEnvironment) => { + const [signer] = await hre.ethers.getSigners(); + + const factory = await hre.ethers.getContractFactory(contractName); + const contract = factory.attach(args.contract); + + const tx = await contract.connect(signer).mint(signer.address); + + const receipt = await tx.wait(); + const event = receipt.events?.find((event) => event.event === "Transfer"); + const nftId = event?.args?.tokenId.toString(); + + if (args.json) { + console.log(nftId); + } else { + console.log(`🔑 Using account: ${signer.address}\n`); + console.log(`✅ "mint" transaction has been broadcasted to ${hre.network.name} +📝 Transaction hash: ${receipt.transactionHash} +🌠 Minted NFT ID: ${nftId} +`); + } +}; + +task("mint", "Mint a new NFT.", main) + .addParam("contract", "Contract address") + .addFlag("json", "Output JSON"); +``` + +Import the mint task in the Hardhat config file: + +```ts title="hardhat.config.ts" +import "./tasks/mint"; +``` + +## Update the Deploy Task + +Modify the deploy task by adding a new argument `parity` and passing it to the +`deployContract` function. The `parity` argument is used to determine the parity +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(","); + 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 as ParamChainName, + parity, + args.json, + args.gasLimit + ); + }) + // highlight-end + ); + + // ... +}; + +const deployContract = async ( + hre: HardhatRuntimeEnvironment, + networkName: string, + // highlight-next-line + parity: boolean, + json: boolean = false, + gasLimit: number +) => { + //... + const contract = await factory.deploy( + connector, + zetaToken, + zetaTokenConsumerUniV2 || zetaTokenConsumerUniV3, + // highlight-next-line + parity, + { gasLimit } + ); + //... +}; +``` + +## Deploy the Contract + +Clear the cache and artifacts, then compile the contract: + +``` +npx hardhat compile --force +``` + +Run the following command to deploy the contract to two networks: + +``` +npx hardhat deploy --networks sepolia_testnet,bsc_testnet +``` + +``` +🚀 Successfully deployed contract on bsc_testnet +📜 Contract address: 0x345b7a0ecd2faecF980db7fC1E645b960450E8E4 + +🚀 Successfully deployed contract on sepolia_testnet +📜 Contract address: 0x8A0061fFb4572e4D57D260C3c2a99DD25e8Ab66C + +🔗 Setting interactors for a contract on bsc_testnet +✅ Interactor address for 11155111 (sepolia_testnet) is set to 0x8a0061ffb4572e4d57d260c3c2a99dd25e8ab66c + +🔗 Setting interactors for a contract on sepolia_testnet +✅ Interactor address for 97 (bsc_testnet) is set to 0x345b7a0ecd2faecf980db7fc1e645b960450e8e4 +``` + +## Mint an NFT + +``` +npx hardhat mint --contract 0x8A0061fFb4572e4D57D260C3c2a99DD25e8Ab66C --network sepolia_testnet +``` + +``` +🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 + +✅ "mint" transaction has been broadcasted to sepolia_testnet +📝 Transaction hash: 0xd29edcb4a785fb237965cee63ad415a4a3d091eb022241583ddc30eda49b1d0c +🌠 Minted NFT ID: 4 +``` + +## Send the NFT to the Destination Chain + +``` +npx hardhat interact --to 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 --token 4 --contract 0x8A0061fFb4572e4D57D260C3c2a99DD25e8Ab66C --network sepolia_testnet --amount 0.01 --destination bsc_testnet +``` + +``` +🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 + +✅ The transaction has been broadcasted to sepolia_testnet +📝 Transaction hash: 0x132d6312be65590a66eae59df835528b74b6a46ded59b67712c0646280ceac33 +``` + +You can check the broadcasted transaction on Sepolia's Etherscan: + +https://sepolia.etherscan.io/tx/0x132d6312be65590a66eae59df835528b74b6a46ded59b67712c0646280ceac33 + +Next, you can track the progress of the cross-chain transaction: + +``` +npx hardhat cctx 0x132d6312be65590a66eae59df835528b74b6a46ded59b67712c0646280ceac33 +``` + +``` +✓ CCTXs on ZetaChain found. + +✓ 0xb1ee24fb046397a8fd8444ad2e1ab4762b8d98e091e88bfa6dc7bf1a3a93c70f: 11155111 → 97: 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/messaging/nft diff --git a/docs/developers/cross-chain-messaging/examples/your-first-message.md b/docs/developers/cross-chain-messaging/examples/your-first-message.md deleted file mode 100644 index a9ac88c9..00000000 --- a/docs/developers/cross-chain-messaging/examples/your-first-message.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -sidebar_position: 1 -sidebar_label: Your First Cross-Chain Message -hide_title: true -id: hello-world -title: Your First Cross-Chain Message ---- - -# First Cross-Chain Message - -In this tutorial we will create a simple contract that allows sending a message -from one chain to another using the -[Connector API](/developers/cross-chain-messaging/connector/). - -
- -
- -## Prerequisites - -- [Node.js](https://nodejs.org/en/) (version 18 or above) -- [Yarn](https://yarnpkg.com/) -- [Git](https://git-scm.com/) - -## Set up your environment - -``` -git clone https://github.com/zeta-chain/template -cd template -yarn -``` - -## Create the Contract - -To create a new cross-chain messaging contract you will use the `messaging` -Hardhat task available by default in the template. - -``` -npx hardhat messaging CrossChainMessage message:string -``` - -The `messaging` task accepts one or more arguments: the name of the contract and -a list of arguments (optionally with types). The arguments define the contents -of the message that will be sent across chains. - -In the example above the message will have only one field: `message` of type -`string`. If the type is not specified it is assumed to be `string`. - -The `messaging` task has created: - -- `contracts/CrossChainMessage.sol`: a Solidity cross-chain messaging contract -- `tasks/deploy.ts`: a Hardhat task to deploy the contract on one or more chains -- `tasks/interact.ts`: a Hardhat task to interact with the contract - -It also modified `hardhat.config.ts` to import both `deploy` and `interact` -tasks. - -## Cross-Chain Messaging Contract - -Let's review the contents of the `CrossChainMessage` contract: - -```solidity title="contracts/CrossChainMessage.sol" reference -https://github.com/zeta-chain/example-contracts/blob/main/messaging/message/contracts/CrossChainMessage.sol -``` - -The contract: - -- inherits from - [`ZetaInteractor`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/evm/tools/ZetaInteractor.sol), - which provides two modifiers that are used to validate the message and revert - calls: `isValidMessageCall` and `isValidRevertCall`. -- implements - [`ZetaReceiver`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/evm/interfaces/ZetaInterfaces.sol), - which defines two functions: `onZetaMessage` and `onZetaRevert`. - -State Variables: - -- `CROSS_CHAIN_MESSAGE_MESSAGE_TYPE`: a public constant state variable which - defines the message type. If your contract supports more than one message - type, it's useful to define a constant for each one. -- `_zetaConsumer`: a private immutable state variable that stores the address of - `ZetaTokenConsumer`, which is used amond other things for getting ZETA tokens - from native tokens to pay for gas when sending a message. -- `_zetaToken`: an internal immutable state variable that stores the address of - the ZETA token contract. - -The contract defines two events: `CrossChainMessageEvent` emitted when a message -is processed on the destination chain and `CrossChainMessageRevertedEvent` -emitted when a message is reverted on the destination chain. - -The constructor passes `connectorAddress` to the `ZetaInteractor` constructor -and initializes both `_zetaToken` and `_zetaConsumer` state variables. - -The `sendMessage` function is used to send a message to a recipient contract on -the destination chain. It first checks that the destination chain ID is valid. -Then it uses ZETA consumer to get the needed amount of ZETA tokens from the -provided `msg.value` (amount of native gas assets sent with the function call), -and approves the `ZetaConnector` to spend the `zetaValueAndGas` amount of ZETA -tokens. - -The `sendMessage` function uses `connector.send` to send a crosss-chain message -with the following arguments wrapped in a struct: - -- `destinationChainId`: chain ID of the destination chain -- `destinationAddress`: address of the contract receiving the message on the - destination chain (expressed in bytes since it can be non-EVM) -- `destinationGasLimit`: gas limit for the destination chain's transaction -- `message`: arbitrary message to be parsed by the receiving contract on the - destination chain -- `zetaValueAndGas`: amount of ZETA tokens to be sent to the destination chain, - ZetaChain gas fees, and destination chain gas fees (expressed in ZETA tokens) -- `zetaParams`: optional ZetaChain parameters. - -The `onZetaMessage` function processes incoming cross-chain messages. The -function decodes the message to identify its type and content. If the message -type matches a predefined constant, the message's reception is logged through -the `CrossChainMessageEvent`. However, if the type is unrecognized, the function -reverts to ensure that only specific message types are handled. The function -also uses a `isValidMessageCall` modifier to verify the message's authenticity, -ensuring it comes from a trusted source. - -The `onZetaRevert` function handles the reversal of cross-chain messages. Taking -in a `ZetaInterfaces.ZetaRevert` parameter, the function decodes this reverted -message to identify its type and content. If the message type aligns with a -predefined constant, the function logs the reversal through the -`CrossChainMessageRevertedEvent`. On the other hand, if the type is not -recognized, the function reverts the transaction. The function also uses the -`isValidRevertCall` modifier to ensure that the revert message is genuine and -originates from the trusted source. - -## Deploy Task - -The `messaging` task has created a Hardhat task to deploy the contract. - -```ts title="tasks/deploy.ts" reference -https://github.com/zeta-chain/example-contracts/blob/main/messaging/message/tasks/deploy.ts -``` - -To establish cross-chain messaging between blockchains via ZetaChain, you need -to deploy contracts capable of sending and receiving cross-chain messages to two -or more blockchains connected to ZetaChain. - -You can specify the desired chains by using a `--networks` parameter of the -`deploy` task, which accepts a list of network names separated by commas. For -instance, `--networks sepolia_testnet,bsc_testnet`. - -The `main` function maintains a mapping of network names to their corresponding -deployed contract addresses, iterating over the networks to deploy the contract -on each one. - -The contract's constructor requires three arguments: the connector contract's -address, the ZETA token's address, and the ZETA token consumer contract's -address. These addresses are obtained using ZetaChain's `getAddress`. - -The `main` function subsequently sets interactors for each contract. An -interactor is a mapping between a chain ID of the destination and the contract -address on that chain. - -When deploying to two chains (like Sepolia and BSC testnet), you will invoke -`setInteractorByChainId` on a Sepolia contract and pass the BSC testnet chain ID -(97) and the BSC testnet contract address. You then perform the same operation -on a BSC testnet contract, passing the Sepolia chain ID (11155111) and the -Sepolia contract address. If deploying to more than two chains, you must call -`setInteractorByChainId` for each link between the chains. - -## Interact Task - -The `messaging` task has also created a Hardhat task to interact with the -contract: - -```ts title="tasks/interact.ts" -https://github.com/zeta-chain/example-contracts/blob/main/messaging/message/tasks/interact.ts -``` - -The task accepts the following arguments: - -- `contract`: address of the contract on the source chain -- `amount`: amount of native tokens to send with the transaction -- `destination`: name of the destination chain -- `message`: message to be sent to the destination chain - -The `main` function uses the `contract` argument to attach to the contract on -the source chain. It then uses the `destination` argument to obtain the -destination chain's chain ID. The function subsequently converts the `message` -argument to bytes and sends a transaction to the contract's `sendMessage` -function, passing the destination chain ID and the message. - -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. - -Create a new wallet account: - -``` -npx hardhat account --save -``` - -This command generates a random wallet, prints information about the wallet to -the terminal, and saves the private key to a `.env` file to make it accessible -to Hardhat. - -## Use the Faucet to Request Tokens - -To pay for the transaction fees to deploy and interact with the cross-chain -messaging contracts you will need native gas tokens on the connected chains you -are deploying contracts to. You can find a list of recommended faucets -[in the docs](/reference/get-testnet-zeta/). - -## Check Token Balances - -``` -npx hardhat balances -``` - -## Deploy the Contract - -Clear the cache and artifacts, then compile the contract: - -``` -npx hardhat compile --force -``` - -Run the following command to deploy the contract to two networks: - -``` -npx hardhat deploy --networks bsc_testnet,sepolia_testnet -``` - -``` -🚀 Successfully deployed contract on bsc_testnet -📜 Contract address: 0x6Fd784c16219026Ab0349A1a8A6e99B6eE579C93 - -🚀 Successfully deployed contract on sepolia_testnet. -📜 Contract address: 0xf1907bb130feb28D6e1305C53A4bfdb32140d8E6 - -🔗 Setting interactors for a contract on bsc_testnet -✅ Interactor address for 11155111 (sepolia_testnet) is set to 0xf1907bb130feb28d6e1305c53a4bfdb32140d8e6 - -🔗 Setting interactors for a contract on sepolia_testnet -✅ Interactor address for 97 (bsc_testnet) is set to 0x6fd784c16219026ab0349a1a8a6e99b6ee579c93 -``` - -## Interact with the Contract - -Send a message from BSC testnet to Sepolia 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. - -``` -npx hardhat interact --message hello --network bsc_testnet --destination sepolia_testnet --contract 0x6Fd784c16219026Ab0349A1a8A6e99B6eE579C93 --amount 2 -``` - -``` -🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 - -✅ "sendHelloWorld" transaction has been broadcasted to bsc_testnet -📝 Transaction hash: 0xa3a507d34056f4c00b753e7d0b47b16eb6d35b3c5016efa0323beb274725b1a1 -``` - -After the cross-chain transaction is processed on ZetaChain, look up the -contract on the Sepolia explorer by the contract address -(`0xf1907bb130feb28D6e1305C53A4bfdb32140d8E6`) and you should be able to see the -emitted `HelloWorldEvent` event. - -![](/img/docs/ccm-message-explorer.png) - -Congratulations! 🎉 In this tutorial you have: - -- cloned the Hardhat contract template -- used `npx hardhat messaging` to create a new cross-chain messaging contract -- reviewed the contents of the generated contract and the tasks to deploy and - interact with the contract -- successfully deployed the contract to two connected chains and set interactors - on each contract -- sent a message from one chain to another using the `connector.send` - -## Source Code - -You can find the source code for the example in this tutorial here: - -https://github.com/zeta-chain/example-contracts/tree/main/messaging/message diff --git a/docs/developers/cross-chain-messaging/examples/zeta.md b/docs/developers/cross-chain-messaging/examples/zeta.md new file mode 100644 index 00000000..0a3256f7 --- /dev/null +++ b/docs/developers/cross-chain-messaging/examples/zeta.md @@ -0,0 +1,437 @@ +--- +sidebar_position: 1 +--- + +# Cross-Chain ZETA Transfer + +In this tutorial you will learn how to create a contract capable of sending ZETA +tokens between contracts on connected chains using cross-chain messaging. + +- This tutorial uses ZetaChain cross-chain messaging, but in this example we're + only sending ZETA. You will learn how to send arbitrary data in a message in + the [Message](/developers/cross-chain-messaging/examples/message) tutorial. +- Since this contract's purpose is to send ZETA, the contract in this example + will accept ZETA as input. Example contracts in other tutorials will use + native gas assets as input for convenience. +- All the code will be generated for you by the template. + +## Prerequisites + +- [Node.js](https://nodejs.org/en/) (version 18 or above) +- [Yarn](https://yarnpkg.com/) +- [Git](https://git-scm.com/) + +## Set up your environment + +``` +git clone https://github.com/zeta-chain/template +cd template +yarn +``` + +## Create the Contract + +To create a new cross-chain messaging contract you will use the `messaging` +Hardhat task available by default in the template. + +``` +npx hardhat messaging CrossChainZETA --fees zeta +``` + +The `messaging` task accepts one or more arguments: the name of the contract and +a list of arguments (optionally with types). The arguments define the contents +of the message that will be sent across chains. In this example the contract +will only be sending ZETA, so the list of arguments is empty. + +Use the `--fee zeta` to specify that this contract will be accepting ZETA +tokens. You will learn more about this flag later in this tutorial. + +The `messaging` task has created: + +- `contracts/CrossChainMessage.sol`: a Solidity cross-chain messaging contract +- `tasks/deploy.ts`: a Hardhat task to deploy the contract on one or more chains +- `tasks/interact.ts`: a Hardhat task to interact with the contract + +It also modified `hardhat.config.ts` to import both `deploy` and `interact` +tasks. + +Let's review the contract: + +```solidity title="contracts/CrossChainZeta.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"; + +contract CrossChainZeta is ZetaInteractor { + error ErrorTransferringZeta(); + + event CrossChainZetaEvent(); + event CrossChainZetaRevertedEvent(); + + 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, + message: abi.encode(), + zetaValueAndGas: zetaValueAndGas, + zetaParams: abi.encode("") + }) + ); + } +} +``` + +The contract: + +- inherits from + [`ZetaInteractor`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/evm/tools/ZetaInteractor.sol), + which provides the connector functionality and helper functions like + `_isValidChainId`. + +State Variables: + +- `_zetaToken`: an internal immutable state variable that stores the address of + the ZETA token contract. + +The constructor passes `connectorAddress` to the `ZetaInteractor` constructor +and initializes the `_zetaToken` state variables. + +The `sendMessage` function is used to send ZETA tokens to a recipient contract +on the destination chain. It first checks that the destination chain ID is +valid. Then it approves the contract to spend an amount of ZETA + +## Cross-Chain Fees + +[Cross-chain messaging fees](/developers/cross-chain-messaging/gas-fees/) are +paid in ZETA tokens. This is convenient if the caller has ZETA tokens on the +source chain. However, many users might only have native gas tokens. In this +case it's more convenient for a contract to accept native gas token, and swap it +for ZETA. + +To choose which token your contract accepts, use the `--fees` flag. + +To make the contract accept ZETA, use `--fees zeta`. This value was used to +create a contract above. Since the contract's only purpose is to send ZETA, it +makes sense to also accept ZETA as a fee token. + +To make the contract accept native gas tokens, use `--fees native` or skip this +flag completely as this is the default option. + +## Sending ZETA + +The `sendMessage` function uses `connector.send` to send a crosss-chain message +with the following arguments wrapped in a struct: + +- `destinationChainId`: chain ID of the destination chain +- `destinationAddress`: address of the contract receiving the message on the + destination chain (expressed in bytes since it can be non-EVM) +- `destinationGasLimit`: gas limit for the destination chain's transaction +- `message`: arbitrary message to be parsed by the receiving contract on the + destination chain. In this example it's empty as we're only sending ZETA, not + arbitrary data. +- `zetaValueAndGas`: amount of ZETA tokens to be sent to the destination chain, + ZetaChain gas fees, and destination chain gas fees (expressed in ZETA tokens) +- `zetaParams`: optional ZetaChain parameters. Currently, not being used. + +After handling the fees the contract calls `connector.send` to send +`zetaValueAndGas` amount of ZETA to the `destinationAddress` contract on the +`destinationChainId` blockchain. The message is empty as only ZETA tokens are +being transferred. + +## Deploy Task + +The `messaging` task has created a Hardhat task to deploy the contract. + +```ts title="tasks/deploy.ts" +import { getAddress } from "@zetachain/protocol-contracts"; +import { ethers } from "ethers"; +import { task, types } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import type { ParamChainName } from "@zetachain/protocol-contracts"; + +const contractName = "CrossChainZeta"; + +const main = async (args: any, hre: HardhatRuntimeEnvironment) => { + const networks = args.networks.split(","); + const contracts: { [key: string]: string } = {}; + await Promise.all( + networks.map(async (networkName: ParamChainName) => { + contracts[networkName] = await deployContract( + hre, + networkName, + args.json, + args.gasLimit + ); + }) + ); + + for (const source in contracts) { + await setInteractors( + hre, + source as ParamChainName, + contracts, + args.json, + args.gasLimit + ); + } + + if (args.json) { + console.log(JSON.stringify(contracts, null, 2)); + } +}; + +const initWallet = ( + hre: HardhatRuntimeEnvironment, + networkName: ParamChainName +) => { + const { url } = hre.config.networks[networkName] as any; + const provider = new ethers.providers.JsonRpcProvider(url); + const wallet = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); + + return wallet; +}; + +const deployContract = async ( + hre: HardhatRuntimeEnvironment, + networkName: ParamChainName, + json: boolean = false, + gasLimit: number +) => { + const wallet = initWallet(hre, networkName); + + const connector = getAddress("connector", networkName); + const zetaToken = getAddress("zetaToken", networkName); + + const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); + const factory = new ethers.ContractFactory(abi, bytecode, wallet); + const contract = await factory.deploy(connector, zetaToken, { gasLimit }); + + await contract.deployed(); + if (!json) { + console.log(` +🚀 Successfully deployed contract on ${networkName} +📜 Contract address: ${contract.address}`); + } + return contract.address; +}; + +const setInteractors = async ( + hre: HardhatRuntimeEnvironment, + source: ParamChainName, + contracts: { [key: string]: string }, + json: boolean = false, + gasLimit: number +) => { + if (!json) { + console.log(` +🔗 Setting interactors for a contract on ${source}`); + } + const wallet = initWallet(hre, source); + + const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); + const factory = new ethers.ContractFactory(abi, bytecode, wallet); + const contract = factory.attach(contracts[source]); + + for (const counterparty in contracts) { + if (counterparty === source) continue; + + const counterpartyContract = ethers.utils.solidityPack( + ["address"], + [contracts[counterparty]] + ); + const chainId = hre.config.networks[counterparty].chainId; + await ( + await contract.setInteractorByChainId(chainId, counterpartyContract, { + gasLimit, + }) + ).wait(); + if (!json) { + console.log( + `✅ Interactor address for ${chainId} (${counterparty}) is set to ${counterpartyContract}` + ); + } + } +}; + +task("deploy", "Deploy the contract", main) + .addParam("networks", "Comma separated list of networks to deploy to") + .addOptionalParam("gasLimit", "Gas limit", 10000000, types.int) + .addFlag("json", "Output JSON"); +``` + +To establish cross-chain messaging between blockchains via ZetaChain, you need +to deploy contracts capable of sending and receiving cross-chain messages to two +or more blockchains connected to ZetaChain. + +You can specify the desired chains by using a `--networks` parameter of the +`deploy` task, which accepts a list of network names separated by commas. For +instance, `--networks sepolia_testnet,bsc_testnet`. + +The `main` function maintains a mapping of network names to their corresponding +deployed contract addresses, iterating over the networks to deploy the contract +on each one. + +The contract's constructor requires three arguments: the connector contract's +address, the ZETA token's address, and the ZETA token consumer contract's +address. These addresses are obtained using ZetaChain's `getAddress`. + +The `main` function subsequently sets interactors for each contract. An +interactor is a mapping between a chain ID of the destination and the contract +address on that chain. + +When deploying to two chains (like Sepolia and BSC testnet), you will invoke +`setInteractorByChainId` on a Sepolia contract and pass the BSC testnet chain ID +(97) and the BSC testnet contract address. You then perform the same operation +on a BSC testnet contract, passing the Sepolia chain ID (11155111) and the +Sepolia contract address. If deploying to more than two chains, you must call +`setInteractorByChainId` for each link between the chains. + +## Interact Task + +The `messaging` task has also created a Hardhat task to interact with the +contract: + +```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"; + +const main = async (args: any, hre: HardhatRuntimeEnvironment) => { + const [signer] = await hre.ethers.getSigners(); + + const factory = await hre.ethers.getContractFactory("CrossChainZeta"); + 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`); + } + + const value = parseEther(args.amount); + + const zetaTokenAddress = getAddress("zetaToken", hre.network.name as any); + const zetaFactory = await hre.ethers.getContractFactory("ZetaEth"); + const zetaToken = zetaFactory.attach(zetaTokenAddress); + + await (await zetaToken.approve(args.contract, value)).wait(); + + const tx = await contract.connect(signer).sendMessage(destination, value); + + const receipt = await tx.wait(); + 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} +`); + } +}; + +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"); +``` + +The task accepts the following arguments: + +- `contract`: address of the contract on the source chain +- `amount`: amount of native tokens to send with the transaction +- `destination`: name of the destination chain + +The `main` function uses the `contract` argument to attach to the contract on +the source chain. It then uses the `destination` argument to obtain the +destination chain's chain ID. The function subsequently calls the `sendMessage` +contract method passing `destination` and `value`. + +## Deploy the Contract + +Clear the cache and artifacts, then compile the contract: + +``` +npx hardhat compile --force +``` + +Run the following command to deploy the contract to two networks: + +``` +npx hardhat deploy --networks sepolia_testnet,bsc_testnet +``` + +``` +🚀 Successfully deployed contract on bsc_testnet +📜 Contract address: 0x65D661B68ff1466dedf80685450ac4c684b522BB + +🚀 Successfully deployed contract on sepolia_testnet +📜 Contract address: 0xDEB42ce9d2F32caaA38Bf3107a054951E11575DF + +🔗 Setting interactors for a contract on bsc_testnet +✅ Interactor address for 11155111 (sepolia_testnet) is set to 0xdeb42ce9d2f32caaa38bf3107a054951e11575df + +🔗 Setting interactors for a contract on sepolia_testnet +✅ Interactor address for 97 (bsc_testnet) is set to 0x65d661b68ff1466dedf80685450ac4c684b522bb +``` + +## Send ZETA + +Run the following command to send ZETA tokens from Sepolia to BSC testnet. +Please, note that since the contract expect ZETA tokens as fees, the value of +the `--amount` param is denominated 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 interact --contract 0xDEB42ce9d2F32caaA38Bf3107a054951E11575DF --network sepolia_testnet --amount 3 --destination bsc_testnet +``` + +``` +🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32 + +✅ The transaction has been broadcasted to sepolia_testnet +📝 Transaction hash: 0x90db3fa9e6e169eadae84b2dbfac15c6829807029f7198f1fcf515e6fb4d04cb +``` + +You can check the broadcasted transaction on Sepolia's Etherscan: + +https://sepolia.etherscan.io/tx/0x90db3fa9e6e169eadae84b2dbfac15c6829807029f7198f1fcf515e6fb4d04cb + +Next, you can track the progress of the cross-chain transaction: + +``` +npx hardhat cctx 0x90db3fa9e6e169eadae84b2dbfac15c6829807029f7198f1fcf515e6fb4d04cb +``` + +``` +✓ CCTXs on ZetaChain found. + +✓ 0xbf258794df4475fc79fec38f9afe3a92bee82477c3af1c9e61912e6b21408bb0: 11155111 → 97: 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/messaging/zeta diff --git a/docusaurus.config.js b/docusaurus.config.js index ae8c57b1..fcba4124 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -86,6 +86,18 @@ const config = { from: "/developers/omnichain/zrc-20", to: "/developers/tokens/zrc20", }, + { + from: "/developers/cross-chain-messaging/examples/cross-chain-nft", + to: "/developers/cross-chain-messaging/examples/nft", + }, + { + from: "/developers/cross-chain-messaging/examples/hello-world", + to: "/developers/cross-chain-messaging/examples/message", + }, + { + from: "/developers/cross-chain-messaging/examples/multichain-value-transfer", + to: "/developers/cross-chain-messaging/examples/zeta", + }, ], }, ],