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.{" "} - +![https://excalidraw.com/#json=dQJisu_uJ0N8T6IPi2m0E,PJU63ktFfbi1WsfAXsompA](/img/docs/tutorials-nft.png) + +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