January 7, 2023

ZetaChain

ZetaEVM (zEVM)

Zeta EVM (zEVM) is an Ethereum-compatible virtual machine where you can deploy and use Omnichain Smart Contracts, built on top of ZetaChain’s core blockchain. Contracts on zEVM are connected to ZetaChain’s interoperability layer and are able to orchestrate assets on external chains as if they were all on a single chain.

Contracts on zEVM can be called via ZRC-20 from external chains. To connect to zEVM directly, check out the network info here.

ZetaChain contributors will continue to add features and improvements to zEVM to support more robust, secure, and fast omnichain applications.

ZRC-20

ZRC-20 is a token standard integrated into ZetaChain’s omnichain smart contract platform. With ZRC-20, developers can build dApps that orchestrate native assets on any connected chain. This makes building Omnichain DeFi protocols and dApps such as Omnichain DEXs, Omnichain Lending, Omnichain Portfolio Management, and anything else that involves fungible tokens on multiple chains from a single place extremely simple — as if they were all on a single chain.

Introduction

At a high-level, ZRC-20 tokens are an extension of the standard ERC-20 tokens found in the Ethereum ecosystem, ZRC-20 tokens have the added ability to manage assets on all ZetaChain-connected chains. Any fungible token, including Bitcoin, Dogecoin, ERC-20-equivalents on other chains, gas assets on other chains, and so on, may be represented on ZetaChain as a ZRC-20 and orchestrated as if it were any other fungible token (like an ERC-20).

Interface

ZRC-20 is based on ERC-20, with three additional functions and some associated events for integration with Cross-Chain Transactions (CCTXs) in ZetaChain.

ZEVMSwapApp.sol

pragma solidity 0.8.7;interface IZRC20 {    function totalSupply() external view returns (uint256);    function balanceOf(address account) external view returns (uint256);    function transfer(address recipient, uint256 amount) external returns (bool);    function allowance(address owner, address spender) external view returns (uint256);    function approve(address spender, uint256 amount) external returns (bool);    function transferFrom(        address sender,        address recipient,        uint256 amount    ) external returns (bool);    function deposit(address to, uint256 amount) external returns (bool);    function withdraw(bytes memory to, uint256 amount) external returns (bool);    function withdrawGasFee() external view returns (address, uint256);    event Transfer(address indexed from, address indexed to, uint256 value);    event Approval(address indexed owner, address indexed spender, uint256 value);    event Deposit(bytes from, address indexed to, uint256 value);    event Withdrawal(address indexed from, bytes to, uint256 value);}

Copy

Comparing ZRC-20 to ERC-20, there are additional external functions to deposit and withdraw, and additional events for each of them. This makes ZRC-20 completely compatible with any applications built for ERC-20s, but with an extremely simple interface to also function in an omnichain way.

deposit

When a user sends/deposits assets to the ZetaChain TSS address (Testnet, Mainnet) on a connected chain, deposit is called by zetacore and made available to the address that deposited. If there is data on the TX message, the system contract DepositAndCall is called, forwarding that data in a call to onCrossChainCall on the target zEVM contract. The deposit and DepositAndCall functions are only callable by the CCTX module (zetacore module) address.

This is a snippet of what the system contract looks like, where DepositAndCall may be called by zetacore after receiving a deposit into a TSS address (Testnet, Mainnet) managed by the ZetaChain network.

SystemContract

