diff --git a/docs/developers/omnichain/system-contract.md b/docs/developers/omnichain/system-contract.md
index e09fb032..a8354df1 100644
--- a/docs/developers/omnichain/system-contract.md
+++ b/docs/developers/omnichain/system-contract.md
@@ -12,7 +12,7 @@ information you may need in your protocol. You can import this code into your
project and instance with the deployed address. You can find the up-to-date
address of the system contract using the ZetaChain's API:
-https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/zetacore/fungible/system_contract
+https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/fungible/system_contract
In this contract you will find:
diff --git a/docs/developers/omnichain/tutorials/curve.md b/docs/developers/omnichain/tutorials/curve.md
index 11f9c84c..643c77b2 100644
--- a/docs/developers/omnichain/tutorials/curve.md
+++ b/docs/developers/omnichain/tutorials/curve.md
@@ -27,7 +27,7 @@ it take a look to official script, does all the work for you out of the box
[deployment script](https://github.com/curvefi/curve-contract/blob/master/scripts/deploy.py)),
using the address of three ZRC-2020 tokens. You can find the ZetaChain addresses
of ZRC-20 tokens supported right now on the ZetaChain testnet using this
-[endpoint](https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/zetacore/fungible/foreign_coins).
+[endpoint](https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/fungible/foreign_coins).
## Implement a cross-chain stableswap
diff --git a/docs/developers/omnichain/tutorials/hello.md b/docs/developers/omnichain/tutorials/hello.md
index bff1d5af..6fa0067a 100644
--- a/docs/developers/omnichain/tutorials/hello.md
+++ b/docs/developers/omnichain/tutorials/hello.md
@@ -98,27 +98,31 @@ The constructor function accepts the address of the system contract and stores
it in the `systemContract` state variable.
`onCrossChainCall` is a function that is called when the contract gets called by
-a token transfer transaction sent to the TSS address on a connected chain. The
-function receives the following inputs:
+a token transfer transaction sent to the TSS address on a connected chain (when
+a gas token is deposited) or a `deposit` method call on the ERC-20 custody
+contract (when an ERC-20 token is deposited). The function receives the
+following inputs:
- `context`: is a struct of type
[`zContext`](https://github.com/zeta-chain/protocol-contracts/blob/main/contracts/zevm/interfaces/zContract.sol)
that contains the following values:
- `origin`: EOA address that sent the token transfer transaction to the TSS
- address (triggering the omnichain contract)
+ address (triggering the omnichain contract) or the value passed to the
+ `deposit` method call on the ERC-20 custody contract.
- `chainID`: interger ID of the connected chain from which the omnichain
contract was triggered.
- `sender` (reserved for future use, currently empty)
- `zrc20`: the address of the ZRC-20 token contract that represents an asset
from a connected chain on ZetaChain.
-- `amount`: the amount of tokens that were transferred to the TSS address.
+- `amount`: the amount of tokens that were transferred to the TSS address or an
+ amount of tokens that were deposited to the ERC-20 custody contract.
- `message`: the contents of the `data` field of the token transfer transaction.
The `onCrossChainCall` function should only be called by the system contract (in
other words, by the ZetaChain protocol) to prevent a caller from supplying
arbitrary values in `context`. The `onlySystem` modifier ensures that the
function is called only as a response to a token transfer transaction sent to
-the TSS address.
+the TSS address or an ERC-20 custody contract.
By default, the `onCrossChainCall` function doesn't do anything else. You will
implement the logic yourself based on your use case.
@@ -183,18 +187,40 @@ contract:
```ts title="tasks/interact.ts"
import { task } from "hardhat/config";
import { HardhatRuntimeEnvironment } from "hardhat/types";
-import { parseEther } from "@ethersproject/units";
+import { parseUnits } from "@ethersproject/units";
import { getAddress } from "@zetachain/protocol-contracts";
+import ERC20Custody from "@zetachain/protocol-contracts/abi/evm/ERC20Custody.sol/ERC20Custody.json";
import { prepareData } from "@zetachain/toolkit/helpers";
+import { utils, ethers } from "ethers";
+import ERC20 from "@openzeppelin/contracts/build/contracts/ERC20.json";
const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
const [signer] = await hre.ethers.getSigners();
const data = prepareData(args.contract, [], []);
- const to = getAddress("tss", hre.network.name);
- const value = parseEther(args.amount);
- const tx = await signer.sendTransaction({ data, to, value });
+ let tx;
+
+ if (args.token) {
+ const custodyAddress = getAddress("erc20Custody", hre.network.name as any);
+ const custodyContract = new ethers.Contract(
+ custodyAddress,
+ ERC20Custody.abi,
+ signer
+ );
+ const tokenContract = new ethers.Contract(args.token, ERC20.abi, signer);
+ const decimals = await tokenContract.decimals();
+ const value = parseUnits(args.amount, decimals);
+ const approve = await tokenContract.approve(custodyAddress, value);
+ await approve.wait();
+
+ tx = await custodyContract.deposit(signer.address, args.token, value, data);
+ tx.wait();
+ } else {
+ const value = parseUnits(args.amount, 18);
+ const to = getAddress("tss", hre.network.name as any);
+ tx = await signer.sendTransaction({ data, to, value });
+ }
if (args.json) {
console.log(JSON.stringify(tx, null, 2));
@@ -203,13 +229,14 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
console.log(`🚀 Successfully broadcasted a token transfer transaction on ${hre.network.name} network.
📝 Transaction hash: ${tx.hash}
-`);
+ `);
}
};
task("interact", "Interact with the contract", main)
.addParam("contract", "The address of the withdraw contract on ZetaChain")
.addParam("amount", "Amount of tokens to send")
+ .addOptionalParam("token", "The address of the token to send")
.addFlag("json", "Output in JSON");
```
@@ -226,6 +253,30 @@ information:
In the code generated above there are no arguments, so the `data` field is
simply the address of the contract on ZetaChain.
+Calling omnichain contracts is differs depending on whether a gas token is being
+deposited or an ERC-20 token.
+
+If an ERC-20 token address is passed to the `--token` optional parameter, the
+interact task assumes you want to deposit an ERC-20 token in an omnichain
+contract.
+
+To deposit an ERC-20 token into an omnichain contract you need to call the
+`deposit` method of the ERC-20 custody contract. The task first gets the address
+of the custody contract on the current network, creates an instance of a token
+contract, gets the number of decimals of the token, and approves the custody
+contract to spend the specified amount of ERC-20 tokens. The task then calls the
+`deposit` method of the custody contract, passing the following information:
+
+- `signer.address`: the sender address that will be available in the `origin`
+ field of the `context` parameter of the `onCrossChainCall` function
+- `args.token`: the address of the ERC-20 token being deposited
+- `value`: the amount of tokens being deposited
+- `data`: the contents of the `message`
+
+If the `--token` optional parameter is not used, the interact task assumes you
+want to deposit a gas token. To deposit a gas token you need to send a token
+transfer transaction to the TSS address on a connected chain.
+
`getAddress` retrieves the address of the TSS on the current network.
The task then uses Ethers.js to send a token transfer transaction to the TSS
diff --git a/docs/developers/omnichain/tutorials/swap.md b/docs/developers/omnichain/tutorials/swap.md
index 651cc4d0..15f22778 100644
--- a/docs/developers/omnichain/tutorials/swap.md
+++ b/docs/developers/omnichain/tutorials/swap.md
@@ -7,13 +7,17 @@ sidebar_position: 4
## Overview
In this tutorial you will write a cross-chain swap contract that allows users to
-transfer native tokens from one of the connected chains to ZetaChain, swap them
-for a ZRC-20 representation of a token on another chain, and withdraw the tokens
-to the recipient address on the target chain.
+swap a native gas or ERC-20 token from one of the connected chains for a token
+on another chain.
-
+The swap process involves depositing a token from a connected chain to
+ZetaChain, triggering a swap omnichain contract to swap between ZRC-20
+representations of the tokens and then withdrawing the swapped token to the
+recipient address on the destination chain.
+
+
## Set Up Your Environment
@@ -53,11 +57,8 @@ import "@zetachain/toolkit/contracts/BytesHelperLib.sol";
contract Swap is zContract {
SystemContract public immutable systemContract;
- // highlight-start
+ // highlight-next-line
uint256 constant BITCOIN = 18332;
- error WrongGasContract();
- error NotEnoughToPayGasFee();
- // highlight-end
constructor(address systemContractAddress) {
systemContract = SystemContract(systemContractAddress);
@@ -95,27 +96,31 @@ contract Swap is zContract {
recipientAddress = recipient;
}
+ (address gasZRC20, uint256 gasFee) = IZRC20(targetTokenAddress)
+ .withdrawGasFee();
+
+ uint256 inputForGas = SwapHelperLib.swapTokensForExactTokens(
+ systemContract.wZetaContractAddress(),
+ systemContract.uniswapv2FactoryAddress(),
+ systemContract.uniswapv2Router02Address(),
+ zrc20,
+ gasFee,
+ gasZRC20,
+ amount
+ );
+
uint256 outputAmount = SwapHelperLib._doSwap(
systemContract.wZetaContractAddress(),
systemContract.uniswapv2FactoryAddress(),
systemContract.uniswapv2Router02Address(),
zrc20,
- amount,
+ amount - inputForGas,
targetTokenAddress,
0
);
- (address gasZRC20, uint256 gasFee) = IZRC20(targetTokenAddress)
- .withdrawGasFee();
-
- if (gasZRC20 != targetTokenAddress) revert WrongGasContract();
- if (gasFee >= outputAmount) revert NotEnoughToPayGasFee();
-
- IZRC20(targetTokenAddress).approve(targetTokenAddress, gasFee);
- IZRC20(targetTokenAddress).withdraw(
- recipientAddress,
- outputAmount - gasFee
- );
+ IZRC20(gasZRC20).approve(targetTokenAddress, gasFee);
+ IZRC20(targetTokenAddress).withdraw(recipientAddress, outputAmount);
// highlight-end
}
}
@@ -147,10 +152,16 @@ convert the address to `bytes`.
If it's an EVM chain, use `abi.decode` to decode the `message` into the
`targetToken` and `recipient` variables.
-Next, swap the incoming token for the gas coin on the destination chain.
-ZetaChain has liquidity pools with the ZRC-20 representation of the gas coin on
-all connected chains. The `SwapHelperLib._doSwap` helper method to swap the
-tokens.
+Next, get the gas fee and the gas coin address from the target token. The gas
+coin is the token that will be used to pay for the gas on the destination chain.
+
+Use the `SwapHelperLib.swapTokensForExactTokens` helper method to swap the
+incoming token for the gas coin using the internal liquidity pools. The method
+returns the amount of the incoming token that was used to pay for the gas.
+
+Next, swap the incoming amount minus the amount spent swapping for a gas fee for
+the target token on the destination chain using the `SwapHelperLib._doSwap`
+helper method.
Finally, withdraw the tokens to the recipient address on the destination chain.
@@ -176,10 +187,8 @@ import { HardhatRuntimeEnvironment } from "hardhat/types";
import { parseEther } from "@ethersproject/units";
import { getAddress } from "@zetachain/protocol-contracts";
import { prepareData } from "@zetachain/toolkit/helpers";
-// highlight-start
+// highlight-next-line
import bech32 from "bech32";
-import { utils } from "ethers";
-// highlight-end
const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
const [signer] = await hre.ethers.getSigners();
@@ -205,10 +214,29 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
[args.targetToken, recipient]
// highlight-end
);
- const to = getAddress("tss", hre.network.name);
- const value = parseEther(args.amount);
- const tx = await signer.sendTransaction({ data, to, value });
+ let tx;
+
+ if (args.token) {
+ const custodyAddress = getAddress("erc20Custody", hre.network.name as any);
+ const custodyContract = new ethers.Contract(
+ custodyAddress,
+ ERC20Custody.abi,
+ signer
+ );
+ const tokenContract = new ethers.Contract(args.token, ERC20.abi, signer);
+ const decimals = await tokenContract.decimals();
+ const value = parseUnits(args.amount, decimals);
+ const approve = await tokenContract.approve(custodyAddress, value);
+ await approve.wait();
+
+ tx = await custodyContract.deposit(signer.address, args.token, value, data);
+ tx.wait();
+ } else {
+ const value = parseUnits(args.amount, 18);
+ const to = getAddress("tss", hre.network.name as any);
+ tx = await signer.sendTransaction({ data, to, value });
+ }
if (args.json) {
console.log(JSON.stringify(tx, null, 2));
@@ -224,6 +252,7 @@ const main = async (args: any, hre: HardhatRuntimeEnvironment) => {
task("interact", "Interact with the contract", main)
.addParam("contract", "The address of the withdraw contract on ZetaChain")
.addParam("amount", "Amount of tokens to send")
+ .addOptionalParam("token", "The address of the token to send")
.addFlag("json", "Output in JSON")
.addParam("targetToken")
.addParam("recipient");
@@ -235,7 +264,11 @@ Before proceeding with the next steps, make sure you have
[created an account and requested ZETA tokens](/developers/omnichain/tutorials/hello#create-an-account)
from the faucet.
-## Deploy the Contract
+## Compile and Deploy the Contract
+
+```
+npx hardhat compile --force
+```
```
npx hardhat deploy --network zeta_testnet
@@ -245,40 +278,72 @@ npx hardhat deploy --network zeta_testnet
🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
🚀 Successfully deployed contract on ZetaChain.
-📜 Contract address: 0x458bCAF5d95025cdd00f946f1C5F09623E856579
-🌍 Explorer: https://athens3.explorer.zetachain.com/address/0x458bCAF5d95025cdd00f946f1C5F09623E856579
+📜 Contract address: 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
+🌍 Explorer: https://athens3.explorer.zetachain.com/address/0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E
```
-## Swap from an EVM Chain
+## Swap Native Gas Tokens Between EVM Chains
Use the `interact` task to perform a cross-chain swap. In this example, we're
-swapping native gETH for for a ZRC-20 representation of tMATIC. The contract
-will perform a swap and then withdraw tMATIC to Polygon Mumbai. To get the value
-of the `--target-token` find the ZRC-20 contract address of the destination
-token in the [docs](https://www.zetachain.com/docs/reference/testnet/).
+swapping native gETH from Goerli for tMATIC on Polygon Mumbai. The contract will
+deposit gETH to ZetaChain as ZRC-20, swap it for ZRC-20 tMATIC and then withdraw
+native tMATIC Polygon Mumbai. To get the value of the `--target-token` find the
+ZRC-20 contract address of the destination token in the
+[ZRC-20 section of the docs](/developers/omnichain/zrc-20).
```
-npx hardhat interact --contract 0x458bCAF5d95025cdd00f946f1C5F09623E856579 --amount 0.1 --target-token 0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1 --network goerli_testnet
+npx hardhat interact --contract 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E --amount 0.01 --network goerli_testnet --target-token 0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
```
```
🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
🚀 Successfully broadcasted a token transfer transaction on goerli_testnet network.
-📝 Transaction hash: 0x7ebd2bff64cbc530c145a60e4830ba2ddc536bc62cf8c5566c900143b0e08baf
+📝 Transaction hash: 0x6b4156c195d955d1325a5e6275214db63ff2e3642838607333e74abd74b8fc13
```
Track your cross-chain transaction:
```
- npx hardhat cctx 0x7ebd2bff64cbc530c145a60e4830ba2ddc536bc62cf8c5566c900143b0e08baf
+npx hardhat cctx 0x6b4156c195d955d1325a5e6275214db63ff2e3642838607333e74abd74b8fc13
```
```
✓ CCTXs on ZetaChain found.
-✓ 0x5082897440218490193a724a22b7ed3f8744760956d857199e76bf2453f901b2: 5 → 7001: OutboundMined (Remote omnichain contract call completed)
-✓ 0x3aebbba04cd284d2e87b9c8414f6946bdf933efad45076aba7c691e5a32895ba: 7001 → 80001: PendingOutbound → OutboundMined
+✓ 0xb263483d61d0b1c89685364324c32438a3546b3f732a3d37840e6042e4837357: 5 → 7001: OutboundMined (Remote omnichain contract call completed)
+✓ 0xcad3a0b8949b1bd39c51e80ff6be9a5c6d337df8a04b39a48667d2c688172e35: 7001 → 80001: PendingOutbound → OutboundMined
+```
+
+## Swap ERC-20 Tokens Between EVM Chains
+
+Now let's swap USDC from Goerli to MATIC on Polygon Mumbai. To send USDC specify
+the ERC-20 token contract address (on Goerli) in the `--token` parameter. You
+can find the address of the token in the
+[ZRC-20 section of the docs](/developers/omnichain/zrc-20).
+
+```
+npx hardhat interact --contract 0xf6CDd83AB44E4d947FE52c2637ee4A04F330328E --amount 10 --token 0x07865c6e87b9f70255377e024ace6630c1eaa37f --network goerli_testnet --target-token 0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb --recipient 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+```
+
+```
+🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1
+
+🚀 Successfully broadcasted a token transfer transaction on goerli_testnet
+network. 📝 Transaction hash:
+0xff32dd2391c4f62694cc99afd0da1c2a1352c8caf29846cc366aab54a631e8f8
+```
+
+```
+npx hardhat cctx
+0xff32dd2391c4f62694cc99afd0da1c2a1352c8caf29846cc366aab54a631e8f8
+```
+
+```npx hardhat cctx 0xff32dd2391c4f62694cc99afd0da1c2a1352c8caf29846cc366aab54a631e8f8
+✓ CCTXs on ZetaChain found.
+
+✓ 0xa4cb698122916f10c932e146c45517b4f47de1e16be493ea66f28b5a34c7bfb5: 5 → 7001: OutboundMined (Remote omnichain contract call completed)
+✓ 0xad18c759713ce5604683aeb389fc9a1a91f537c0abbb8d9f9fc6cfc11e55fdc7: 7001 → 80001: PendingOutbound → OutboundMined
```
## Swap from Bitcoin
@@ -287,12 +352,23 @@ Use the `send-btc` task to send Bitcoin to the TSS address with a memo. The memo
should contain the following:
- Omnichain contract address on ZetaChain:
- `cC02751bAA435E9A5cF3bd22F96a21d7C002E150`
+ `f6CDd83AB44E4d947FE52c2637ee4A04F330328E`
- Target token address: `48f80608B672DC30DC7e3dbBd0343c5F02C738Eb`
- Recipient address: `2cD3D070aE1BD365909dD859d29F387AA96911e1`
```
-npx hardhat send-btc --amount 0.001 --memo cC02751bAA435E9A5cF3bd22F96a21d7C002E15048f80608B672DC30DC7e3dbBd0343c5F02C738Eb2cD3D070aE1BD365909dD859d29F387AA96911e1 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
+npx hardhat send-btc --amount 0.001 --memo f6CDd83AB44E4d947FE52c2637ee4A04F330328E48f80608B672DC30DC7e3dbBd0343c5F02C738Eb2cD3D070aE1BD365909dD859d29F387AA96911e1 --recipient tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur
+```
+
+```
+npx hardhat cctx 3c2eeee38fafbfbcdceca0d595c1433c48c738aaa6e1df407a681aeeeb1da3d6
+```
+
+```
+✓ CCTXs on ZetaChain found.
+
+✓ 0xa7d4a46545806a5aff4d4fc20cb37295f426b70f0f6b2a123f67cbdb3014c995: 18332 → 7001: OutboundMined (Remote omnichain contract call completed)
+✓ 0x963cf8890b3da9e84379eca06a2e4835aba3a027bca6560e76d19945b75b2c39: 7001 → 80001: PendingOutbound → OutboundMined
```
## Source Code
diff --git a/src/components/ForeignCoins/ForeignCoins.tsx b/src/components/ForeignCoins/ForeignCoins.tsx
index 90de4e7e..fe08111a 100644
--- a/src/components/ForeignCoins/ForeignCoins.tsx
+++ b/src/components/ForeignCoins/ForeignCoins.tsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
const COINS_URL =
- "https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/zetacore/fungible/foreign_coins";
+ "https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/fungible/foreign_coins";
const CHAINS_URL =
"https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/observer/supportedChains";
@@ -64,10 +64,11 @@ const ForeignCoinsTable = () => {
- Chain Name |
+ Chain |
Symbol |
- Coin Type |
- ZRC-20 Contract Address |
+ Type |
+ ZRC-20 on ZetaChain |
+ ERC-20 on Connected Chain |
@@ -77,6 +78,7 @@ const ForeignCoinsTable = () => {
{coin.symbol} |
{coin.coin_type} |
{coin.zrc20_contract_address} |
+ {coin.asset} |
))}