Skip to main content
Version: 0.2.x-amarok



This quickstart will teach you how to use xcall, the cross-chain communication primitive, to send funds and data across chains.

In this guide, we will build a cross-chain Greeter. The DestinationGreeting contract on the destination domain (Mumbai) stores a greeting variable that we want to update. The SourceGreeting contract on the origin domain (Goerli) will use xcall to execute the updating function on DestinationGreeting.


  • Node v18 installed

Follow these instructions to install Node.js and use Node.js v18. We also recommend installing nvm, a node version manager, which will make switching versions easier.

  • An Ethereum development environment like Foundry, Hardhat, Truffle, etc. This guide will be using Hardhat.

Follow these instructions to install Hardhat.

Create a new projectโ€‹

Create a new project by running the following command:

$ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888

๐Ÿ‘ท Welcome to Hardhat v2.12.1 ๐Ÿ‘ทโ€

? What do you want to do? โ€ฆ
โฏ Create a JavaScript project
Create a TypeScript project
Create an empty hardhat.config.js

Choose a Javascript project. Choose y on all of the prompts.

Open your project in a code editor and rename your contract file under src to SourceGreeting.sol.

Install the latest beta version of Connext contracts package in your project:

npm install @connext/smart-contracts

Next, install the OpenZepplin contract package:

npm install @openzeppelin/contracts

You'll need to manually install the library @openzeppelin/contracts-upgradeable

npm install @openzeppelin/contracts-upgradeable 

Install dotenv to protect your private key needed to deploy your contract:

npm install dotenv

In the root of your project, create a new file .env. Here you will store your private key used to deploy your contract.

Update your .env file to only have the following line:


Source Contractโ€‹

The source contract initiates the cross-chain operation with xcall and passes the encoded greeting into the call. All xcall params are detailed here.

Notice the token that is passed in is the TEST token on Goerli. You can read more about token flavors and why they matter here.

Note that in this contract, we're defining a constructor that takes in the address of the deployed Connext diamond contract on the same chain that this contract will be deployed to. Find the list of all Connext diamond contracts here.

In the /contracts directory, create a new contract called SourceGreeting.sol:

// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.15;import {IConnext} from "@connext/smart-contracts/contracts/core/connext/interfaces/IConnext.sol";import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";/** * @title SourceGreeting * @notice Example source contract that updates a greeting in DestinationGreeting. * @dev Must pay at least 1 TEST to update the greeting. */contract SourceGreeting {  // The connext contract on the origin domain  IConnext public immutable connext;  // Hardcoded cost to update the greeting, in wei units  // Exactly 0.05% above 1 TEST to account for router fees  uint256 public cost = 1.0005003e18;  // The canonical TEST Token on Goerli  IERC20 public token = IERC20(0x7ea6eA49B0b0Ae9c5db7907d139D9Cd3439862a1);  constructor(IConnext _connext) {    connext = _connext;  }  /** @notice Updates a greeting variable on the DestinationGreeting contract.    * @param target Address of the DestinationGreeting contract.    * @param destinationDomain The destination domain ID.    * @param newGreeting New greeting to update to.    * @param relayerFee The fee offered to relayers.    */  function updateGreeting (    address target,     uint32 destinationDomain,    string memory newGreeting,    uint256 relayerFee  ) external {    require(      token.allowance(msg.sender, address(this)) >= cost,      "User must approve amount"    );    // User sends funds to this contract    token.transferFrom(msg.sender, address(this), cost);    // This contract approves transfer to Connext    token.approve(address(connext), cost);    // Encode the data needed for the target contract call.    // Note: the receiver contract expects an encoded calldata argument so sending     //       empty calldata would look like `abi.encode("")` or simply "0x"    bytes memory callData = abi.encode(newGreeting);    connext.xcall{value: relayerFee}(      destinationDomain, // _destination: Domain ID of the destination chain      target,            // _to: address of the target contract      address(token),    // _asset: address of the token contract      msg.sender,        // _delegate: address that can revert or forceLocal on destination      cost,              // _amount: amount of tokens to transfer      30,                // _slippage: the max slippage the user will accept in BPS (0.3%)      callData           // _callData: the encoded calldata to send    );  }}

Compile Contractโ€‹

Compile the contract with the following command:

npx hardhat compile

Note: Hardhat may require you to manually install dependencies for @nomicfoundation/hardhat-toolbox. If you get an error about missing dependencies for that plugin, run the follwing command:

npm install --save-dev "@nomicfoundation/[email protected]^1.0.0" "@nomicfoundation/[email protected]^1.0.0" "@nomiclabs/[email protected]^2.0.0" "@nomiclabs/[email protected]^3.0.0" "@types/[email protected]^4.2.0" "@types/[email protected]^9.1.0" "@typechain/[email protected]^10.1.0" "@typechain/[email protected]^6.1.2" "[email protected]^0.8.1" "[email protected]>=8.0.0" "[email protected]>=4.5.0"

To deploy your contract, update the hardhat.config.js file:

// Any file that has require('dotenv').config() statement
// will automatically load any variables in the root's .env file.

