diff --git a/public/img/docs/tutorials-nft.png b/public/img/docs/tutorials-nft.png
new file mode 100644
index 000000000..6a445ae25
Binary files /dev/null and b/public/img/docs/tutorials-nft.png differ
diff --git a/src/pages/developers/_meta.json b/src/pages/developers/_meta.json
index 1e0b30941..e8e3e3649 100644
--- a/src/pages/developers/_meta.json
+++ b/src/pages/developers/_meta.json
@@ -4,7 +4,7 @@
"description": "Begin your journey on ZetaChain, the decentralized blockchain and smart contract platform designed for omnichain interoperability."
},
"apps": {
- "title": "Building Universal Apps",
+ "title": "Universal Apps",
"description": "Explore the basics of developing on ZetaChain."
},
"evm": {
@@ -12,7 +12,7 @@
"description": "EVM enhanced with omnichain interoperability features, enabling the development of robust universal apps."
},
"chains": {
- "title": "Connected Blockchains",
+ "title": "Connected Chains",
"description": "Use Gateway to make calls to and from universal apps, deposit and withdraw tokens."
},
"tutorials": {
diff --git a/src/pages/developers/apps/_meta.json b/src/pages/developers/apps/_meta.json
index fd9e3c5c0..cdbc1fb5a 100644
--- a/src/pages/developers/apps/_meta.json
+++ b/src/pages/developers/apps/_meta.json
@@ -1,6 +1,6 @@
{
"intro": {
- "title": "Universal App Basics",
+ "title": "Basics",
"description": "Build universal apps that can be called from any blockchain"
},
"advantages": {
diff --git a/src/pages/developers/apps/intro.mdx b/src/pages/developers/apps/intro.mdx
index 6e79afefb..02debd17e 100644
--- a/src/pages/developers/apps/intro.mdx
+++ b/src/pages/developers/apps/intro.mdx
@@ -50,12 +50,6 @@ connected chain. Each connected chain has a single Gateway contract that exposes
methods to deposit tokens to and call universal apps. Users can pass both data
and tokens when calling universal apps.
-
- {" "}
- Gateway is an upcoming unified interface that will replace the TSS address and ERC-20 custody contract. Gateway is scheduled
- for release towards the end of Q3 2024.{" "}
-
-
In this example an Ethereum user sends 1 ETH and a message "hello" to a
universal app:
@@ -63,7 +57,7 @@ universal app:
style={{ border: "1px solid rgba(0,0,0,.1)", marginTop: "2rem", borderRadius: "0.5rem" }}
width="100%"
height="450"
- src="https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Fboard%2FgPX2Rjuxb44SYr1FTwZCXv%2FIntro-1%3Fnode-id%3D0-1%26t%3DowucPIvKfVlZrHJR-1"
+ src="https://embed.figma.com/board/Tz0eeQMlUDAigpSdWYPi0F/Intro-1?node-id=0-1&embed-host=share"
allowfullscreen
>
@@ -102,7 +96,7 @@ different chains.
style={{ border: "1px solid rgba(0,0,0,.1)", marginTop: "2rem", borderRadius: "0.5rem" }}
width="100%"
height="450"
- src="https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Fboard%2FKtVyu73smn4q9P4Gq5Bczn%2FIntro-2%3Fnode-id%3D0-1%26t%3DB7lT6avDGo620CZb-1"
+ src="https://embed.figma.com/board/3eA0Lo6hqPrh40WFaRKATf/Intro-2?node-id=0-1&embed-host=share"
allowfullscreen
>
diff --git a/src/pages/developers/chains/_meta.json b/src/pages/developers/chains/_meta.json
index 20ca42bee..abf87b0bd 100644
--- a/src/pages/developers/chains/_meta.json
+++ b/src/pages/developers/chains/_meta.json
@@ -1,6 +1,6 @@
{
"list": {
- "title": "List of Connected Chains",
+ "title": "List of Chains",
"description": ""
},
"zetachain": {
diff --git a/src/pages/developers/chains/evm.mdx b/src/pages/developers/chains/evm.mdx
index 32e616757..b2d91fff7 100644
--- a/src/pages/developers/chains/evm.mdx
+++ b/src/pages/developers/chains/evm.mdx
@@ -1,70 +1,69 @@
-To interact with universal apps from EVM chains (like Ethereum, BNB, Polygon,
-etc.) use the EVM gateway.
+To interact with universal applications from EVM-compatible chains like
+Ethereum, BNB, Polygon, and others, use the EVM gateway.
EVM gateway supports:
-- depositing gas tokens to a universal app or an account on ZetaChain
-- depositing supported ERC-20 tokens (including ZETA tokens)
-- depositing gas tokens and calling a universal app
-- depositing supported ERC-20 tokens and calling a universal app
-- calling a universal app
+- Depositing gas tokens to a universal app or an account on ZetaChain.
+- Depositing supported ERC-20 tokens (including ZETA tokens).
+- Depositing gas tokens and calling a universal app.
+- Depositing supported ERC-20 tokens and calling a universal app.
+- Calling a universal app.
## Deposit Gas Tokens
-To deposit tokens to an EOA or a universal contract call the `deposit` function
-of the Gateway contract.
+To deposit tokens to an EOA or a universal contract, call the `deposit` function
+of the Gateway contract:
```solidity
deposit(address receiver, RevertOptions calldata revertOptions) external payable;
```
-The `deposit` function is payable, which means it accepts native gas tokens (for
-example, ETH on Ethereum), which will then be sent to a `receiver` on ZetaChain.
+The `deposit` function is payable, meaning it accepts native gas tokens (e.g.,
+ETH on Ethereum), which will then be sent to a `receiver` on ZetaChain.
-The `receiver` is either an externally-owned account (EOA) or a universal app
-address on ZetaChain. Even if the receiver is a universal app contract with the
-standard `receive` function, the `deposit` function will not trigger a contract
-call. If you want to deposit and call a universal app, please, use the
-`depositAndCall` function, instead.
+The `receiver` can be either an externally-owned account (EOA) or a universal
+app address on ZetaChain. Even if the receiver is a universal app contract with
+the standard `receive` function, the `deposit` function will not trigger a
+contract call. If you want to deposit and call a universal app, use the
+`depositAndCall` function instead.
-After the deposit is processed, the receiver gets [ZRC-20
-version](/developers/tokens/zrc20) of the deposited token, for example (ZRC-20
-ETH).
+After the deposit is processed, the receiver receives the [ZRC-20
+version](/developers/tokens/zrc20) of the deposited token—for example, ZRC-20
+ETH.
## Deposit ERC-20 Tokens
The `deposit` function can also be used to send supported ERC-20 tokens to EOAs
-and universal apps on ZetaChain.
+and universal apps on ZetaChain:
```solidity
deposit(address receiver, uint256 amount, address asset, RevertOptions calldata revertOptions) external;
```
Only [supported ERC-20 assets](/developers/tokens/zrc20) can be deposited. The
-receiver gets ZRC-20 version of the deposited token (for example, ZRC-20
-USDC.ETH).
+receiver gets the ZRC-20 version of the deposited token (e.g., ZRC-20 USDC.ETH).
-The `amount` is the amount and `asset` is the token address of the ERC-20 that
-is being deposited.
+The `amount` specifies the quantity, and `asset` is the token address of the
+ERC-20 being deposited.
## Deposit Gas Tokens and Call a Universal App
-To deposit tokens and call a universal app contract use the `depositAndCall`
-function.
+To deposit tokens and call a universal app contract, use the `depositAndCall`
+function:
```solidity
depositAndCall(address receiver, bytes calldata payload, RevertOptions calldata revertOptions) external payable;
```
-After the cross-chain transaction is processed, the `onCall` function of a
+After the cross-chain transaction is processed, the `onCall` function of the
universal app contract is executed.
-The `receiver` must be a universal app contract address.
+The `receiver` must be the address of a universal app contract.
```solidity
-pragma solidity 0.8.7;
+pragma solidity 0.8.26;
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
contract UniversalApp is UniversalContract {
function onCall(
@@ -78,46 +77,56 @@ contract UniversalApp is UniversalContract {
}
```
-`onCall` receives:
+In the `onCall` function, the parameters are as follows:
-- `message`: value of the `payload`
-- `amount`: amount of deposited tokens
-- `zrc20`: ZRC-20 address of a the deposited tokens (for example, contract
- address of ZRC-20 ETH)
+- `message`: the value of the `payload`.
+- `amount`: the amount of deposited tokens.
+- `zrc20`: the ZRC-20 address of the deposited tokens (e.g., the contract
+ address of ZRC-20 ETH).
- `context`:
- - `context.origin`: the original sender address on a connected chain (the EOA
- or contract that called the Gateway)
- - `context.chainID`: chain ID of the connected chain from which the call was
- made
+ - `context.sender`: the sender address on the connected chain (the EOA or
+ contract that called the Gateway).
+ - `context.chainID`: the chain ID of the connected chain from which the call
+ was made.
-When calling a universal app, the payload contains bytes passed to `onCall` as
-`message`. You don't need to pass a function selector in the payload as the only
-function that can be called from a connected chain is `onCall`.
+When calling a universal app, the `payload` is passed to `onCall` as `message`.
+You do not need to include a function selector in the payload, since `onCall` is
+the only function that can be called from a connected chain.
## Deposit ERC-20 Tokens and Call a Universal App
-`depositAndCall` can also be used to call a universal app contract and send
-ERC-20 tokens.
+The `depositAndCall` function can also be used to call a universal app contract
+and send ERC-20 tokens:
```solidity
depositAndCall(address receiver, uint256 amount, address asset, bytes calldata payload, RevertOptions calldata revertOptions) external;
```
-The `amount` is the amount and `asset` is the token address of the ERC-20 that
-is being deposited.
+Here, `amount` specifies the quantity, and `asset` is the token address of the
+ERC-20 being deposited.
-In the current version of the protocol only one ERC-20 asset can be deposited at
-a time.
+In the current version of the protocol, only one ERC-20 asset can be deposited
+at a time.
## Call a Universal App
-To call a universal app (without depositing tokens), use the `call` function.
+To call a universal app without depositing tokens, use the `call` function:
```solidity
call(address receiver, bytes calldata payload, RevertOptions calldata revertOptions) external;
```
+The `call` function invokes the `onCall` function of the specified `receiver`
+universal contract and passes the `payload` as the `message` parameter.
+
+The `call` function doesn't support revert handling. If
+`revertOptions.callOnRevert` is set to `true`, the transaction will fail. This
+is because executing a contract call on revert requires tokens to cover gas
+fees on ZetaChain, and the `call` function doesn't transfer any assets. If you
+need to handle reverts, use `depositAndCall` instead and ensure sufficient
+tokens are deposited to cover potential gas fees.
+
## Revert Transactions
-For information on `RevertOptions` refer to the [ZetaChain "Revert Transactions"
-doc](/developers/chains/zetachain#revert-transactions).
+For information on `RevertOptions`, refer to the [ZetaChain "Revert
+Transactions" documentation](/developers/chains/zetachain#revert-transactions).
diff --git a/src/pages/developers/chains/zetachain.mdx b/src/pages/developers/chains/zetachain.mdx
index 72e7e8b6e..9a839c79f 100644
--- a/src/pages/developers/chains/zetachain.mdx
+++ b/src/pages/developers/chains/zetachain.mdx
@@ -1,118 +1,118 @@
-To make a call from a universal app to a contract on a connected chain or to
+To make a call from a universal app to a contract on a connected chain or
withdraw tokens, use the ZetaChain gateway.
-ZetaChain gateway supports:
+The ZetaChain gateway supports:
-- withdrawing ZRC-20 tokens as native gas or ERC-20 tokens to connected chains
-- withdrawing ZETA tokens to connected chains
-withdrawing tokens to and making a contract call on connected chains
-- calling contracts on connected chains
+- Withdrawing ZRC-20 tokens as native gas or ERC-20 tokens to connected chains.
+- Withdrawing ZETA tokens to connected chains.
+- Withdrawing tokens and making a contract call on connected chains.
+- Calling contracts on connected chains.
## Withdraw ZRC-20 Tokens
-To withdraw ZRC-20 tokens to an EOA or a contract on a connected chain, call
-the `withdraw` function of the gateway contract.
+To withdraw ZRC-20 tokens to an EOA or a contract on a connected chain, use the
+`withdraw` function of the gateway contract:
```solidity
-withdraw(bytes memory receiver, uint256 amount, address zrc20, RevertOptions calldata revertOptions) external;
+function withdraw(bytes memory receiver, uint256 amount, address zrc20, RevertOptions calldata revertOptions) external;
```
-The `receiver` is either an externally-owned account (EOA) or a contract on a
-connected chain. Even if the receiver is a smart contract with the standard
+The `receiver` can be either an externally-owned account (EOA) or a contract on
+a connected chain. Even if the receiver is a smart contract with a standard
`receive` function, the `withdraw` function will not trigger a contract call. If
-you want to withdraw and call a contract on a connected chain, please, use the
-`withdrawAndCall` function, instead.
+you need to withdraw and call a contract on a connected chain, use the
+`withdrawAndCall` function instead.
-The `receiver` is of type `bytes`, because the receiver may be on a chain that
-uses a different address type, for example, bech32 on Bitcoin. `bytes` allow the
-receiver address to be chain agnostic. When withdrawing to a receiver on an EVM
-chain make sure that you convert `address` to `bytes`.
+The `receiver` is of type `bytes` to accommodate different address formats used
+by various chains (e.g., Bech32 for Bitcoin). This type ensures the receiver
+address is chain-agnostic. When withdrawing to an EVM chain, ensure you convert
+`address` to `bytes`.
-The `amount` is the amount and `zrc20` is the ZRC-20 address of the token that
-is being withdrawn.
+The `amount` specifies the quantity to withdraw, and `zrc20` is the ZRC-20
+address of the token being withdrawn.
-You don't need to specify which chain to withdraw to, because each ZRC-20 has an
-associated chain from which it was deposited. A ZRC-20 token can be withdrawn
-only to the chain from which it was originally deposited. This means that if you
-want to withdraw ZRC-20 USDC.ETH to the BNB chain, you first have to swap it to
-ZRC-20 USDC.BNB.
+You don’t need to specify the destination chain since each ZRC-20 token is tied
+to the chain from which it was deposited. A ZRC-20 token can only be withdrawn
+to its originating chain. For example, to withdraw ZRC-20 USDC.ETH to the BNB
+chain, you must first swap it to ZRC-20 USDC.BNB.
-## Withdraw ZETA Tokens
+## Withdraw ZRC-20 Tokens and Call a Contract on a Connected Chain
-The `withdraw` function can also be used to withdraw ZETA tokens to a connected
-chain.
-
-```
-withdraw(bytes memory receiver, uint256 amount, uint256 chainId, RevertOptions calldata revertOptions) external;
-```
-
-## Withdraw ZRC-20 Tokens and Call a Contract on Connected Chain
-
-To withdraw ZRC-20 tokens and make a call from a universal app to a contract on
-a connected chain use the `withdrawAndCall` function of the gateway contract.
+To withdraw ZRC-20 tokens and call a contract on a connected chain, use the
+`withdrawAndCall` function:
```solidity
-function withdrawAndCall(bytes memory receiver, uint256 amount, address zrc20, bytes calldata message, uint256 gasLimit, RevertOptions calldata revertOptions) external;
+function withdrawAndCall(bytes memory receiver, uint256 amount, address zrc20, bytes calldata message, CallOptions calldata callOptions, RevertOptions calldata revertOptions) external;
```
-The tokens are withdrawn and a call is made to a contract on the connected chain
-identified by the `zrc20` address. For example, if ZRC-20 ETH is being
-withdrawn, then the call is made to a contract on Ethereum.
+This function withdraws tokens and makes a call to a contract on the connected
+chain identified by the `zrc20` address. For instance, if ZRC-20 ETH is
+withdrawn, the call is made to a contract on Ethereum.
-## Withdraw ZETA Tokens and Call a Contract on Connected Chain
+## Call a Contract on a Connected Chain
-The `withdrawAndCall` function can also be used to withdraw ZETA tokens and make
-a call from a universal app to a contract on a connected chain.
+To call a contract on a connected chain without withdrawing tokens, use the
+`call` function:
```solidity
-withdrawAndCall(bytes memory receiver, uint256 amount, uint256 chainId, bytes calldata message, RevertOptions calldata revertOptions) external;
+function call(bytes memory receiver, address zrc20, bytes calldata message, CallOptions calldata callOptions, RevertOptions calldata revertOptions) external;
```
-## Call a Contract on a Connected Chain
+Here, `zrc20` represents the ZRC-20 token address of the gas token for the
+destination chain. This address acts as an identifier for the target chain. For
+example, to call a contract on Ethereum, use the ZRC-20 ETH token address.
-To call a contract on a connected chain (without withdrawing tokens), use the
-`call` function.
+## Call Options
+
+The `CallOptions` parameter specifies details for making calls to contracts on
+connected chains. It is used in both the `call` and `withdrawAndCall` functions:
```solidity
-function call(bytes memory receiver, address zrc20, bytes calldata message, uint256 gasLimit, RevertOptions calldata revertOptions) external;
+struct CallOptions {
+ uint256 gasLimit;
+ bool isArbitraryCall;
+}
```
-`zrc20` represents the ZRC-20 token address of the gas token of the destination
-chain. In the context of this function `zrc20` address acts as an identifier for
-the chain to which the call is made. For example, to make a call to Ethereum,
-use ZRC-20 ETH token address.
+- **`gasLimit`**: The maximum gas the cross-chain contract call can consume. If
+ the gas usage exceeds this limit, the transaction reverts.
+- **`isArbitraryCall`**: Determines whether the call is "arbitrary" (`true`) or
+ "authenticated" (`false`).
+
+An arbitrary call invokes any function on a connected chain but does not retain
+the original caller's identity—within the target contract, `msg.sender` is the
+Gateway address, not the originating universal contract. This is suitable for
+scenarios like token swaps, where the caller’s identity is unnecessary.
-## Format of the `message` when Calling Contracts
+An authenticated call specifically targets the `onCall` function of a contract
+on the connected chain. Authentication is achieved because the `onCall` function
+receives the `context.sender` parameter, referencing the originating universal
+contract. This allows the target contract to verify and trust the initiating
+universal app, rejecting unauthorized calls.
-The `withdrawAndCall` and `call` functions have a `bytes calldata message`
-parameter. This parameter contains the function selector and the encoded
-arguments necessary to call a specific function in the target contract.
+## Format of the `message` Parameter
-The message parameter should contain:
+The `message` parameter in the `withdrawAndCall` and `call` functions contains
+the encoded function selector and arguments for the target contract:
-- Function selector: the first 8 bytes represent the function selector, which is
- the first 4 bytes of the Keccak-256 hash of the function signature.
-- Arguments: the remaining bytes in the message correspond to the arguments
- passed to the function, encoded according to Ethereum's ABI encoding rules.
- These arguments can vary in length depending on the data types.
+- **Function Selector**: The first 4 bytes of the Keccak-256 hash of the
+ function signature.
+- **Arguments**: The remaining bytes, ABI-encoded according to Ethereum’s rules.
-For example, consider the following message:
+For example:
```
0xa777d0dc00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005616c696365000000000000000000000000000000000000000000000000000000
```
-- Function Selector: `0xa777d0dc`. This corresponds to the function
- `hello(string)`.
-- Arguments: The remaining data
- (`0000000000000000000000000000000000000000000000000000000000000020...`) is the
- ABI-encoded argument passed to the `hello(string)` function. Specifically:
- `616c696365` represents the string `alice` in hexadecimal.
+- **Function Selector**: `0xa777d0dc` corresponds to `hello(string)`.
+- **Arguments**: The remaining data represents the argument `alice`, encoded in
+ hexadecimal (`616c696365`).
## Revert Transactions
-The `RevertOptions` struct defines how assets should be handled in the event of
-a cross-chain transaction (CCTX) reverting:
+The `RevertOptions` struct specifies how assets are handled in case of a
+cross-chain transaction (CCTX) revert:
```solidity
struct RevertOptions {
@@ -124,28 +124,18 @@ struct RevertOptions {
}
```
-`revertAddress`: the address that should get assets back if the CCTX reverts.
-For example, if a smart contract using ZetaChain wants to send assets back to
-the sender upon a revert, `revertAddress` should be set to `msg.sender`.
-
-`callOnRevert`: a boolean flag indicating whether the `onRevert` function should
-be called on the `revertAddress`. For example, a smart contract may want to
-execute custom logic when a revert occurs, such as unlocking tokens. In this
-case, the contract would set `callOnRevert` to true and assign `revertAddress`
-to `address(this)`.
-
-`abortAddress`: the address that will receive the funds on ZetaChain if the CCTX
-aborts. This feature is not currently used.
-
-`revertMessage`: message sent back to the `onRevert` function. This allows
-additional context to be provided for handling the revert.
+- **`revertAddress`**: The address to return assets to if the transaction
+ reverts.
+- **`callOnRevert`**: A flag indicating whether to call the `onRevert` function
+ on the `revertAddress`.
+- **`abortAddress`**: The address to receive funds on ZetaChain if the CCTX
+ aborts (not currently used).
+- **`revertMessage`**: Context passed to the `onRevert` function.
+- **`onRevertGasLimit`**: Gas limit for executing the `onRevert` function.
-`onRevertGasLimit`: the gas limit to be used when executing the `onRevert`
-function.
+### Revertable Contracts
-Contracts that implement the `onRevert` functionality are referred to as
-`Revertable` contracts. These contracts should conform to the following
-interface:
+Contracts implementing the `onRevert` function must conform to this interface:
```solidity
struct RevertContext {
@@ -159,5 +149,5 @@ interface Revertable {
}
```
-This interface allows the contract to handle reverts in a customized way, based
-on the context provided through the `RevertContext` struct.
+This interface allows contracts to handle reverts dynamically, using the
+information provided in the `RevertContext`.
diff --git a/src/pages/developers/evm/_meta.json b/src/pages/developers/evm/_meta.json
index a065bebd2..5a2202836 100644
--- a/src/pages/developers/evm/_meta.json
+++ b/src/pages/developers/evm/_meta.json
@@ -1,6 +1,6 @@
{
"overview": {
- "title": "Architecture Overview",
+ "title": "Architecture",
"description": "Architecture overview of ZetaChain"
},
"gateway": {
diff --git a/src/pages/developers/evm/gas.mdx b/src/pages/developers/evm/gas.mdx
index 341d52ee2..0bd1da5e5 100644
--- a/src/pages/developers/evm/gas.mdx
+++ b/src/pages/developers/evm/gas.mdx
@@ -33,7 +33,7 @@ When withdrawing ZRC-20 tokens back to a connected connected chain, the
"withdraw gas fee" is applicable (listed as "Total fee" in the table below).
To find out the fee amount, call the `withdrawGasFee` function on the [ZRC-20
-contract](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/zevm/ZRC20.sol)
+contract](https://github.com/zeta-chain/protocol-contracts/blob/main/v2/contracts/zevm/ZRC20.sol)
for the token you wish to withdraw. This function will return the fee in the
native gas token of the connected chain.
diff --git a/src/pages/developers/tokens/zrc20.mdx b/src/pages/developers/tokens/zrc20.mdx
index 1f6b1bcf7..473815914 100644
--- a/src/pages/developers/tokens/zrc20.mdx
+++ b/src/pages/developers/tokens/zrc20.mdx
@@ -47,8 +47,6 @@ New assets can be added or removed by broadcasting a transaction with a
corresponding message of the [`fungible`
module](/developers/architecture/modules/fungible/messages/) on ZetaChain.
-## Introduction
-
At a high-level, ZRC-20 tokens are an extension of the standard
[ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/)
tokens found in the Ethereum ecosystem, ZRC-20 tokens have the added ability to
@@ -57,146 +55,6 @@ Bitcoin, ETH, other gas assets and ERC-20-equivalents on other chains, 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 (see
-the [`IZRC20`
-interface](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/zevm/interfaces/IZRC20.sol)).
-
-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.
-
-## Depositing Native Gas Tokens as ZRC-20
-
-```mermaid
-flowchart LR
- subgraph Ethereum ["Ethereum (Polygon or BSC)"]
- direction LR
- subgraph send ["Transaction"]
- direction LR
- Data -- contains --- Message("Message")
- Data((Data)) -- contains --- address("Omnichain contract address")
- Value((Value)) -- contains --- eth("1 ETH")
- end
- account("Account") -- sends --- send
- send --> TSS("TSS Address")
- end
- subgraph ZetaChain
- SystemContract("System Contract")
- subgraph contract ["Omnichain contract"]
- addr("Contract address")
- subgraph onCall
- msg("bytes calldata message")
- zrc20("address zrc20")
- amount("uint256 amount")
- context("MessageContext calldata context")
- end
- end
- end
- TSS --> SystemContract
- SystemContract -- calls --> contract
- address -.- addr
- eth -. ZRC-20 contract address of ETH .- zrc20
- eth -. deposited amount .- amount
- Ethereum -. chainID .- context
- Message -. arbitrary data .- msg
- account -. "origin" .- context
-```
-
-To deposit a native gas token (like sETH, tMATIC, tBNB, or tBTC) to ZetaChain,
-send it to the [TSS address](/reference/network/contracts) on a connected chain.
-
-If the input data field of the transaction is empty, the token will be deposited
-to the sender's address on ZetaChain.
-
-If the input data field is not empty, the protocol looks up the first 20 bytes
-of the input data field. If the first 20 bytes correspond to an EOA address on
-ZetaChain, the token will be deposited to that address. If the first 20 bytes
-correspond to a contract address on ZetaChain, the token will be deposited to
-that contract and the `onCall` function of that contract will be called with the
-remaining input data as the `message`.
-
-When depositing native gas tokens from EVM-based connected chains, there is no
-additional cross-chain fee. If you send 1 token to a TSS address, you will
-receive 1 ZRC-20 version of the same token on ZetaChain.
-
-For Bitcoin deposits, which utilize the UTXO (Unspent Transaction Output) model,
-the process [incurs additional fees](/developers/chains/bitcoin#deposit-fee).
-
-## Depositing Supported ERC-20 Tokens as ZRC-20
-
-```mermaid
-flowchart LR
- subgraph Ethereum ["Ethereum (Polygon or BSC)"]
- direction LR
- subgraph send ["Deposit arguments"]
- direction LR
- recipient("bytes recipient") -- contains --- address("Omnichain contract address")
- message("bytes message") -- contains --- Message("Message")
- asset("IERC20 asset") -- contains --- erc20address("ERC-20 address")
- erc20amount("uint256 amount") -- contains --- eth("1")
- end
- account("Account") -- "call deposit method" --- send
- send --> TSS("ERC-20 custody")
- end
- subgraph ZetaChain
- SystemContract("System Contract")
- subgraph contract ["Omnichain contract"]
- addr("Contract address")
- subgraph onCall
- msg("bytes calldata message")
- zrc20("address zrc20")
- amount("uint256 amount")
- context("MessageContext calldata context")
- end
- end
- end
- TSS --> SystemContract
- SystemContract -- calls --> contract
- address -.- addr
- erc20address -. ZRC-20 contract address of ERC-20 .- zrc20
- eth -. deposited amount .- amount
- Ethereum -. chainID .- context
- Message -. arbitrary data .- msg
- account -. "origin" .- context
-```
-
-To deposit a supported ERC-20 token to ZetaChain, use the `deposit` method of
-the [ERC-20 custody contract](/reference/network/contracts) on a connected
-chain.
-
-The `deposit` method accepts the following parameters:
-
-- `recipient`: the address on ZetaChain to deposit the tokens to. If the
- recipient is an EOA, the tokens will be deposited to the recipient's address.
- If the recipient is a contract, the tokens will be deposited to the contract
- and the `onCall` function of that contract will be called with the `message`
- as an argument.
-- `asset`: the address of the ERC-20 token to deposit.
-- `amount`: the amount of tokens to deposit.
-- `message`: an arbitrary message to be passed to the `onCall` function of the
- recipient contract. If the recipient is an EOA, the message should be empty.
-
-## Withdrawing ZRC-20 Tokens from ZetaChain
-
-ZRC-20 tokens, which include those representing native gas tokens as well as
-ERC-20 tokens, can be withdrawn from ZetaChain by calling the `withdraw` method
-on the ZRC-20 contract.
-
-This method burns the tokens and emits a `Withdrawal` event. This event will
-trigger a CCTX from ZetaChain to the connected chain from which the token was
-deposited. After the CCTX is processed the token amount will be transferred to
-the recipient address on the connected chain either from a TSS address (for
-native gas tokens) or from the ERC-20 custody contract (for ERC-20 tokens).
-
-Check out the [Swap tutorial](/developers/tutorials/swap) to learn how to build
-omnichain contracts that accept token deposits form connected chains, swap
-between ZRC-20 tokens using the internal liquidity pools on ZetaChain, and
-withdraw them to connected chains.
-
## Block Confirmations
When depositing to or withdrawing from ZetaChain, the protocol requires a
diff --git a/src/pages/developers/tutorials/_meta.json b/src/pages/developers/tutorials/_meta.json
index b83e3fe33..c5788ba55 100644
--- a/src/pages/developers/tutorials/_meta.json
+++ b/src/pages/developers/tutorials/_meta.json
@@ -2,10 +2,15 @@
"intro": {
"title": "Getting Started",
"readTime": "10 min",
- "description": "Learn how to set up a smart contract template, create an account, and use faucet"
+ "description": "Learn more about universal apps"
},
"hello": {
- "title": "Message Passing",
+ "title": "First Universal App",
+ "readTime": "10 min",
+ "description": "Build your first universal app"
+ },
+ "call": {
+ "title": "Call & Deposit",
"readTime": "30 min",
"description": "Learn the fundamentals of message passing and cross-chain contract calls"
},
@@ -25,8 +30,13 @@
"description": "Mint a universal NFT, which can be transferred between connected chains"
},
"localnet": {
- "title": "Localnet",
+ "title": "Localnet Setup",
"readTime": "30 min",
"description": "Build and interact with your universal app in a local dev environment"
+ },
+ "testnet": {
+ "title": "Testnet Setup",
+ "readTime": "10 min",
+ "description": "Setup your account, request test tokens from the faucet"
}
}
\ No newline at end of file
diff --git a/src/pages/developers/tutorials/call.mdx b/src/pages/developers/tutorials/call.mdx
new file mode 100644
index 000000000..d397ded47
--- /dev/null
+++ b/src/pages/developers/tutorials/call.mdx
@@ -0,0 +1,555 @@
+---
+title: Call, Deposit & Withdraw
+---
+
+import { Alert } from "~/components/shared";
+
+In this tutorial, you'll create two contracts: a Universal contract on ZetaChain
+and a Connected contract, deployed on one of the connected EVM chains. The
+Connected contract implements functions that use the Gateway contract to make
+calls and token deposits to the Universal contract. The Universal app implements
+functions to make calls and tokens withdrawals to a connected chain.
+
+By the end of this tutorial, you will have learned how to:
+
+- Create contracts that use the Gateway to make cross-chain calls
+- Make cross-chain calls, token deposits and withdrawals (both native gas and
+ supported ERC-20s)
+- Gracefully handle reverts
+
+This tutorial relies on the Gateway, which is currently available only on localnet and testnet.
+
+## Prerequisites
+
+Before you begin, make sure you've completed the following tutorials:
+
+- [Introduction to Universal Apps](/developers/apps/intro/)
+- [Getting Started with ZetaChain](/developers/tutorials/intro)
+- [First Universal App](/developers/tutorials/hello)
+
+## Set Up Your Environment
+
+Start by cloning the example contracts repository and installing the necessary
+dependencies:
+
+```bash
+git clone https://github.com/zeta-chain/example-contracts
+cd example-contracts/examples/call
+yarn
+```
+
+## Universal Contract
+
+```solidity filename="contracts/Universal.sol"
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.26;
+
+import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
+import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
+
+contract Universal is UniversalContract {
+ GatewayZEVM public immutable gateway;
+
+ event HelloEvent(string, string);
+ event RevertEvent(string, RevertContext);
+
+ error TransferFailed();
+ error Unauthorized();
+
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
+
+ constructor(address payable gatewayAddress) {
+ gateway = GatewayZEVM(gatewayAddress);
+ }
+
+ function call(
+ bytes memory receiver,
+ address zrc20,
+ bytes calldata message,
+ CallOptions memory callOptions,
+ RevertOptions memory revertOptions
+ ) external {
+ (, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(
+ callOptions.gasLimit
+ );
+ if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), gasFee)) {
+ revert TransferFailed();
+ }
+ IZRC20(zrc20).approve(address(gateway), gasFee);
+ gateway.call(receiver, zrc20, message, callOptions, revertOptions);
+ }
+
+ function withdraw(
+ bytes memory receiver,
+ uint256 amount,
+ address zrc20,
+ RevertOptions memory revertOptions
+ ) external {
+ (address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee();
+ uint256 target = zrc20 == gasZRC20 ? amount + gasFee : amount;
+ if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), target)) {
+ revert TransferFailed();
+ }
+ IZRC20(zrc20).approve(address(gateway), target);
+ if (zrc20 != gasZRC20) {
+ if (
+ !IZRC20(gasZRC20).transferFrom(
+ msg.sender,
+ address(this),
+ gasFee
+ )
+ ) {
+ revert TransferFailed();
+ }
+ IZRC20(gasZRC20).approve(address(gateway), gasFee);
+ }
+ gateway.withdraw(receiver, amount, zrc20, revertOptions);
+ }
+
+ function withdrawAndCall(
+ bytes memory receiver,
+ uint256 amount,
+ address zrc20,
+ bytes calldata message,
+ CallOptions memory callOptions,
+ RevertOptions memory revertOptions
+ ) external {
+ (address gasZRC20, uint256 gasFee) = IZRC20(zrc20)
+ .withdrawGasFeeWithGasLimit(callOptions.gasLimit);
+ uint256 target = zrc20 == gasZRC20 ? amount + gasFee : amount;
+ if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), target))
+ revert TransferFailed();
+ IZRC20(zrc20).approve(address(gateway), target);
+ if (zrc20 != gasZRC20) {
+ if (
+ !IZRC20(gasZRC20).transferFrom(
+ msg.sender,
+ address(this),
+ gasFee
+ )
+ ) {
+ revert TransferFailed();
+ }
+ IZRC20(gasZRC20).approve(address(gateway), gasFee);
+ }
+ gateway.withdrawAndCall(
+ receiver,
+ amount,
+ zrc20,
+ message,
+ callOptions,
+ revertOptions
+ );
+ }
+
+ function onCall(
+ MessageContext calldata context,
+ address zrc20,
+ uint256 amount,
+ bytes calldata message
+ ) external override onlyGateway {
+ string memory name = abi.decode(message, (string));
+ emit HelloEvent("Hello on ZetaChain", name);
+ }
+
+ function onRevert(
+ RevertContext calldata revertContext
+ ) external onlyGateway {
+ emit RevertEvent("Revert on ZetaChain", revertContext);
+ }
+}
+```
+
+Let's break down what this contract does. The `Universal` contract inherits from
+the
+[`UniversalContract`](https://github.com/zeta-chain/protocol-contracts/blob/main/v2/contracts/zevm/interfaces/UniversalContract.sol)
+interface, which requires the implementation of `onCall` and `onRevert`
+functions for handling cross-chain interactions.
+
+A state variable `gateway` of type `GatewayZEVM` holds the address of
+ZetaChain's gateway contract. This gateway facilitates communication between
+ZetaChain and connected chains.
+
+In the constructor, we initialize the `gateway` state variable with the address
+of the ZetaChain gateway contract.
+
+### Handling Incoming Cross-Chain Calls
+
+Within `onCall`, the contract decodes the `name` from the `message` and emits a
+`HelloEvent` to signal successful reception and processing of the message. It's
+important to note that `onCall` can only be invoked by the ZetaChain protocol,
+ensuring the integrity of cross-chain interactions.
+
+### Making an Outgoing Contract Call
+
+The `call` function demonstrates how a universal app can initiate a contract
+call to an arbitrary contract on a connected chain using the gateway. It
+operates as follows:
+
+1. **Calculate Gas Fee**: Determines the required gas fee based on the specified
+ `gasLimit`. The gas limit represents the anticipated amount of gas needed to
+ execute the contract on the destination chain.
+
+2. **Transfer Gas Fee**: Moves the calculated gas fee from the sender to the
+ `Universal` contract. The user must grant the `Universal` contract permission
+ to spend their gas fee tokens.
+
+3. **Approve Gateway**: Grants the gateway permission to spend the transferred
+ gas fee.
+
+4. **Execute Cross-Chain Call**: Invokes `gateway.call` to initiate the contract
+ call on the connected chain. The message (for authenticated calls) or the
+ function selector and its arguments (for arbitrary calls) are encoded within
+ the `message`. Learn more about arbitrary and authenticated calls in the
+ [Call Options doc](/developers/chains/zetachain/#call-options). The gateway
+ identifies the target chain based on the ZRC-20 token, as each chain's gas
+ asset is associated with a specific ZRC-20 token.
+
+### Withdrawing Tokens
+
+The `withdraw` function withdraws ZRC-20 tokens from ZetaChain to a connected
+chain. The function executes the following steps:
+
+1. **Calculate Gas Fee**: Computes the necessary gas fee based on the provided
+ `gasLimit`.
+
+2. **Transfer Tokens**: Moves the specified `amount` of tokens from the sender
+ to the `Universal` contract. If the token being withdrawn is the gas token of
+ the destination chain, the function transfers and approves both the gas fee
+ and the withdrawal amount. If the target token is not the gas token, it
+ transfers and approves the gas fee separately.
+
+3. **Approve Gateway**: Grants the gateway permission to spend the transferred
+ tokens and gas fee.
+
+4. **Execute Withdrawal**: Calls `gateway.withdraw` to withdraw the tokens.
+
+If a user withdraws an ERC-20 token, the contract assumes that the user has a
+required amount of gas ZRC-20 tokens to pay the withdraw fee. In a
+production-ready contract the contract might need to swap a fraction of the
+token being withdrawn to cover the withdraw fee.
+
+### Withdrawing Tokens and Making a Call
+
+The `withdrawAndCall` function shows how a universal app can perform a token
+withdrawal with a call to an arbitrary contract on a connected chain using the
+gateway. The function executes the following steps:
+
+1. **Calculate gas fee, transfer tokens, approve gateway**: execute the same
+ steps as `withdraw`.
+2. **Execute Withdrawal and Call**: Calls `gateway.withdrawAndCall` to withdraw
+ the tokens and initiate the contract call on the connected chain. Compared to
+ simple withdrawing, `withdrawAndCall` also accepts a message (similar to
+ `call` it can be a regular message for authenticated calls or a function
+ selector and parameters for arbitrary calls) and call options.
+
+## Connected Contract
+
+```solidity filename="contracts/Connected.sol"
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.26;
+
+import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
+contract Connected {
+ using SafeERC20 for IERC20; // Use SafeERC20 for IERC20 operations
+
+ GatewayEVM public immutable gateway;
+
+ event RevertEvent(string, RevertContext);
+ event HelloEvent(string, string);
+
+ error Unauthorized();
+
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
+
+ constructor(address payable gatewayAddress) {
+ gateway = GatewayEVM(gatewayAddress);
+ }
+
+ function call(
+ address receiver,
+ bytes calldata message,
+ RevertOptions memory revertOptions
+ ) external {
+ gateway.call(receiver, message, revertOptions);
+ }
+
+ function deposit(
+ address receiver,
+ RevertOptions memory revertOptions
+ ) external payable {
+ gateway.deposit{value: msg.value}(receiver, revertOptions);
+ }
+
+ function deposit(
+ address receiver,
+ uint256 amount,
+ address asset,
+ RevertOptions memory revertOptions
+ ) external {
+ IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
+ IERC20(asset).approve(address(gateway), amount);
+ gateway.deposit(receiver, amount, asset, revertOptions);
+ }
+
+ function depositAndCall(
+ address receiver,
+ uint256 amount,
+ address asset,
+ bytes calldata message,
+ RevertOptions memory revertOptions
+ ) external {
+ IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
+ IERC20(asset).approve(address(gateway), amount);
+ gateway.depositAndCall(receiver, amount, asset, message, revertOptions);
+ }
+
+ function depositAndCall(
+ address receiver,
+ bytes calldata message,
+ RevertOptions memory revertOptions
+ ) external payable {
+ gateway.depositAndCall{value: msg.value}(
+ receiver,
+ message,
+ revertOptions
+ );
+ }
+
+ function hello(string memory message) external payable {
+ emit HelloEvent("Hello on EVM", message);
+ }
+
+ function onCall(
+ MessageContext calldata context,
+ bytes calldata message
+ ) external payable onlyGateway returns (bytes4) {
+ emit HelloEvent("Hello on EVM from onCall()", "hey");
+ return "";
+ }
+
+ function onRevert(
+ RevertContext calldata revertContext
+ ) external onlyGateway {
+ emit RevertEvent("Revert on EVM", revertContext);
+ }
+
+ receive() external payable {}
+
+ fallback() external payable {}
+}
+```
+
+### Making a Contract Call
+
+The `call` functions executes `gateway.call` to make a cross-chain call to the
+`onCall` function of a universal contract on ZetaChain. The `receiver` parameter
+determines which universal contract will be called, and the `message` contains
+the bytes that will be passed to the `onCall` function.
+
+### Depositing Tokens
+
+The `deposit` function uses the Gateway to deposit native gas or supported
+ERC-20 tokens to a contract or an EOA on ZetaChain. `deposit` only makes a
+cross-chain transfer without executing logic on ZetaChain, even if the
+`receiver` is a universal contract.
+
+Tokens deposited through the Gateway end up as ZRC-20 assets on ZetaChain.
+
+### Depositing Tokens and Making a Call
+
+The `depositAndCall` function uses the Gateway to deposit native gas or
+supported ERC-20 tokens and make a cross-chain call to the `onCall` function of
+a universal contract on ZetaChain.
+
+## Option 1: Deploy on Localnet
+
+[Localnet](/developers/tutorials/localnet) provides a simulated environment for
+developing and testing ZetaChain contracts locally.
+
+To start localnet, open a terminal window and run:
+
+```bash
+npx hardhat localnet
+```
+
+This command initializes a local blockchain environment that simulates the
+behavior of ZetaChain protocol contracts.
+
+## Deploying the Contracts
+
+With localnet running, open a new terminal window to compile and deploy the
+`Universal` and `Connected` contracts:
+
+```bash
+npx hardhat compile --force
+npx hardhat deploy --name Universal --network localhost --gateway 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
+npx hardhat deploy --name Connected --json --network localhost --gateway 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
+```
+
+You should see output indicating the successful deployment of the contracts:
+
+```
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+
+🚀 Successfully deployed "Universal" contract on localhost.
+📜 Contract address: 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB
+
+🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+
+🚀 Successfully deployed "Conntected" contract on localhost.
+📜 Contract address: 0x9E545E3C0baAB3E08CdfD552C960A1050f373042
+```
+
+**Note**: The deployed contract addresses may differ in your environment.
+
+## Calling the `Connected` Contract from `Universal`
+
+In this example, you'll invoke the `Connected` contract on the connected EVM
+chain, which in turn calls the `Universal` contract on ZetaChain. Run the
+following command, replacing the contract addresses with those from your
+deployment:
+
+```bash
+npx hardhat connected-call \
+ --contract 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
+ --receiver 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
+ --network localhost \
+ --types '["string"]' alice
+```
+
+## Calling an EVM Contract from a Universal App
+
+Now, let's perform the reverse operation: calling a contract on a connected EVM
+chain from the `Universal` universal app on ZetaChain. Execute the following
+command, replacing the contract addresses and ZRC-20 token address with those
+from your deployment:
+
+```bash
+npx hardhat universal-call \
+ --contract 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
+ --receiver 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
+ --zrc20 0x2ca7d64A7EFE2D62A725E2B35Cf7230D6677FfEe \
+ --function "hello(string)" \
+ --network localhost \
+ --types '["string"]' alice
+```
+
+## Withdrawing and Calling an EVM Contract from a Universal App
+
+To withdraw tokens and call a contract on a connected chain from a universal
+app, run the following command:
+
+```bash
+npx hardhat universal-withdraw-and-call \
+ --contract 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
+ --receiver 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
+ --zrc20 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c \
+ --function "hello(string)" \
+ --amount 1 \
+ --network localhost \
+ --types '["string"]' hello
+```
+
+## Option 2: Deploy on Testnet
+
+Before proceeding, you might want to check out the [Testnet
+Setup](/developers/tutorials/testnet) guide to learn how to set up an account
+and request testnet tokens.
+
+```bash
+npx hardhat compile --force
+npx hardhat deploy --gateway 0x6c533f7fe93fae114d0954697069df33c9b74fd7 --network zeta_testnet --name Universal
+npx hardhat deploy --gateway 0x0c487a766110c85d301d96e33579c5b317fa4995 --network base_sepolia --name Connected
+```
+
+```
+🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+
+🚀 Successfully deployed "Universal" contract on zeta_testnet.
+📜 Contract address: 0x496CD66950a1F1c5B02513809A2d37fFB942be1B
+
+🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
+
+🚀 Successfully deployed "Connected" contract on base_sepolia.
+📜 Contract address: 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed
+```
+
+## Call from Base to ZetaChain
+
+```
+npx hardhat connected-call \
+ --contract 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
+ --receiver 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
+ --network base_sepolia \
+ --tx-options-gas-price 20000 --types '["string"]' alice
+```
+
+https://sepolia.basescan.org/tx/0x133cdf3a06195de0a6bb89dd4761ca98d1301534b3c4987f0ff93c95c3fff78c
+
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x133cdf3a06195de0a6bb89dd4761ca98d1301534b3c4987f0ff93c95c3fff78c
+
+## Call from ZetaChain to Base
+
+```
+npx hardhat universal-call \
+ --contract 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
+ --receiver 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
+ --zrc20 0x236b0DE675cC8F46AE186897fCCeFe3370C9eDeD \
+ --function "hello(string)" \
+ --network zeta_testnet \
+ --tx-options-gas-price 200000 --types '["string"]' alice
+```
+
+https://zetachain-testnet.blockscout.com/tx/0x19d476fa2c3d29ba41467ae7f2742541fd11e0b67d6548fe7655a3d40274323e
+
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x19d476fa2c3d29ba41467ae7f2742541fd11e0b67d6548fe7655a3d40274323e
+
+## Withdraw And Call from ZetaChain to Base
+
+```
+npx hardhat universal-withdraw-and-call \
+ --contract 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
+ --receiver 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
+ --zrc20 0x236b0DE675cC8F46AE186897fCCeFe3370C9eDeD \
+ --function "hello(string)" \
+ --amount 0.005 \
+ --network zeta_testnet \
+ --types '["string"]' hello
+```
+
+https://zetachain-testnet.blockscout.com/tx/0x67099389ab6cb44ee03602d164320c615720b57762c5ddab042d65bdbe30c7a2
+
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x67099389ab6cb44ee03602d164320c615720b57762c5ddab042d65bdbe30c7a2
+
+## Conclusion
+
+In this tutorial, you've:
+
+- Defined a universal app contract (`Universal`) to handle cross-chain messages.
+- Deployed both `Universal` and `Connected` contracts to a local development
+ network.
+- Interacted with the `Universal` contract by sending messages from a connected
+ EVM chain via the `Connected` contract.
+- Simulated revert scenarios and handled them gracefully using revert logic in
+ both contracts.
+
+By mastering cross-chain calls and revert handling, you're now prepared to build
+robust and resilient universal applications on ZetaChain.
+
+## Source Code
+
+You can find the complete source code for this tutorial in the [example
+contracts
+repository](https://github.com/zeta-chain/example-contracts/tree/main/examples/call).
diff --git a/src/pages/developers/tutorials/hello.mdx b/src/pages/developers/tutorials/hello.mdx
index 8749923bd..c0d78dc74 100644
--- a/src/pages/developers/tutorials/hello.mdx
+++ b/src/pages/developers/tutorials/hello.mdx
@@ -1,30 +1,19 @@
----
-title: Message Passing
----
-
-import { Alert } from "~/components/shared";
-
-In this tutorial, you'll build a universal app contract that accepts messages
-from connected chains and emits corresponding events on ZetaChain. For example,
-a user on Ethereum can send the message `"alice"`, and the universal contract on
-ZetaChain will emit an event with the string `"Hello on ZetaChain, alice"`.
+In this tutorial you will create a simple universal app deployed on ZetaChain,
+which emits when called from a connected chain.
By the end of this tutorial, you will have learned how to:
-- Define a universal app contract to handle messages from connected chains.
-- Deploy the contract to localnet.
-- Interact with the contract by sending a message from a connected EVM
- blockchain in localnet.
-- Gracefully handle reverts by implementing revert logic.
-
-This tutorial relies on the Gateway, which is currently available only on localnet and testnet.
-
-## Prerequisites
+- Build a simple universal app
+- Deploy the universal app on localnet
+- Use the Gateway on a connected chain to call a universal app
-Before you begin, make sure you've completed the following tutorials:
-
-- [Introduction to Universal Apps](/developers/apps/intro/)
-- [Getting Started with ZetaChain](/developers/tutorials/intro)
+
## Set Up Your Environment
@@ -37,29 +26,26 @@ cd example-contracts/examples/hello
yarn
```
-## Universal App Contract: `Hello`
-
-The `Hello` contract is a simple universal app deployed on ZetaChain. It
-implements the `UniversalContract` interface, enabling it to handle cross-chain
-calls and token transfers from connected chains.
+## Universal Contract
-Here's the code for the `Hello` contract:
+A universal app is a contract that inherits from `UniversalContract` interface.
-```solidity filename="contracts/Hello.sol"
+```solidity filename="contracts/Universal.sol"
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
-import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
-import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
-contract Hello is UniversalContract {
+contract Universal is UniversalContract {
GatewayZEVM public immutable gateway;
event HelloEvent(string, string);
- event RevertEvent(string, RevertContext);
- error TransferFailed();
+ error Unauthorized();
+
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
constructor(address payable gatewayAddress) {
gateway = GatewayZEVM(gatewayAddress);
@@ -70,398 +56,124 @@ contract Hello is UniversalContract {
address zrc20,
uint256 amount,
bytes calldata message
- ) external override {
+ ) external override onlyGateway {
string memory name = abi.decode(message, (string));
- emit HelloEvent("Hello on ZetaChain", name);
- }
-
- function onRevert(RevertContext calldata revertContext) external override {
- emit RevertEvent("Revert on ZetaChain", revertContext);
- }
-
- function call(
- bytes memory receiver,
- address zrc20,
- bytes calldata message,
- uint256 gasLimit,
- RevertOptions memory revertOptions
- ) external {
- (, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(gasLimit);
- if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), gasFee))
- revert TransferFailed();
- IZRC20(zrc20).approve(address(gateway), gasFee);
- gateway.call(receiver, zrc20, message, gasLimit, revertOptions);
- }
-
- function withdrawAndCall(
- bytes memory receiver,
- uint256 amount,
- address zrc20,
- bytes calldata message,
- uint256 gasLimit,
- RevertOptions memory revertOptions
- ) external {
- (address gasZRC20, uint256 gasFee) = IZRC20(zrc20)
- .withdrawGasFeeWithGasLimit(gasLimit);
- uint256 target = zrc20 == gasZRC20 ? amount + gasFee : amount;
- if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), target))
- revert TransferFailed();
- IZRC20(zrc20).approve(address(gateway), target);
- if (zrc20 != gasZRC20) {
- if (
- !IZRC20(gasZRC20).transferFrom(
- msg.sender,
- address(this),
- gasFee
- )
- ) revert TransferFailed();
- IZRC20(gasZRC20).approve(address(gateway), gasFee);
- }
- gateway.withdrawAndCall(
- receiver,
- amount,
- zrc20,
- message,
- gasLimit,
- revertOptions
- );
+ emit HelloEvent("Hello: ", name);
}
}
```
-Let's break down what this contract does. The `Hello` contract inherits from the
-[`UniversalContract`](https://github.com/zeta-chain/protocol-contracts/blob/main/v2/contracts/zevm/interfaces/UniversalContract.sol)
-interface, which requires the implementation of `onCall` and `onRevert`
-functions for handling cross-chain interactions.
-
-A state variable `gateway` of type `GatewayZEVM` holds the address of
-ZetaChain's gateway contract. This gateway facilitates communication between
-ZetaChain and connected chains.
+The constructor accepts the ZetaChain's Gateway address and saves it into a
+state variable. Gateway is used to make outgoing contract calls and token
+withdrawals.
-In the constructor, we initialize the `gateway` state variable with the address
-of the ZetaChain gateway contract.
-
-### Handling Incoming Cross-Chain Calls
-
-The `onCall` function is a special handler that gets triggered when the contract
-receives a call from a connected chain through the gateway. This function
-processes the incoming data, which includes:
+A universal contract must implement the `onCall` function. `onCall` is a
+function that gets triggered when the contract receives a call from a connected
+chain through the Gateway. This function processes the incoming data, which
+includes:
- `context`: A `MessageContext` struct containing:
- - `origin`: The address (EOA or contract) that initiated the gateway call on
- the connected chain.
- `chainID`: The integer ID of the connected chain from which the cross-chain
call originated.
- - `sender`: Reserved for future use (currently empty).
+ - `sender`: The address (EOA or contract) that initiated the gateway call on
+ the connected chain.
+ - `origin`: deprecated.
- `zrc20`: The address of the ZRC-20 token representing the asset from the
source chain.
- `amount`: The number of tokens transferred.
- `message`: The encoded data payload.
-Within `onCall`, the contract decodes the `name` from the `message` and emits a
-`HelloEvent` to signal successful reception and processing of the message. It's
-important to note that `onCall` can only be invoked by the ZetaChain protocol,
-ensuring the integrity of cross-chain interactions.
-
-### Making an Outgoing Contract Call
-
-The `call` function demonstrates how a universal app can initiate a contract
-call to an arbitrary contract on a connected chain using the gateway. It
-operates as follows:
-
-1. **Calculate Gas Fee**: Determines the required gas fee based on the specified
- `gasLimit`. The gas limit represents the anticipated amount of gas needed to
- execute the contract on the destination chain.
-
-2. **Transfer Gas Fee**: Moves the calculated gas fee from the sender to the
- `Hello` contract. The user must grant the `Hello` contract permission to
- spend their gas fee tokens.
-
-3. **Approve Gateway**: Grants the gateway permission to spend the transferred
- gas fee.
+In this example, `onCall` simply decodes the message into a variable and emits
+an event.
-4. **Execute Cross-Chain Call**: Invokes `gateway.call` to initiate the contract
- call on the connected chain. The function selector and its arguments are
- encoded within the `message`. The gateway identifies the target chain based
- on the ZRC-20 token, as each chain's gas asset is associated with a specific
- ZRC-20 token.
+`onCall` should only be called by the Gateway to ensure that it is only called
+as a response to a call on a connected chain and that you can trust the values
+of the function parameters.
-### Making a Withdrawal and Call
-
-The `withdrawAndCall` function shows how a universal app can perform a token
-withdrawal with a call to an arbitrary contract on a connected chain using the
-gateway. The function executes the following steps:
-
-1. **Calculate Gas Fee**: Computes the necessary gas fee based on the provided
- `gasLimit`.
-
-2. **Transfer Tokens**: Moves the specified `amount` of tokens from the sender
- to the `Hello` contract. If the token being withdrawn is the gas token of the
- destination chain, the function transfers and approves both the gas fee and
- the withdrawal amount. If the target token is not the gas token, it transfers
- and approves the gas fee separately.
-
-3. **Approve Gateway**: Grants the gateway permission to spend the transferred
- tokens and gas fee.
-
-4. **Execute Withdrawal and Call**: Calls `gateway.withdrawAndCall` to withdraw
- the tokens and initiate the contract call on the connected chain.
-
-## EVM `Echo` Contract
-
-The `Echo` contract is a simple contract deployed on an EVM-compatible chain. It
-can be invoked by the `Hello` contract on ZetaChain to demonstrate cross-chain
-communication.
-
-```solidity filename="contracts/Echo.sol"
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.26;
-
-import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";
-import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
-
-contract Echo {
- GatewayEVM public immutable gateway;
-
- event RevertEvent(string, RevertContext);
- event HelloEvent(string, string);
-
- constructor(address payable gatewayAddress) {
- gateway = GatewayEVM(gatewayAddress);
- }
-
- function hello(string memory message) external payable {
- emit HelloEvent("Hello on EVM", message);
- }
-
- function onRevert(RevertContext calldata revertContext) external {
- emit RevertEvent("Revert on EVM", revertContext);
- }
-
- function call(
- address receiver,
- bytes calldata message,
- RevertOptions memory revertOptions
- ) external {
- gateway.call(receiver, message, revertOptions);
- }
-
- receive() external payable {}
-
- fallback() external payable {}
-}
-```
+## Options 1: Deploy on Localnet
-## Option 1: Deploy on Testnet
+[Localnet](/developers/tutorials/localnet) is a local development environment,
+which simulates the behavior of Gateways deployed on multiple EVM blockchains.
+Start localnet:
```
-npx hardhat compile --force
-npx hardhat deploy --gateway 0x6c533f7fe93fae114d0954697069df33c9b74fd7 --network zeta_testnet
-npx hardhat deploy --gateway 0x0c487a766110c85d301d96e33579c5b317fa4995 --network base_testnet --name Echo
+npx hardhat localnet
```
```
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-
-🚀 Successfully deployed "Hello" contract on zeta_testnet.
-📜 Contract address: 0x496CD66950a1F1c5B02513809A2d37fFB942be1B
-
-🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-
-🚀 Successfully deployed "Echo" contract on base_sepolia.
-📜 Contract address: 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed
+npx hardhat compile --force
```
-## Call from Base to ZetaChain
+Deploy the universal contract and pass ZetaChain's Gateway address to the
+constructor. You can find the Gateway address in the output of Localnet.
```
-npx hardhat echo-call \
- --contract 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
- --receiver 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
- --network base_sepolia \
- --tx-options-gas-price 20000 --types '["string"]' alice
+npx hardhat deploy --network localhost --gateway 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
```
-https://sepolia.basescan.org/tx/0x133cdf3a06195de0a6bb89dd4761ca98d1301534b3c4987f0ff93c95c3fff78c
-
-https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x133cdf3a06195de0a6bb89dd4761ca98d1301534b3c4987f0ff93c95c3fff78c
-
-## Call from ZetaChain to Base
-
```
-npx hardhat hello-call \
- --contract 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
- --receiver 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
- --zrc20 0x236b0DE675cC8F46AE186897fCCeFe3370C9eDeD \
- --function "hello(string)" \
- --network zeta_testnet \
- --tx-options-gas-price 200000 --types '["string"]' alice
+🚀 Successfully deployed "Universal" contract on localhost.
+📜 Contract address: 0xc351628EB244ec633d5f21fBD6621e1a683B1181
```
-https://zetachain-testnet.blockscout.com/tx/0x19d476fa2c3d29ba41467ae7f2742541fd11e0b67d6548fe7655a3d40274323e
-
-https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x19d476fa2c3d29ba41467ae7f2742541fd11e0b67d6548fe7655a3d40274323e
+## Make a Call to the Universal App
-## Withdraw And Call from ZetaChain to Base
+To call the universal app deployed on ZetaChain from a connected chain, make a
+call to the Gateway contract on a connected EVM chain using the `evm-call` task:
```
-npx hardhat hello-withdraw-and-call \
- --contract 0x496CD66950a1F1c5B02513809A2d37fFB942be1B \
- --receiver 0x775329c70A8d09AEb5e5ca92C369FF3155C5f1Ed \
- --zrc20 0x236b0DE675cC8F46AE186897fCCeFe3370C9eDeD \
- --function "hello(string)" \
- --amount 0.005 \
- --network zeta_testnet \
- --types '["string"]' hello
+npx hardhat evm-call \
+ --network localhost \
+ --gateway-evm 0x610178dA211FEF7D417bC0e6FeD39F05609AD788 \
+ --receiver 0xc351628EB244ec633d5f21fBD6621e1a683B1181 \
+ --types '["string"]' alice
```
-https://zetachain-testnet.blockscout.com/tx/0x67099389ab6cb44ee03602d164320c615720b57762c5ddab042d65bdbe30c7a2
+Pass the EVM Gateway Address from the Localnet's table of addresses and the
+universal contract address as the receiver. A universal app expects to receive a
+single string in the message, so pass the appropriate type and value to the
+command.
-https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x67099389ab6cb44ee03602d164320c615720b57762c5ddab042d65bdbe30c7a2
+After the transaction is processed you will see an `[ZetaChain]: Event from onCall` message in the terminal where Localnet is running.
-## Option 2: Start Localnet
+## Option 2: Deploy on Testnet
-[Localnet](/developers/tutorials/localnet) provides a simulated environment for
-developing and testing ZetaChain contracts locally.
+Before proceeding, you might want to check out the [Testnet
+Setup](/developers/tutorials/testnet) guide to learn how to set up an account
+and request testnet tokens.
-To start localnet, open a terminal window and run:
+Deploy the contract to ZetaChain's testnet using the Gateway address from
+[`Contract Addresses page`](/reference/network/contracts/).
-```bash
-npx hardhat localnet
```
-
-This command initializes a local blockchain environment that simulates the
-behavior of ZetaChain protocol contracts.
-
-## Deploying the Contracts
-
-With localnet running, open a new terminal window to compile and deploy the
-`Hello` and `Echo` contracts:
-
-```bash
-yarn deploy:localnet
+npx hardhat deploy --network zeta_testnet --gateway 0x6c533f7fe93fae114d0954697069df33c9b74fd7
```
-You should see output indicating the successful deployment of the contracts:
-
```
-🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
-
-🚀 Successfully deployed "Hello" contract on localhost.
-📜 Contract address: 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB
-
-🔑 Using account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
+🔑 Using account: 0x4955a3F38ff86ae92A914445099caa8eA2B9bA32
-🚀 Successfully deployed "Echo" contract on localhost.
-📜 Contract address: 0x9E545E3C0baAB3E08CdfD552C960A1050f373042
+🚀 Successfully deployed "Universal" contract on zeta_testnet.
+📜 Contract address: 0x11998e1A5D2e770753263376ceE78B14c9617f16
```
-**Note**: The deployed contract addresses may differ in your environment.
-
-## Calling the `Echo` Contract from `Hello`
+Make a transaction to the Gateway on the Base Sepolia testnet to make a
+cross-chain call to the universal app on ZetaChain. Make sure to use Gateway
+address on Base Sepolia.
-In this example, you'll invoke the `Echo` contract on the connected EVM chain,
-which in turn calls the `Hello` contract on ZetaChain. Run the following
-command, replacing the contract addresses with those from your deployment:
-
-```bash
-npx hardhat echo-call \
- --contract 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
- --receiver 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
- --network localhost \
- --types '["string"]' alice
```
-
-**Parameters:**
-
-- `--contract`: Address of the `Echo` contract on the connected EVM chain.
-- `--receiver`: Address of the `Hello` contract on ZetaChain.
-- `--network`: Network to interact with (`localhost` for localnet).
-- `--types`: ABI types of the message parameters (e.g., `["string"]`).
-- `alice`: The message to send.
-
-**Overview:**
-
-- **EVM Chain**: Executes the `call` function of the `Echo` contract.
-- **EVM Chain**: The `call` function invokes `gateway.call`, emitting a `Called`
- event.
-- **ZetaChain**: The protocol detects the event and triggers the `Hello`
- contract's `onCall`.
-- **ZetaChain**: The `Hello` contract decodes the message and emits a
- `HelloEvent`.
-
-## Calling an EVM Contract from a Universal App
-
-Now, let's perform the reverse operation: calling a contract on a connected EVM
-chain from the `Hello` universal app on ZetaChain. Execute the following
-command, replacing the contract addresses and ZRC-20 token address with those
-from your deployment:
-
-```bash
-npx hardhat hello-call \
- --contract 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
- --receiver 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
- --zrc20 0x2ca7d64A7EFE2D62A725E2B35Cf7230D6677FfEe \
- --function "hello(string)" \
- --network localhost \
+npx hardhat evm-call \
+ --network base_sepolia \
+ --gateway-evm 0x0c487a766110c85d301d96e33579c5b317fa4995 \
+ --receiver 0x11998e1A5D2e770753263376ceE78B14c9617f16 \
--types '["string"]' alice
```
-**Parameters:**
-
-- `--contract`: Address of the `Hello` contract on ZetaChain.
-- `--receiver`: Address of the `Echo` contract on the connected EVM chain.
-- `--zrc20`: Address of the ZRC-20 token representing the gas token of the
- connected chain. This determines the destination chain.
-- `--function`: Function signature to invoke on the `Echo` contract (e.g.,
- `"hello(string)"`).
-- `--network`: Network to interact with (`localhost` for localnet).
-- `--types`: ABI types of the message parameters.
-- `alice`: The message to send.
-
-## Withdrawing and Calling an EVM Contract from a Universal App
-
-To withdraw tokens and call a contract on a connected chain from a universal
-app, run the following command:
-
-```bash
-npx hardhat hello-withdraw-and-call \
- --contract 0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB \
- --receiver 0x9E545E3C0baAB3E08CdfD552C960A1050f373042 \
- --zrc20 0x9fd96203f7b22bCF72d9DCb40ff98302376cE09c \
- --function "hello(string)" \
- --amount 1 \
- --network localhost \
- --types '["string"]' hello
+```
+Transaction hash: 0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2
```
-**Parameters:**
-
-- `--contract`: Address of the `Hello` contract on ZetaChain.
-- `--receiver`: Address of the `Echo` contract on the connected EVM chain.
-- `--zrc20`: Address of the ZRC-20 token representing the asset to withdraw.
-- `--function`: Function signature to invoke on the `Echo` contract.
-- `--amount`: Amount of tokens to withdraw.
-- `--network`: Network to interact with.
-- `--types`: ABI types of the message parameters.
-- `hello`: The message to send.
-
-## Conclusion
-
-In this tutorial, you've:
-
-- Defined a universal app contract (`Hello`) to handle cross-chain messages.
-- Deployed both `Hello` and `Echo` contracts to a local development network.
-- Interacted with the `Hello` contract by sending messages from a connected EVM
- chain via the `Echo` contract.
-- Simulated revert scenarios and handled them gracefully using revert logic in
- both contracts.
-
-By mastering cross-chain calls and revert handling, you're now prepared to build
-robust and resilient universal applications on ZetaChain.
+https://sepolia.basescan.org/tx/0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2
-## Source Code
+You can track the progress of a cross-chain transaction using ZetaChain's API:
-You can find the complete source code for this tutorial in the [example
-contracts
-repository](https://github.com/zeta-chain/example-contracts/tree/main/examples/hello).
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x3fbed0a3dc48eda00212aff6e930940d84b142f7da316b9186f0c68edd0793b2
diff --git a/src/pages/developers/tutorials/intro.mdx b/src/pages/developers/tutorials/intro.mdx
index e4eef098e..cfd7e1a4f 100644
--- a/src/pages/developers/tutorials/intro.mdx
+++ b/src/pages/developers/tutorials/intro.mdx
@@ -1,144 +1,41 @@
-# Getting Started
-
-This introductory tutorial will guide you through the essential steps to set up
-your smart contract template, create and configure your account, and request
-tokens for testing on ZetaChain.
-
-## Prerequisites
-
-- A Unix-like environment (for example, macOS)
-- [Node.js](https://nodejs.org/en/) (version 18 or above)
-- [Yarn](https://yarnpkg.com/)
-- [Git](https://git-scm.com/)
-
-## Universal App Template
-
-ZetaChain provides [a smart contract
-template](https://github.com/zeta-chain/template) to help you start building
-universal apps quickly. First, clone the template from GitHub:
-
-```
-git clone https://github.com/zeta-chain/template
-```
-
-Next, navigate to the template directory and install the necessary dependencies:
-
-```
-cd template/contracts
-yarn
-```
-
-The template uses [Hardhat](https://hardhat.org/) for compiling, testing, and
-deploying contracts. It also uses
-[`@zetachain/toolkit`](https://github.com/zeta-chain/toolkit/), which offers
-various utilities for creating contracts, querying balances, tracking
-cross-chain transactions, accessing the faucet, and more. The template exposes
-most of the features available in the toolkit through Hardhat tasks.
-
-## Create an Account
-
-To interact with a blockchain, you'll need an account. If you don't have an
-account, you can generate a new one using the following command:
-
-```
-npx hardhat account --save
-```
-
-This command creates a random EVM wallet and a Solana wallet, and displays the wallet information in the
-terminal, and saves the private key to a `.env` file, making it accessible to
-Hardhat.
-
-If you don't want to save the wallet (for example, if you just need an address
-to send tokens to for testing purposes), you can run the command without the
-`--save` flag.
-
-If you already have a private key or mnemonic, you can import it with the
-`--recover` flag:
-
-```
-npx hardhat account --save --recover
-```
-
-For EVM wallet, this will prompt you to enter the private key or mnemonic, derive the addresses,
-and save the private key into the `.env` file.
-
-For Solana wallets, the process is as follows:
-1. It first attempts to automatically import the private key from the `~/.config/solana/id.json` file.
-2. If this file doesn't exist or can't be read, you'll be prompted to enter the private key manually.
-
-Finally, the private key is saved into the `.env` file.
-
-The `account` command shows derived addresses in hexadecimal (for EVM-based
-chains), bech32 with `zeta` prefix for ZetaChain, base58 for Solana, and bech32 for Bitcoin.
-
-## Query Token Balances
-
-To check the token balances:
-
-```
-npx hardhat balances
-```
-
-This command queries token balances for the account address derived from the
-private key specified in the `.env`.
-
-If you need to query for balances as part of a script, you can also use a
-`--json` flag to output the balances in JSON format:
-
-```
-npx hardhat balances --json
-```
-
-If you want to query for token balances for a different account, you can use the
-`--evm`, `--solana`, or `--bitcoin` flag:
-
-```
-npx hardhat balances --evm EVM_ADDRESS --solana SOLANA_ADDRESS --bitcoin BITCOIN_ADDRESS
-```
-
-The `balances` command supports querying for native gas tokens, wrapped ZETA on
-all connected chains as well as ZetaChain, ZRC-20 tokens, SOL on Solana, and BTC on Bitcoin.
-
-## Request Tokens from the Faucet
-
-To request ZETA tokens from the faucet:
-
-```
-npx hardhat faucet
-```
-
-This command requests tokens from the faucet for the account address derived
-from the private key specified in the `.env`. To prevent abuse, the faucet will
-prompt you to sign in with GitHub. Once the process is complete, you will
-receive native ZETA tokens on ZetaChain testnet. It may take a few minutes for
-your request to be processed.
-
-To send tokens to a different address, use:
-
-```
-npx hardhat faucet --address ADDRESS
-```
-
-Alternatively, you can install a standalone faucet CLI:
-
-```
-yarn global add @zetachain/faucet-cli
-```
-
-You can then use it with the following command:
-
-```
-zetafaucet -h
-```
-
-If the faucet is throwing an error, delete the `access_token` file from the
-project and try again.
-
-In addition to ZETA tokens on ZetaChain, you might need tokens on connected
-blockchains like Ethereun, BNB and Bitcoin testnets.
-
-- https://www.bnbchain.org/en/testnet-faucet
-- https://www.alchemy.com/faucets/ethereum-sepolia
-- https://www.infura.io/faucet/sepolia
-- https://bitcoinfaucet.uo1.net/
-- https://cloud.google.com/application/web3/faucet/ethereum/sepolia
+ZetaChain is a blockchain designed for universal apps—smart contracts on the
+ZetaChain EVM that are natively connected to other blockchains like Ethereum,
+BNB, Solana, and Bitcoin.
+
+A universal app can accept incoming contract calls, messages, and token
+transfers from connected chains and initiate outgoing transactions to those
+chains.
+
+Each chain, including ZetaChain, has a Gateway. On ZetaChain and EVM chains, the
+Gateway is a smart contract; on Solana, it is a program; and on Bitcoin, it is
+an address. All interactions with universal apps happen through Gateways.
+
+ZetaChain acts as a hub in the hub-spoke-model and developers build universal
+apps that can orchestrate cross-chain transactions. This architecture allows
+most of the app's logic to be encapsulated in a single universal contract.
+
+## Native Token Transfer
+
+Universal apps can accept transfers of supported native tokens from connected
+chains. For example, native USDC can be transferred (deposited) from Ethereum
+through the Gateway to a universal app on ZetaChain. Assets transferred from
+connected chains are represented as ZRC-20 tokens on ZetaChain. A ZRC-20 token
+can be transferred back (withdrawn) to its origin chain as a native asset. To
+facilitate native token transfers ZetaChain validators maintain custody of
+tokens on every connected chain. Tokens get locked in the custody when being
+transferred to ZetaChain and get unlocked when being transferred from ZetaChain.
+
+There are ZRC-20 liquidity pools on ZetaChain, which can be used by universal
+apps to swap between ZRC-20s and other tokens, to acquire ZRC-20 to pay for gas
+for outgoing transactions and by the ZetaChain protocol to handle gas when
+processing reverts.
+
+## Gas Fees
+
+Making an incoming call or a deposit to a universal app only costs a gas fee on
+the connected chain from which the call is made. Execution of universal apps is
+free when called from a connected chain.
+
+Making an outgoing call or a withdrawal from a universal app costs a gas fee on
+the destination chain. The Gateway charges a universal app making a call in
+ZRC-20 token. To cover the gas fee a universal app can swap tokens
diff --git a/src/pages/developers/tutorials/nft.mdx b/src/pages/developers/tutorials/nft.mdx
index b26044580..d00d182e3 100644
--- a/src/pages/developers/tutorials/nft.mdx
+++ b/src/pages/developers/tutorials/nft.mdx
@@ -54,11 +54,9 @@ a list of "counterparty" contracts on connected chains. This ensures that only
the contracts from the same NFT collection can participate in the cross-chain
transfer.
-
- {" "}
- This tutorial uses the authenticated calls feature of the Gateway, which is currently available only on a pre-release (v4.0.0-rc*)
- version of localnet, which is installed by default in this example.{" "}
-
+
+
+This tutorial relies on the Gateway, which is currently available only on localnet and testnet.
## Set Up Your Environment
@@ -80,48 +78,67 @@ pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
-import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
+import "@openzeppelin/contracts/access/Ownable2Step.sol";
import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol";
import {SystemContract} from "@zetachain/toolkit/contracts/SystemContract.sol";
+import "./shared/Events.sol";
contract Universal is
ERC721,
ERC721Enumerable,
ERC721URIStorage,
- Ownable,
- UniversalContract
+ Ownable2Step,
+ UniversalContract,
+ Events
{
GatewayZEVM public immutable gateway;
- SystemContract public immutable systemContract =
- SystemContract(0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9);
+ address public immutable uniswapRouter;
uint256 private _nextTokenId;
bool public isUniversal = true;
- uint256 public gasLimit = 700000;
+ uint256 public immutable gasLimit;
error TransferFailed();
+ error Unauthorized();
+ error InvalidAddress();
+ error InvalidGasLimit();
- mapping(address => bytes) public counterparty;
+ mapping(address => address) public connected;
- event CounterpartySet(address indexed zrc20, bytes indexed contractAddress);
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
constructor(
address payable gatewayAddress,
- address initialOwner
- ) ERC721("MyToken", "MTK") Ownable(initialOwner) {
+ address owner,
+ string memory name,
+ string memory symbol,
+ uint256 gas,
+ address uniswapRouterAddress
+ ) ERC721(name, symbol) Ownable(owner) {
+ if (
+ gatewayAddress == address(0) ||
+ owner == address(0) ||
+ uniswapRouterAddress == address(0)
+ ) revert InvalidAddress();
+ if (gas == 0) revert InvalidGasLimit();
gateway = GatewayZEVM(gatewayAddress);
+ uniswapRouter = uniswapRouterAddress;
+ gasLimit = gas;
}
- function setCounterparty(
+ function setConnected(
address zrc20,
- bytes memory contractAddress
+ address contractAddress
) external onlyOwner {
- counterparty[zrc20] = contractAddress;
- emit CounterpartySet(zrc20, contractAddress);
+ connected[zrc20] = contractAddress;
+ emit SetConnected(zrc20, contractAddress);
}
function transferCrossChain(
@@ -129,6 +146,7 @@ contract Universal is
address receiver,
address destination
) public {
+ if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);
@@ -139,7 +157,13 @@ contract Universal is
!IZRC20(destination).transferFrom(msg.sender, address(this), gasFee)
) revert TransferFailed();
IZRC20(destination).approve(address(gateway), gasFee);
- bytes memory encodedData = abi.encode(tokenId, receiver, uri);
+ bytes memory message = abi.encode(
+ receiver,
+ tokenId,
+ uri,
+ 0,
+ msg.sender
+ );
CallOptions memory callOptions = CallOptions(gasLimit, false);
@@ -147,17 +171,19 @@ contract Universal is
address(this),
true,
address(0),
- encodedData,
+ abi.encode(tokenId, uri, msg.sender),
gasLimit
);
gateway.call(
- counterparty[destination],
+ abi.encodePacked(connected[destination]),
destination,
- encodedData,
+ message,
callOptions,
revertOptions
);
+
+ emit TokenTransfer(receiver, destination, tokenId, uri);
}
function safeMint(address to, string memory uri) public onlyOwner {
@@ -178,52 +204,62 @@ contract Universal is
address zrc20,
uint256 amount,
bytes calldata message
- ) external override {
- if (keccak256(context.origin) != keccak256(counterparty[zrc20]))
- revert("Unauthorized");
+ ) external override onlyGateway {
+ if (context.sender != connected[zrc20]) revert Unauthorized();
(
+ address destination,
+ address receiver,
uint256 tokenId,
- address sender,
string memory uri,
- address destination
- ) = abi.decode(message, (uint256, address, string, address));
+ address sender
+ ) = abi.decode(message, (address, address, uint256, string, address));
if (destination == address(0)) {
- _safeMint(sender, tokenId);
+ _safeMint(receiver, tokenId);
_setTokenURI(tokenId, uri);
+ emit TokenTransferReceived(receiver, tokenId, uri);
} else {
(, uint256 gasFee) = IZRC20(destination).withdrawGasFeeWithGasLimit(
- 700000
+ gasLimit
);
- SwapHelperLib.swapExactTokensForTokens(
- systemContract,
+ uint256 out = SwapHelperLib.swapExactTokensForTokens(
+ uniswapRouter,
zrc20,
amount,
destination,
0
);
- IZRC20(destination).approve(address(gateway), gasFee);
- gateway.call(
- counterparty[destination],
+ IZRC20(destination).approve(address(gateway), out);
+ gateway.withdrawAndCall(
+ abi.encodePacked(connected[destination]),
+ out - gasFee,
destination,
- abi.encode(tokenId, sender, uri),
- CallOptions(700000, false),
- RevertOptions(address(0), false, address(0), "", 0)
+ abi.encode(receiver, tokenId, uri, out - gasFee, sender),
+ CallOptions(gasLimit, false),
+ RevertOptions(
+ address(this),
+ true,
+ address(0),
+ abi.encode(tokenId, uri, sender),
+ 0
+ )
);
}
+ emit TokenTransferToDestination(receiver, destination, tokenId, uri);
}
- function onRevert(RevertContext calldata context) external {
- (uint256 tokenId, address sender, string memory uri) = abi.decode(
+ function onRevert(RevertContext calldata context) external onlyGateway {
+ (uint256 tokenId, string memory uri, address sender) = abi.decode(
context.revertMessage,
- (uint256, address, string)
+ (uint256, string, address)
);
_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
+ emit TokenTransferReverted(sender, tokenId, uri);
}
// The following functions are overrides required by Solidity.
@@ -344,24 +380,51 @@ pragma solidity 0.8.26;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
-import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
+import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";
+import "./shared/Events.sol";
-contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
+contract Connected is
+ ERC721,
+ ERC721Enumerable,
+ ERC721URIStorage,
+ Ownable2Step,
+ Events
+{
GatewayEVM public immutable gateway;
uint256 private _nextTokenId;
- address public counterparty;
+ address public universal;
+ uint256 public immutable gasLimit;
+
+ error InvalidAddress();
+ error Unauthorized();
+ error InvalidGasLimit();
+ error GasTokenTransferFailed();
+
+ function setUniversal(address contractAddress) external onlyOwner {
+ if (contractAddress == address(0)) revert InvalidAddress();
+ universal = contractAddress;
+ emit SetUniversal(contractAddress);
+ }
- function setCounterparty(address contractAddress) external onlyOwner {
- counterparty = contractAddress;
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
}
constructor(
address payable gatewayAddress,
- address initialOwner
- ) ERC721("MyToken", "MTK") Ownable(initialOwner) {
+ address owner,
+ string memory name,
+ string memory symbol,
+ uint256 gas
+ ) ERC721(name, symbol) Ownable(owner) {
+ if (gatewayAddress == address(0) || owner == address(0))
+ revert InvalidAddress();
+ if (gas == 0) revert InvalidGasLimit();
+ gasLimit = gas;
gateway = GatewayEVM(gatewayAddress);
}
@@ -376,6 +439,7 @@ contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
+ emit TokenMinted(to, tokenId, uri);
}
function transferCrossChain(
@@ -383,57 +447,73 @@ contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
address receiver,
address destination
) external payable {
+ if (receiver == address(0)) revert InvalidAddress();
+
string memory uri = tokenURI(tokenId);
_burn(tokenId);
- bytes memory encodedData = abi.encode(
- tokenId,
+ bytes memory message = abi.encode(
+ destination,
receiver,
+ tokenId,
uri,
- destination
- );
-
- RevertOptions memory revertOptions = RevertOptions(
- address(this),
- true,
- address(0),
- encodedData,
- 0
+ msg.sender
);
-
if (destination == address(0)) {
- gateway.call(counterparty, encodedData, revertOptions);
+ gateway.call(
+ universal,
+ message,
+ RevertOptions(address(this), false, address(0), message, 0)
+ );
} else {
gateway.depositAndCall{value: msg.value}(
- counterparty,
- encodedData,
- revertOptions
+ universal,
+ message,
+ RevertOptions(
+ address(this),
+ true,
+ address(0),
+ abi.encode(receiver, tokenId, uri, msg.sender),
+ gasLimit
+ )
);
}
+
+ emit TokenTransfer(destination, receiver, tokenId, uri);
}
function onCall(
- MessageContext calldata messageContext,
+ MessageContext calldata context,
bytes calldata message
- ) external payable returns (bytes4) {
- if (messageContext.sender != counterparty) revert("Unauthorized");
+ ) external payable onlyGateway returns (bytes4) {
+ if (context.sender != universal) revert Unauthorized();
+
+ (
+ address receiver,
+ uint256 tokenId,
+ string memory uri,
+ uint256 gasAmount,
+ address sender
+ ) = abi.decode(message, (address, uint256, string, uint256, address));
- (uint256 tokenId, address receiver, string memory uri) = abi.decode(
- message,
- (uint256, address, string)
- );
_safeMint(receiver, tokenId);
_setTokenURI(tokenId, uri);
+ if (gasAmount > 0) {
+ (bool success, ) = sender.call{value: gasAmount}("");
+ if (!success) revert GasTokenTransferFailed();
+ }
+ emit TokenTransferReceived(receiver, tokenId, uri);
return "";
}
- function onRevert(RevertContext calldata context) external {
- (uint256 tokenId, address sender, string memory uri) = abi.decode(
+ function onRevert(RevertContext calldata context) external onlyGateway {
+ (, uint256 tokenId, string memory uri, address sender) = abi.decode(
context.revertMessage,
- (uint256, address, string)
+ (address, uint256, string, address)
);
_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
+ emit TokenTransferReverted(sender, tokenId, uri);
}
receive() external payable {}
@@ -476,10 +556,6 @@ contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
}
```
-```
-./scripts/test.sh
-```
-
### Transfer NFT from a Connected Chain
`transferCrossChain` initiates a transfer of an NFT to ZetaChain or to another
@@ -512,7 +588,7 @@ contract address to prevent unauthorized minting.
If an NFT transfer from ZetaChain to a connected chain fails, `onRevert` is
called. `onRevert` mints the NFT and transfers it back to original sender.
-## Start Localnet
+## Option 1: Deploy on Localnet
[Localnet](/developers/tutorials/localnet) provides a simulated environment for
developing and testing ZetaChain contracts locally.
@@ -533,7 +609,7 @@ counterparty addresses, mint an NFT and transfer it ZetaChain → Ethereum → B
ZetaChain:
```
-./scripts/test.sh
+./scripts/localnet.sh
```
On localnet Ethereum, BNB, and ZetaChain are all running on the same Anvil
@@ -595,3 +671,96 @@ Transferring NFT: BNB → ZetaChain...
🟡 BNB Chain: 0
---------------------------------------------
```
+
+## Option 2: Deploy on Testnet
+
+Before proceeding, you might want to check out the [Testnet
+Setup](/developers/tutorials/testnet) guide to learn how to set up an account
+and request testnet tokens.
+
+To launch a universal NFT, deploy the Universal contract to ZetaChain and the
+Connected contract to one or more connected chains, and set counterparty
+addresses. To set everything up automatically run `./scripts/testnet.sh` or
+follow the steps below to do it manually.
+
+```
+npx hardhat compile --force --quiet
+```
+
+```
+npx hardhat deploy --name Universal --network zeta_testnet --gateway 0x6c533f7fe93fae114d0954697069df33c9b74fd7 --system-contract 0xEdf1c3275d13489aCdC6cD6eD246E72458B8795B --gas-limit 500000 --json
+
+UNIVERSAL=0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1
+```
+
+```
+npx hardhat deploy --name Connected --network base_sepolia --gateway 0x0c487a766110c85d301d96e33579c5b317fa4995 --gas-limit 500000 --json
+
+CONNECTED_BASE=0x0A6DdaA0C1592593632EbE96B02a1305B6aA72c8
+```
+
+```
+npx hardhat deploy --name Connected --network bsc_testnet --gateway 0x0c487a766110c85d301d96e33579c5b317fa4995 --gas-limit 500000 --json
+
+CONNECTED_BNB=0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7
+```
+
+```
+npx hardhat connected-set-counterparty --network base_sepolia --contract 0x0A6DdaA0C1592593632EbE96B02a1305B6aA72c8 --counterparty 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --json
+```
+
+```
+npx hardhat connected-set-counterparty --network bsc_testnet --contract 0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7 --counterparty 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --json
+```
+
+```
+npx hardhat universal-set-counterparty --network zeta_testnet --contract 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --counterparty 0x0A6DdaA0C1592593632EbE96B02a1305B6aA72c8 --zrc20 0x236b0DE675cC8F46AE186897fCCeFe3370C9eDeD --json
+```
+
+```
+npx hardhat universal-set-counterparty --network zeta_testnet --contract 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --counterparty 0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7 --zrc20 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891 --json
+```
+
+Mint a universal NFT on ZetaChain:
+
+```
+npx hardhat mint --network zeta_testnet --json --contract 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --token-uri https://example.com/nft/metadata/1
+
+{"contractAddress":"0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1","mintTransactionHash":"0xf2544827e5ac4434a300bca190f6b5e391e3cd7c87e515ac2dee4f7cb3b14e44","recipient":"0x4955a3F38ff86ae92A914445099caa8eA2B9bA32","tokenURI":"https://example.com/nft/metadata/1","tokenId":"1145931601449361378070955209842677114337257109052"}
+```
+
+Transfer the NFT from ZetaChain to BNB testnet (represented by its ZRC-20 gas
+token address `0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891`):
+
+```
+npx hardhat transfer --network zeta_testnet --json --token-id 1145931601449361378070955209842677114337257109052 --from 0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1 --to 0xd97B1de3619ed2c6BEb3860147E30cA8A7dC9891
+```
+
+```
+{"contractAddress":"0x83E60C63b38974Cfe89c287f2b9ddF153eBD13A1","transferTransactionHash":"0x418ebe5cd999c19ec4a93a2295f25508728c655fb95bc5773c366a956ceac842","sender":"0x4955a3F38ff86ae92A914445099caa8eA2B9bA32","tokenId":"1145931601449361378070955209842677114337257109052"}
+```
+
+Notice that the transaction has been processed on ZetaChain:
+
+https://zetachain-testnet.blockscout.com/tx/0x418ebe5cd999c19ec4a93a2295f25508728c655fb95bc5773c366a956ceac842
+
+You can track the cross-chain transaction (CCTX) progress using the API:
+
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x418ebe5cd999c19ec4a93a2295f25508728c655fb95bc5773c366a956ceac842
+
+After the CCTX has been processed, you will see the NFT with the same token ID
+in the recipient's address on the BNB testnet.
+
+You can send the NFT back from BNB testnet to ZetaChain:
+
+```
+npx hardhat transfer --network bsc_testnet --json --token-id 1145931601449361378070955209842677114337257109052 --from 0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7
+```
+
+```
+{"contractAddress":"0x48D512595699A1c1c40C7B5Fc378512Ab0dCFAd7","transferTransactionHash":"0x4459aa5369f7a1c5caf8f7eb26082ff5699cffec7748e95145030df7493ffe17","sender":"0x4955a3F38ff86ae92A914445099caa8eA2B9bA32","tokenId":"1145931601449361378070955209842677114337257109052"}
+```
+
+https://testnet.bscscan.com/tx/0x4459aa5369f7a1c5caf8f7eb26082ff5699cffec7748e95145030df7493ffe17
+
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData/0x4459aa5369f7a1c5caf8f7eb26082ff5699cffec7748e95145030df7493ffe17
diff --git a/src/pages/developers/tutorials/swap-any.mdx b/src/pages/developers/tutorials/swap-any.mdx
index 19fca036f..bd7e73a94 100644
--- a/src/pages/developers/tutorials/swap-any.mdx
+++ b/src/pages/developers/tutorials/swap-any.mdx
@@ -61,12 +61,22 @@ import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IWZETA.sol";
import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
contract SwapToAnyToken is UniversalContract {
- SystemContract public systemContract;
+ address public immutable uniswapRouter;
GatewayZEVM public gateway;
uint256 constant BITCOIN = 18332;
- constructor(address systemContractAddress, address payable gatewayAddress) {
- systemContract = SystemContract(systemContractAddress);
+ error InvalidAddress();
+ error Unauthorized();
+
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
+
+ constructor(address payable gatewayAddress, address uniswapRouterAddress) {
+ if (gatewayAddress == address(0) || uniswapRouterAddress == address(0))
+ revert InvalidAddress();
+ uniswapRouter = uniswapRouterAddress;
gateway = GatewayZEVM(gatewayAddress);
}
@@ -81,7 +91,7 @@ contract SwapToAnyToken is UniversalContract {
address zrc20,
uint256 amount,
bytes calldata message
- ) external virtual override {
+ ) external onlyGateway {
Params memory params = Params({
target: address(0),
to: bytes(""),
@@ -135,7 +145,7 @@ contract SwapToAnyToken is UniversalContract {
swapAmount = amount - gasFee;
} else {
inputForGas = SwapHelperLib.swapTokensForExactTokens(
- systemContract,
+ uniswapRouter,
inputToken,
gasFee,
gasZRC20,
@@ -146,7 +156,7 @@ contract SwapToAnyToken is UniversalContract {
}
uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
- systemContract,
+ uniswapRouter,
inputToken,
swapAmount,
targetToken,
@@ -195,7 +205,9 @@ contract SwapToAnyToken is UniversalContract {
swapAndWithdraw(inputToken, amount, targetToken, recipient, withdraw);
}
- function onRevert(RevertContext calldata revertContext) external override {}
+ function onRevert(
+ RevertContext calldata revertContext
+ ) external onlyGateway {}
}
```
diff --git a/src/pages/developers/tutorials/swap.mdx b/src/pages/developers/tutorials/swap.mdx
index 8d8287971..ae2e0310a 100644
--- a/src/pages/developers/tutorials/swap.mdx
+++ b/src/pages/developers/tutorials/swap.mdx
@@ -78,12 +78,22 @@ import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol
import {GatewayZEVM} from "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
contract Swap is UniversalContract {
- SystemContract public systemContract;
+ address public immutable uniswapRouter;
GatewayZEVM public gateway;
uint256 constant BITCOIN = 18332;
- constructor(address systemContractAddress, address payable gatewayAddress) {
- systemContract = SystemContract(systemContractAddress);
+ error InvalidAddress();
+ error Unauthorized();
+
+ modifier onlyGateway() {
+ if (msg.sender != address(gateway)) revert Unauthorized();
+ _;
+ }
+
+ constructor(address payable gatewayAddress, address uniswapRouterAddress) {
+ if (gatewayAddress == address(0) || uniswapRouterAddress == address(0))
+ revert InvalidAddress();
+ uniswapRouter = uniswapRouterAddress;
gateway = GatewayZEVM(gatewayAddress);
}
@@ -97,7 +107,7 @@ contract Swap is UniversalContract {
address zrc20,
uint256 amount,
bytes calldata message
- ) external override {
+ ) external onlyGateway {
Params memory params = Params({target: address(0), to: bytes("")});
if (context.chainID == BITCOIN) {
params.target = BytesHelperLib.bytesToAddress(message, 0);
@@ -133,7 +143,7 @@ contract Swap is UniversalContract {
swapAmount = amount - gasFee;
} else {
inputForGas = SwapHelperLib.swapTokensForExactTokens(
- systemContract,
+ uniswapRouter,
inputToken,
gasFee,
gasZRC20,
@@ -143,7 +153,7 @@ contract Swap is UniversalContract {
}
uint256 outputAmount = SwapHelperLib.swapExactTokensForTokens(
- systemContract,
+ uniswapRouter,
inputToken,
swapAmount,
targetToken,
@@ -171,7 +181,9 @@ contract Swap is UniversalContract {
);
}
- function onRevert(RevertContext calldata revertContext) external override {}
+ function onRevert(
+ RevertContext calldata revertContext
+ ) external onlyGateway {}
}
```
diff --git a/src/pages/developers/tutorials/testnet.mdx b/src/pages/developers/tutorials/testnet.mdx
new file mode 100644
index 000000000..12ee101ee
--- /dev/null
+++ b/src/pages/developers/tutorials/testnet.mdx
@@ -0,0 +1,140 @@
+# Getting Started
+
+This introductory tutorial will guide you through the essential steps to set up
+your smart contract template, create and configure your account, and request
+tokens for testing on ZetaChain.
+
+## Prerequisites
+
+- A Unix-like environment (for example, macOS)
+- [Node.js](https://nodejs.org/en/) (version 18 or above)
+- [Yarn](https://yarnpkg.com/)
+- [Git](https://git-scm.com/)
+
+## Set Up Your Environment
+
+Start by cloning the example contracts repository:
+
+```bash
+git clone https://github.com/zeta-chain/example-contracts
+```
+
+Change directory to any of the examples, like `hello` and install dependencies:
+
+```
+cd examples/hello
+yarn
+```
+
+## Create an Account
+
+To interact with a blockchain, you'll need an account. If you don't have an
+account, you can generate a new one using the following command:
+
+```
+npx hardhat account --save
+```
+
+This command creates a random EVM wallet and a Solana wallet, and displays the
+wallet information in the terminal, and saves the private key to a `.env` file,
+making it accessible to Hardhat.
+
+If you don't want to save the wallet (for example, if you just need an address
+to send tokens to for testing purposes), you can run the command without the
+`--save` flag.
+
+If you already have a private key or mnemonic, you can import it with the
+`--recover` flag:
+
+```
+npx hardhat account --save --recover
+```
+
+For EVM wallet, this will prompt you to enter the private key or mnemonic,
+derive the addresses, and save the private key into the `.env` file.
+
+For Solana wallets, the process is as follows:
+
+1. It first attempts to automatically import the private key from the
+ `~/.config/solana/id.json` file.
+2. If this file doesn't exist or can't be read, you'll be prompted to enter the
+ private key manually.
+
+Finally, the private key is saved into the `.env` file.
+
+The `account` command shows derived addresses in hexadecimal (for EVM-based
+chains), bech32 with `zeta` prefix for ZetaChain, base58 for Solana, and bech32
+for Bitcoin.
+
+## Query Token Balances
+
+To check the token balances:
+
+```
+npx hardhat balances
+```
+
+This command queries token balances for the account address derived from the
+private key specified in the `.env`.
+
+If you need to query for balances as part of a script, you can also use a
+`--json` flag to output the balances in JSON format:
+
+```
+npx hardhat balances --json
+```
+
+If you want to query for token balances for a different account, you can use the
+`--evm`, `--solana`, or `--bitcoin` flag:
+
+```
+npx hardhat balances --evm EVM_ADDRESS --solana SOLANA_ADDRESS --bitcoin BITCOIN_ADDRESS
+```
+
+The `balances` command supports querying for native gas tokens, wrapped ZETA on
+all connected chains as well as ZetaChain, ZRC-20 tokens, SOL on Solana, and BTC
+on Bitcoin.
+
+## Request Tokens from the Faucet
+
+To request ZETA tokens from the faucet:
+
+```
+npx hardhat faucet
+```
+
+This command requests tokens from the faucet for the account address derived
+from the private key specified in the `.env`. To prevent abuse, the faucet will
+prompt you to sign in with GitHub. Once the process is complete, you will
+receive native ZETA tokens on ZetaChain testnet. It may take a few minutes for
+your request to be processed.
+
+To send tokens to a different address, use:
+
+```
+npx hardhat faucet --address ADDRESS
+```
+
+Alternatively, you can install a standalone faucet CLI:
+
+```
+yarn global add @zetachain/faucet-cli
+```
+
+You can then use it with the following command:
+
+```
+zetafaucet -h
+```
+
+If the faucet is throwing an error, delete the `access_token` file from the
+project and try again.
+
+In addition to ZETA tokens on ZetaChain, you might need tokens on connected
+blockchains like Ethereun, BNB and Bitcoin testnets.
+
+- https://www.bnbchain.org/en/testnet-faucet
+- https://www.alchemy.com/faucets/ethereum-sepolia
+- https://www.infura.io/faucet/sepolia
+- https://bitcoinfaucet.uo1.net/
+- https://cloud.google.com/application/web3/faucet/ethereum/sepolia