contract SystemContract {    address public constant FUNGIBLE_MODULE_ADDRESS;    // ...    constructor(address fungibleModule) {        FUNGIBLE_MODULE_ADDRESS = fungibleModule;    }    // ...    function DepositAndCall(address zrc20, uint256 amount, address target, bytes calldata message) external {        require(msg.sender == FUNGIBLE_MODULE_ADDRESS);        require(target != FUNGIBLE_MODULE_ADDRESS && target != address(this));        IZRC20(zrc20).deposit(target, amount);        zContract(target).onCrossChainCall(zrc20, amount, message);    }}

Copy

ZEVMSwapApp.sol

// a contract that implements this interface may be called by a ZRC-20 deposit callinterface zContract {    function onCrossChainCall(        address zrc20,        uint256 amount,        bytes calldata message    ) external;}

Copy

How to deposit and call zEVM contracts from a smart contract chain

This is an example calling from an Ethereum chain to send a transaction to the Athens 2 TSS address in order to deposit.

TestDeposit.js

import { parseEther } from "@ethersproject/units";import { ethers } from "hardhat";// This is a constant address, the TSS address of the ZetaChain network.import { TSS_ATHENS2 } from "../systemConstants";// Primary function definitionconst main = async () => {  // Get signer in order to write transction.  const [signer] = await ethers.getSigners();  // Sign a transaction that sends Ether to the TSS address.  const tx = await signer.sendTransaction({    to: TSS_ATHENS2,    value: parseEther("100")  });  // That's it! ZetaChain will pick up the transaction.  console.log("Token sent. tx:", tx.hash);};main().catch(error => {  console.error(error);  process.exit(1);});

Copy

If you instead wanted to do a DepositAndCall, you can do a similar pattern but include data in the deposit call. This example demonstrates calling a swap contract that exists on the zEVM.

TestDepositAndCall.js

import { BigNumber } from "@ethersproject/bignumber";import { parseEther } from "@ethersproject/units";import { getAddress, isNetworkName } from "@zetachain/addresses";import { ethers } from "hardhat";import { ZRC20Addresses, TSS_ATHENS2 } from "../systemConstants";import { network } from "hardhat";// Helper function to format data for sending a swap transactionconst getSwapData = (zetaSwap: string, destination: string, destinationToken: string, minOutput: BigNumber) => {  const params = getSwapParams(destination, destinationToken, minOutput);  return zetaSwap + params.slice(2);};// Primary function definitionconst main = async () => {  if (!isNetworkName(network.name) || !network.name) throw new Error("Invalid network name");  // Here you're choosing the target token you want the swap to output based on the network  const destinationToken = network.name == 'goerli' ? ZRC20Addresses['tMATIC'] : ZRC20Addresses['gETH']  console.log("Swapping native token...");  // Get a signer to write your transaction  const [signer] = await ethers.getSigners();  // Get the correct address of the swap contract (using a helper function)  const zetaSwap = getAddress({    address: "zetaSwap",    networkName: "athens",    zetaNetwork: "athens"  });  // Use formatting function to get the correct data format  const data = getSwapData(zetaSwap, signer.address, destinationToken, BigNumber.from("0"));  // Sign your transaction with Swap data.  const tx = await signer.sendTransaction({    data,    to: TSS_ATHENS2,    value: parseEther("0.5")  });  console.log("tx:", tx.hash);};main().catch(error => {  console.error(error);  process.exit(1);});

Copy

Initial supported assets include ZETA and gas assets on all connected chains including Bitcoin. The protocol will expand to all fungible tokens in a future upgrade.

How to deposit and call zEVM contracts from Bitcoin

In order to test with Bitcoin, you will need to use a wallet that allows setting an OP_RETURN. Please see our wallet suggestions here.

In order to deposit Bitcoin into ZetaChain to use via ZRC-20 and with the rest of the ZetaChain ecosystem, you must send your Bitcoin to an address managed by ZetaChain’s TSS (Testnet, Mainnet). the transaction should include an OP_RETURN formatted as we document below (the | symbols are to make it more readable, you shouldn’t include them in your actual message):

z|0xcc7bb2d219a0fc08033e130629c2b854b7ba9195|00000000000000000000000000000000000000000000000000000000000
| |                                          |
| |                                          â”” An arbitrary message to send to the contract you want to call (59 bytes)
| └─────── An address that can be a contract or an account (20 bytes)
└───── The letter "z" in lowercase (1 byte)
  • The letter “z” is constant, and it’s used by ZetaCore to make sure the OP_RETURN is valid.
  • The address can be a contract if the transaction will execute zEVM code, or an account if you just want to send zBTC to it.
  • The message is arbitrary and will be parsed by the destination contract (in case there’s one).

If invalid information is sent (i. e. invalid address), the assets are returned to the original sender address.

In summary, a zEVM BTC transaction would look like this:

  • A user sends 1 BTC on Bitcoin network to the Bitcoin TSS address (Testnet, Mainnet), adding a memo (via OP_RETURN) in the tx saying (colloquially) “deposit to 0x1337”.
  • Upon receiving this tx, the ZetaCore state machine calls the deposit (0x1337, 1e8) to mint and credit 0x1337 with 1 zBTC minus fees.
  • If 0x1337 is an Externally Owned Account (EOA), that’s it. If it’s a contract, ZetaCore will call the onCrossChainMessage function sending the message that was specified in the OP_RETURN memo.

The TSS address (Testnet, Mainnet) holds the BTC, where ownerships are tracked inside this BTC ZRC-20 contract.

withdraw

The withdraw function can be called by any Externally Owned Account (EOA) or smart contract. This function is like transfer(), except that the amount is burned, and leaves a Withdrawal() event. This event will trigger a CCTX in zetacore module, which the zetaclient will pick up and process the outbound 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.

TestSwap.sol

contract ZEVMSwapApp is zContract {    address public router02;     constructor(address router02_) {        router02 = router02_;    }        // Call this function to perform a cross-chain swap    function onCrossChainCall(address zrc20, uint256 amount, bytes calldata message) external override {        address targetZRC20;        address receipient;        uint256 minAmountOut;         (targetZRC20, receipient,minAmountOut) = abi.decode(message, (address,address,uint256));        address[] memory path;        path = new address[](2);        path[0] = zrc20;        path[1] = targetZRC20;        // Approve the usage of this token by router02        IZRC20(zrc20).approve(address(router02), amount);        // Swap for your target token        uint256[] memory amounts = IUniswapV2Router01(router02).swapExactTokensForTokens(amount, minAmountOut, path, address(this), block.timestamp);        // Withdraw amountto target recipient        IZRC20(targetZRC20).withdraw(abi.encodePacked(receipient), amounts[1]);    }}

Copy

Note how simple this example is. With ~20 lines of code -- much of which is generic code -- one is able to create a cross-chain swap dApp where users can trade native assets for other native assets. withdraw may be used in any situation where a user needs to get assets back onto one's native wallet, while deposit from above allows you to deposit and orchestrate any assets via zEVM smart contract calls. Together, these simple functions unlock powerful yet simple solutions for omnichain application building.

Building on ZRC-20

With ZRC-20, developers have the power to build seamless, omnichain applications while also leveraging the entire EVM ecosystem to-date and plethora of contracts/protocols to build on top of. To start building with ZRC-20, check out some quickstart examples here.

Core ZETA Pools

A [ZETA] / [Gas ZRC-20] Uniswap Pool (on zEVM) is the core pool needed by ZetaChain to write outbound transactions to that chain. Whenever a chain’s support is added, a corresponding pool between ZETA and that chain’s native gas asset is also created.

Here, you can visualize how a UniswapV2 contract controls ZETA / gas pools. Liquidity is provided to TSS addresses on connected chains, and then Uniswap (or any exchange contract) can use those assets (ZRC-20) against ZETA or any other asset.

For example, you can see how transactions function using these core pools that pair native gas (ZRC-20) with ZETA to pay for outbound transactions:

Additional zEVM Pools

Any liquidity pool may be created on zEVM. One can deploy normal ERC-20 tokens onto ZetaChain, incorporate external chain tokens through ZRC-20, and make any permutation of liquidity pool required for their applications, just as one would on a single-chain EVM. For example, one could create useful [ZETA] / [Stablecoin] or [Gas] / [Stablecoin] pools that let users trade more fluidly against different assets.

External ZETA Pools

ZETA is an omnichain token that exists both on ZetaChain as well as on any connected chain, as it is for both smart contract gas fees and cross-chain messaging. Certain pools such as [ZETA] / [Gas] on each chain will be helpful for applications to facilitate cross-chain value transfer through messaging. Developers also need pools on external chains to acquire ZETA, in order to use it for messaging.

Smart Contract Fees

When interacting with smart contracts on ZetaChain, a user includes a portion of value that is spent on gas for that transaction.

Smart contract deployments and smart contract calls require gas to run. A user can call a zEVM contract from an external chain by ZRC-20 deposits, including a contract call in the message, or connecting directly to ZetaChain and interacting with a contract that with a contract already deployed on zEVM.

The gas market/mechanism for ZetaEVM smart contracts is based on that of Ethermint and behaves similarly to EIP 1559 Ethereum gas fees. This gas system is built to deter malicious users from spamming the network.

Base Fee

ZetaChain includes a base flat fee of (example) 0.01 ZETA for any transactions, cross-chain messaging transactions or smart contracts. This base fee is adjustable by the network based on network traffic and congestion, and is burned.

Cross-Chain Messaging Fees

A user (wallet, contract) must pay fees in order to send data and value across chains through ZetaChain. A user pays for fees by sending ZETA (and message data) on a connected chain to a Connector contract. This ZETA is used to pay validators/stakers/ecosystem pools, as well as for paying the gas on the destination. To a user, this is all bundled into a single transaction.

Variable fees based on data size/storage

Network fees have a component based on the size of the message size (bytes) that a user is trying to send across chains. As an example:

This throttles the volume of data that a user would be able to send while being economically sound. Sending very complex data would cost more. This mechanism will transition into a variable fee market with further development, similarly to how Smart Contract Fees work.

Base Fee

ZetaChain includes a base flat fee of (example) 0.01 ZETA for any transactions, cross-chain messaging transactions or smart contracts. This base fee is adjustable by the network as needed to deal with network traffic and congestion, and is burned.

Twitter - https://twitter.com/zetablockchain

Discord - https://discord.com/invite/kjQBqcZtnh

Telegram - https://t.me/zetachainofficial

Reddit - https://www.reddit.com/r/zetablockchain/