Quickstart
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
.
Prerequisitesโ
- 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
Quit
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
- Yarn
npm install @connext/smart-contracts
yarn add @connext/smart-contracts
Next, install the OpenZepplin contract package:
- npm
- Yarn
npm install @openzeppelin/contracts
yarn add @openzeppelin/contracts
You'll need to manually install the library @openzeppelin/contracts-upgradeable
- npm
- Yarn
npm install @openzeppelin/contracts-upgradeable
yarn add @openzeppelin/contracts-upgradeable
Install dotenv
to protect your private key needed to deploy your contract:
- npm
- Yarn
npm install dotenv
yarn add 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:
PRIVATE_KEY=YOUR-PRIVATE-KEY-HERE
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
- Yarn
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"
yarn add --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:
require("@nomicfoundation/hardhat-toolbox");
require('@openzeppelin/hardhat-upgrades');
// Any file that has require('dotenv').config() statement
// will automatically load any variables in the root's .env file.
require('dotenv').config();
module.exports = {
solidity: "0.8.17",
networks:{
goerli:{
url: "https://rpc.ankr.com/eth_goerli",
// 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();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
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:
- https://goerli-faucet.mudit.blog/ (Requires Twitter account)
- https://goerlifaucet.com/ (Requires signing up with Alchemy)
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
.
- This is the address of
amount
: 10000000000000000000 (10 TEST)- Recall that
DestinationGreeting
requires a payment of at least 1 TEST (1e18 wei units). InSourceGreeting
, 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 callupdateGreeting
without having to do an approval every time. - If earning fees as a router sounds interesting, check out the Routers documentation.
- Recall that

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.

Let's talk about the different parameters.
target
: 0x9094da44ec4335632c28749437f616a8a6cadcb6- The address of
DestinationGreeting
.
- The address of
destinationDomain
: 9991- The Domain ID of the destination chain. You can find a mapping of Domain IDs here. Remember,
DestinationGreeting
is deployed to Mumbai.
- The Domain ID of the destination chain. You can find a mapping of Domain IDs here. Remember,
newGreeting
: hello chain!- Whatever string you want to update the greeting to.
relayerFee
: 0IMPORTANT! 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}(...)
tipAs 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!

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.