Quickstart (Greeter)
This quickstart will teach you how to use xcall
, the cross-chain communication primitive, to send funds and data across chains.
Introductionโ
In this guide, we will build a cross-chain Greeter. The DestinationGreeter
contract on the destination chain has an updateGreeting
function that changes a stored greeting
variable. The SourceGreeter
contract on the origin chain uses xcall
to send encoded calldata for updateGreeting
.
To demonstrate a combination of an asset transfer and an arbitrary call in a single xcall
, the updateGreeting
function will require a payment to update the greeting. For this example, the contract will be okay with any amount greater than 0.
updateGreeting
is implemented as an unauthenticated call (there are no checks to determine who is calling the function). Therefore, this type of xcall
will be go through the "Fast Path".
If you prefer to fork a repo instead of following this step-by-step guide, our xapp-starter kit contains a full example of this quickstart (plus more) and is compatible with both Hardhat and Foundry.
Prerequisitesโ
Node v18 installed
Follow the 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 the instructions to install Hardhat.
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)
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.
Install the latest version of Connext contracts package in your project:
- npm
- Yarn
- pnpm
npm install @connext/interfaces
yarn add @connext/interfaces
pnpm add @connext/interfaces
Next, install the OpenZeppelin contract package:
- npm
- Yarn
- pnpm
npm install @openzeppelin/contracts
yarn add @openzeppelin/contracts
pnpm add @openzeppelin/contracts
You'll need to manually install the library @openzeppelin/contracts-upgradeable
- npm
- Yarn
- pnpm
npm install @openzeppelin/contracts-upgradeable
yarn add @openzeppelin/contracts-upgradeable
pnpm add @openzeppelin/contracts-upgradeable
Install dotenv
to protect your private key needed to deploy your contract:
- npm
- Yarn
- pnpm
npm install dotenv
yarn add dotenv
pnpm add dotenv
In the root of your project, create a new .env
file. Here you will store your private key used to deploy your contract.
Update .env
with 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.
In the /contracts
directory, create a new contract called SourceGreeter.sol
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;
import {IConnext} from "@connext/interfaces/core/IConnext.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title SourceGreeter
* @notice Example source contract that updates a greeting on DestinationGreeter.
*/
contract SourceGreeter {
// The Connext contract on this domain
IConnext public immutable connext;
// The token to be paid on this domain
IERC20 public immutable token;
// Slippage (in BPS) for the transfer set to 100% for this example
uint256 public immutable slippage = 10000;
constructor(address _connext, address _token) {
connext = IConnext(_connext);
token = IERC20(_token);
}
/** @notice Updates a greeting variable on the DestinationGreeter contract.
* @param target Address of the DestinationGreeter contract.
* @param destinationDomain The destination domain ID.
* @param newGreeting New greeting to update to.
* @param relayerFee The fee offered to relayers.
*/
function xUpdateGreeting (
address target,
uint32 destinationDomain,
string memory newGreeting,
uint256 amount,
uint256 relayerFee
) external payable {
require(
token.allowance(msg.sender, address(this)) >= amount,
"User must approve amount"
);
// User sends funds to this contract
token.transferFrom(msg.sender, address(this), amount);
// This contract approves transfer to Connext
token.approve(address(connext), amount);
// Encode calldata for the target contract call
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
amount, // _amount: amount of tokens to transfer
slippage, // _slippage: max slippage the user will accept in BPS (e.g. 300 = 3%)
callData // _callData: the encoded calldata to send
);
}
}
xUpdateGreeting
is what the user will call on origin to initiate the xcall
.
One important detail to note is that xUpdateGreeting
is a payable
method. This is necessary because a relayerFee
in native gas is passed into the xcall
. More on how this fee is determined later.
Compile Contractโ
Make sure the solidity compiler version in your hardhat.config.js
is at least 0.8.17
.
module.exports = {
solidity: "0.8.17",
};
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 following command:
- npm
- Yarn
- pnpm
npm install --save-dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "solidity-coverage@^0.8.1" "ts-node@>=8.0.0" "typescript@>=4.5.0"
yarn add --dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "solidity-coverage@^0.8.1" "ts-node@>=8.0.0" "typescript@>=4.5.0"
pnpm add --save-dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "solidity-coverage@^0.8.1" "ts-node@>=8.0.0" "typescript@>=4.5.0"
Deploy Contractโ
Update the hardhat.config.js
file:
require("@nomicfoundation/hardhat-toolbox");
require('@openzeppelin/hardhat-upgrades');
require('dotenv').config();
module.exports = {
solidity: "0.8.17",
networks:{
goerli:{
url: "https://rpc.ankr.com/eth_goerli",
accounts: [`0x${process.env.PRIVATE_KEY}`]
}
}
};
Create a /scripts/deploySource.js
file with the following:
const main = async () => {
const sourceGreeterContract = await hre.ethers.deployContract(
"SourceGreeter",
[
"0xFCa08024A6D4bCc87275b1E4A1E22B71fAD7f649", // Connext on Goerli
"0x7ea6eA49B0b0Ae9c5db7907d139D9Cd3439862a1" // TEST on Goerli
]
);
await sourceGreeterContract.waitForDeployment();
console.log("Contract deployed to:", await sourceGreeterContract.getAddress());
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
The addresses for Connext and supported tokens in different domains can be referenced here. We'll be using Goerli as our origin domain and the TEST token for this contract.
Now run the deploy script:
npx hardhat run scripts/deploySource.js --network goerli
Output: Contract deployed to: 0x9Af84578B89FcA019580af02326388987A074ca1
Verify Contractโ
Add an etherScan
section to hardhat.config.js
with your goerli
api key (note: Etherscan API keys for the mainnet explorers will work for testnets):
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}`]
}
},
etherscan: {
apiKey: {
goerli: "YOUR-API-KEY-HERE",
}
}
};
Using the contract address you just deployed, run the hardhat verify
command, including the contract address and its constructor arguments:
npx hardhat verify --network goerli 0x9Af84578B89FcA019580af02326388987A074ca1 0xFCa08024A6D4bCc87275b1E4A1E22B71fAD7f649 0x7ea6eA49B0b0Ae9c5db7907d139D9Cd3439862a1
If you run into any errors like ProviderError: Too Many Requests
, then replace the public RPC url in hardhat.config.js
with another one from https://chainlist.org/ or use your own private RPC from a provider like Infura or Alchemy.
Target Contractโ
In the /contracts
directory, create another contract called DestinationGreeter.sol
:
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: UNLICENSED
pragma solidity ^0.8.15;
import {IXReceiver} from "@connext/interfaces/core/IXReceiver.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title DestinationGreeter
* @notice Example destination contract that stores a greeting.
*/
contract DestinationGreeter is IXReceiver {
string public greeting;
// The token to be paid on this domain
IERC20 public immutable token;
constructor(address _token) {
token = IERC20(_token);
}
/** @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) {
// Check for the right token
require(
_asset == address(token),
"Wrong asset received"
);
// Enforce a cost to update the greeting
require(
_amount > 0,
"Must pay at least 1 wei"
);
// 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;
}
}
Compile Contractโ
Compile:
npx hardhat compile
Deploy Contractโ
Add another entry to hardhat.config.js
, this time for Optimism-Goerli.
module.exports = {
solidity: "0.8.17",
networks:{
"goerli":{
url: "https://rpc.ankr.com/eth_goerli",
accounts: [`0x${process.env.PRIVATE_KEY}`]
},
"optimism-goerli":{
url: "https://goerli.optimism.io",
accounts: [`0x${process.env.PRIVATE_KEY}`],
// gasPrice: 800000 // you may need to set this manually if you get "transaction underpriced"
}
}
};
Create a scripts/deployTarget.js
file with the following:
const main = async () => {
const destinationGreeterContract = await hre.ethers.deployContract(
"DestinationGreeter",
[
"0x68Db1c8d85C09d546097C65ec7DCBFF4D6497CbF" // TEST on Optimism-Goerli
]
);
await destinationGreeterContract.waitForDeployment();
console.log("Contract deployed to:", await destinationGreeterContract.getAddress());
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Then run the deploy script:
npx hardhat run scripts/deployTarget.js --network optimism-goerli
Output: Contract deployed to: 0xC4e508cEe84499958a84C3562e92bD9e71d7D38a
Verify Contractโ
Add an apiKey
to hardhat.config.js
for optimism-goerli
:
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}`]
}
},
etherscan: {
apiKey: {
goerli: "YOUR-API-KEY-HERE",
optimisticGoerli: "YOUR-API-KEY-HERE",
}
}
};
Using the contract address you just deployed, verify it:
npx hardhat verify --network optimism-goerli 0xC4e508cEe84499958a84C3562e92bD9e71d7D38a 0x68Db1c8d85C09d546097C65ec7DCBFF4D6497CbF
If you run into any errors like ProviderError: Too Many Requests
, then replace the public RPC url in hardhat.config.js
with another one from https://chainlist.org/ or use your own private RPC with a provider like Infura or Alchemy.
Executing the Transactionโ
You should try the following steps on your own deployed contracts. For the lazy ones, you can just use these contracts we've deployed already:
Mint TEST Tokensโ
First, you will need some TEST tokens. Recall that the destination contract requires a payment > 0 TEST in order to update its greeting.
Since you'll be updating the greeting from the origin chain, you will need to acquire some TEST tokens on the origin chain.
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. You can actually mint however much you want.
Approve TEST Tokensโ
Tokens will move from User's wallet
=> SourceGreeter
=> Connext
=> DestinationGreeter
.
The user must first approve a spending allowance of the TEST ERC20 to the SourceGreeter
contract. The require
clause starting on line 39 checks for this allowance.
Again, on the Etherscan page for the TEST token, enter the parameters for the approve
function:
spender
: 0x9Af84578B89FcA019580af02326388987A074ca1- This is the address of
SourceGreeter
.
- This is the address of
amount
: 10000000000000000000
Then "Write" to the approve
function.
Execute xUpdateGreeting
โ
Similarly to the approval function for TEST, navigate to the SourceGreeter
contract on Etherscan. Fill out the xUpdateGreeting
function parameters and "Write" to the contract.
Let's walk through the different parameters.
xUpdateGreeting
(payableAmount): 0.03- This is the native gas that you're sending into the
xcall
. This value must match what you pass in asrelayerFee
, but note that it's in ETH units here and wei units inrelayerFee
.
- This is the native gas that you're sending into the
target
: 0xC4e508cEe84499958a84C3562e92bD9e71d7D38a- The address of
DestinationGreeter
.
- The address of
destinationDomain
: 1735356532- The Domain ID of the destination chain. You can find a mapping of Domain IDs here. For this example,
DestinationGreeter
is deployed to Optimism-Goerli.
- The Domain ID of the destination chain. You can find a mapping of Domain IDs here. For this example,
newGreeting
: hello chain!- Whatever string you want to update the greeting to.
amount
: 1000000000000000000- The amount of TEST tokens to pay. We send 1 TEST here.
relayerFee
: 300000000000000000.03 goerli ETH, in wei units. Just a conservative estimate for relayers on testnet.
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 destination. Relayers get paid in the origin chain's native asset. This is why
SourceGreeter
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.
Track the xcallโ
After executing updateGreeting
, you can use Connextscan (testnet) to check the status of the xcall
. Just search up the transaction hash from the execution transaction.
Note that if your relayerFee
was too low, the explorer will prompt you to increase it.
Check DestinationGreeter
โ
DestinationGreeter
should be updated in just a few minutes (because this call is unauthenticated!). Cross-chain calls are not always this fast - see our guide on Authentication.
Head over to the DestinationGreeter
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 SourceGreeter
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.