Skip to main content
Developers
Cross-Chain Messaging
Tutorials
Value Transfer

Multichain Value Transfer

This is an example contract that shows how you can send value across chains via the ZetaChain API.

From this rudimentary example, you could easily extend it to support arbitrary asset exchanges via a swap to/from ZETA on source and destination.

Multichain Value Transfer

In this tutorial we will create a contract that allows sending value from one chain to another using the Connector API.

Set up your environment

git clone https://github.com/zeta-chain/template

Install the dependencies:

yarn add --dev @openzeppelin/contracts

Create a new contract

contracts/MultiChainValue.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@zetachain/protocol-contracts/contracts/evm/Zeta.eth.sol";
import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol";
import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol";

/**
* @dev Custom errors for contract MultiChainValue
*/
interface MultiChainValueErrors {
error ErrorTransferringZeta();

error ChainIdAlreadyEnabled();

error ChainIdNotAvailable();

error InvalidZetaValueAndGas();
}

/**
* @dev MultiChainValue goal is to send Zeta token across all supported chains
* Extends the logic defined in ZetaInteractor to handle multichain standards
*/
contract MultiChainValue is ZetaInteractor, MultiChainValueErrors {
address public zetaToken;
// @dev map of valid chains to send Zeta
mapping(uint256 => bool) public availableChainIds;

// @dev Constructor calls ZetaInteractor's constructor to setup Connector address and current chain
constructor(
address connectorAddress_,
address zetaToken_
) ZetaInteractor(connectorAddress_) {
if (zetaToken_ == address(0)) revert ZetaCommonErrors.InvalidAddress();
zetaToken = zetaToken_;
}

/**
* @dev Whitelist a chain to send Zeta
*/
function addAvailableChainId(
uint256 destinationChainId
) external onlyOwner {
if (availableChainIds[destinationChainId])
revert ChainIdAlreadyEnabled();

availableChainIds[destinationChainId] = true;
}

/**
* @dev Blacklist a chain to send Zeta
*/
function removeAvailableChainId(
uint256 destinationChainId
) external onlyOwner {
if (!availableChainIds[destinationChainId])
revert ChainIdNotAvailable();

delete availableChainIds[destinationChainId];
}

/**
* @dev If the destination chain is a valid chain, send the Zeta tokens to that chain
*/
function send(
uint256 destinationChainId,
bytes calldata destinationAddress,
uint256 zetaValueAndGas
) external {
if (!availableChainIds[destinationChainId])
revert InvalidDestinationChainId();
if (zetaValueAndGas == 0) revert InvalidZetaValueAndGas();

bool success1 = ZetaEth(zetaToken).approve(
address(connector),
zetaValueAndGas
);
bool success2 = ZetaEth(zetaToken).transferFrom(
msg.sender,
address(this),
zetaValueAndGas
);
if (!(success1 && success2)) revert ErrorTransferringZeta();

connector.send(
ZetaInterfaces.SendInput({
destinationChainId: destinationChainId,
destinationAddress: destinationAddress,
destinationGasLimit: 300000,
message: abi.encode(),
zetaValueAndGas: zetaValueAndGas,
zetaParams: abi.encode("")
})
);
}
}

The contract's main functionality is implemented in the sendValue function.

The send function first checks if the destination chain ID is valid and if the Zeta value and gas are not zero.

Next, it attempts to approve and transfer the specified amount of Zeta tokens from the sender's address to the contract's address.

Finally, the function calls the "send" function of a connector contract, providing the necessary inputs such as the destination chain ID, destination address, gas limit, and other parameters. The function encodes these inputs into a message and sends it to the connector contract for further processing.

The contract also uses a notion of "available chains". Before calling the send function and transferring value between chains you need to call the addAvailableChainId function on the source chain and add the destination chain ID to the list of available chains. In this example this logic is implemented in the deploy task.

Create a deployment task

The deploy task is fairly standard. It deploys the contract to two or more chains and sets the interactors on each chain. Additionally, for this example, the script also calls the addAvailableChainId function on each chain to add the other chain to the list of available chains.

tasks/deploy.ts
loading...

Clear the cache and artifacts, then compile the contract:

npx hardhat compile --force

Run the following command to deploy the contract to two networks:

npx hardhat deploy --networks polygon-mumbai,bsc-testnet

Send a message

Create a new task to send tokens from one chain to another. The task accepts the following parameters: contract address, recipient address, amount, destination chain ID, and the source network name.

Send a message from Polygon Mumbai testnet to BSC testnet (chain ID: 97) using the contract address (see the output of the deploy task). Make sure to submit enough native tokens with --amount to pay for the transaction fees.

npx hardhat send --contract 0x042AF09ae20f924Ce18Dc3daBFa1866B114aFa89 --address 0xF5a522092F8E4041F038a6d30131192945478Af0 --amount 20 --destination 97 --network polygon-mumbai

🔑 Using account: 0x2cD3D070aE1BD365909dD859d29F387AA96911e1

✅ The transaction has been broadcasted to polygon-mumbai
📝 Transaction hash: 0x2748882492a565627e4726658744f443fb993943f25ba73f93dba42ae314e689

Please, refer to ZetaChain's explorer for updates on the progress of the cross-chain transaction.

🌍 Explorer: https://explorer.zetachain.com/address/0x042AF09ae20f924Ce18Dc3daBFa1866B114aFa89

Source Code

You can find the source code for the example in this tutorial here:

https://github.com/zeta-chain/example-contracts/blob/feat/import-toolkit/messaging/value