module.exports = {
solidity: "0.8.17",
url: "",
// PRIVATE_KEY loaded from .env file
accounts: [`0x${process.env.PRIVATE_KEY}`]

Deploy Contractโ€‹

Update the scripts/deploy.js file with the following:

const main = async () => {
const SourceGreeting = await hre.ethers.getContractFactory('SourceGreeting');
const sourceGreetingContract = await SourceGreeting.deploy("0x0C70d6E9760DEE639aC761f3564a190220DF5E44");
await sourceGreetingContract.deployed();
console.log("Contract deployed to:", sourceGreetingContract.address);


const runMain = async () => {
try {
await main();
} catch (error) {


Then call the deploy script:

npx hardhat run scripts/deploy.js --network goerli

Output: Contract deployed to: 0x8CC1DB2a76ea4bc40089f0Db2B25f8B13032F72d

Keep track of this contract address because you'll want to verify your contracts later.

Target Contractโ€‹

Follow the same steps above to create a new hardhat project, this time name the project DestinationGreeting. Install all the same dependencies as above.

All target contracts must implement Connext's IXReceiver interface. This interface ensures that Connext can call the contract and pass necessary data.

// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.15;import {IXReceiver} from "@connext/smart-contracts/contracts/core/connext/interfaces/IXReceiver.sol";import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";contract DestinationGreeting is IXReceiver {  string public greeting;  // Hardcoded cost to update the greeting, in wei units  uint256 public cost = 1e18;  // The TEST Token on Mumbai  IERC20 public token = IERC20(0xeDb95D8037f769B72AAab41deeC92903A98C9E16);  /** @notice The receiver function as required by the IXReceiver interface.    * @dev The Connext bridge contract will call this function.    */  function xReceive(    bytes32 _transferId,    uint256 _amount,    address _asset,    address _originSender,    uint32 _origin,    bytes memory _callData  ) external returns (bytes memory) {    // Enforce the cost to update the greeting    require(      _asset == address(token) && _amount >= cost,      "Must pay at least 1 TEST"    );    // Unpack the _callData    string memory newGreeting = abi.decode(_callData, (string));    _updateGreeting(newGreeting);  }  /** @notice Internal function to update the greeting.    * @param newGreeting The new greeting.    */  function _updateGreeting(string memory newGreeting) internal {    greeting = newGreeting;  }}

With this code, you'll be able to update the greeting from the source contract.

Executing the Transactionโ€‹

Now that you've deployed your contracts, let's verify that we're actually able to access the source contract's functionality from the destination contract.

If you don't already have gas funds on Goerli, try these faucets to get some:

For the following steps, you should try deploying (and verifying) your own contracts. For ease, you can use the contracts we deployed for this part:

Note: These contracts are named differently than the ones you just wrote, but the content of the contract is the same.

TEST Token Mintingโ€‹

Mint some TEST tokens that will be used for this example.

You can use Etherscan to call functions on (verified) contracts. Go to the TEST Token on Etherscan and click on the "Write Contract" button.


A new tab will show up with all write functions of the contract. Connect your wallet, switch to the Goerli network, and enter the parameters for the mint function:

  • account: <YOUR_WALLET_ADDRESS>
  • amount: 10000000000000000000 (10 TEST)

TEST Token Spending Approvalโ€‹

Tokens will move from User's wallet => SourceGreeting => Connext => DestinationGreeting.

The user must first approve a spending allowance of the TEST ERC20 to the SourceGreeting contract. The require clause starting on line 37 checks for this allowance.

Again, on the Etherscan page, enter the parameters for the approve function:

  • spender: 0x9ce3799f033d89d316f373f1db161c84a401a26c
    • This is the address of SourceGreeting.
  • amount: 10000000000000000000 (10 TEST)
    • Recall that DestinationGreeting requires a payment of at least 1 TEST (1e18 wei units). In SourceGreeting, this is hardcoded as 1000500300000000000 to account for a 0.05% fee that routers will take on the bridged asset. But you can approve as much as you want. This way, you can call updateGreeting without having to do an approval every time.
    • If earning fees as a router sounds interesting, check out the Routers documentation.
TestERC20 Etherscan Approve

Then "Write" to the approve function.

Execute updateGreetingโ€‹

Similarly to the approval function for TEST, navigate to the HelloSource contract on Etherscan. Fill out the updateGreeting function parameters and "Write" to the contract.

TestERC20 Etherscan Approve

Let's talk about the different parameters.

  • target: 0x9094da44ec4335632c28749437f616a8a6cadcb6

    • The address of DestinationGreeting.
  • destinationDomain: 9991

    • The Domain ID of the destination chain. You can find a mapping of Domain IDs here. Remember, DestinationGreeting is deployed to Mumbai.
  • newGreeting: hello chain!

    • Whatever string you want to update the greeting to.
  • relayerFee: 0

    • IMPORTANT! This is a fee paid to relayers, which are off-chain agents that help execute the final leg of the cross-chain transfer on the destination. Relayers get paid in the origin chain's native asset. This is why SourceGreeting passes the fee like so:

      connext.xcall{value: relayerFee}(...)

      As a xApp developer, you have some tools available to estimate what this relayerFee should be. For now, there are offchain methods for doing so - check out the guide on Estimating Fees.

Check DestinationGreetingโ€‹

After executing updateGreeting, DestinationGreeting should be updated in just a few minutes.

  • Would it always be this fast? See our guide on Authentication to learn when xcall is fast or slow.

Head over to the DestinationGreeting contract on Etherscan. This time, we'll go to the Read Contract tab and look at the value of greeting. It has updated!

TestERC20 Etherscan Approve

Send a couple more updates from HelloSource but make it a different string. At some point, your TEST allowance to HelloSource will run out and you'll need to do the approval dance again.

Congrats! You've gone cross-chain!

Next Stepsโ€‹

  • Try tracking the status of an xcall after you send it.
  • Learn about authentication and important security considerations.
  • See how nested xcalls can open up infinite cross-chain possibilities.
  • Fork the xApp Starter Kit below (includes code for this example) and build your own xApp.
ยปxApp Starter Kit