Skip to main content
Version: 0.2.x-amarok

Quickstart

These are the simplest working contracts that demonstrate how to use xcall, a cross-chain primitive mimicking Solidity's low level call method.


Setup

Install Node.js and use Node.js v16. Follow the instructions to install nvm, a node version manager, which will make switching versions easier.

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

npm install @connext/[email protected]

Example 1: Hello Chain

In this example, there are two contracts. The Target contract on the destination domain will have a greeting that we want to update. The Source contract on the origin domain will use xcall to execute the update on Target by sending encoded calldata.

Target Contract

pragma solidity ^0.8.14;

contract Target {
string public greeting;

function updateGreeting(string newGreeting) external {
greeting = newGreeting;
}
}

The goal is to call updateGreeting from the origin domain.

Source Contract

The source contract initiates the cross-chain operation with xcall.

pragma solidity ^0.8.14;

import {IConnextHandler} from "@connext/nxtp-contracts/contracts/core/connext/interfaces/IConnextHandler.sol";
import {CallParams, XCallArgs} from "@connext/nxtp-contracts/contracts/core/connext/libraries/LibConnextStorage.sol";

contract Source {
// ConnextHandler contract on origin domain
IConnextHandler public connext = IConnextHandler(0xB4C1340434920d70aD774309C75f9a4B679d801e);

// Function that the user will call
function updateGreeting (address target, string memory newGreeting) external {
// We're sending calldata, so encode the target function with its arguments
bytes4 selector = bytes4(keccak256("updateGreeting(string)"));
bytes memory callData = abi.encodeWithSelector(selector, newGreeting);

CallParams memory callParams = CallParams({
to: target, // address of the target contract
callData: callData, // encoded calldata to execute on destination
originDomain: 1735353714, // from Goerli
destinationDomain: 1735356532, // to Optimism-Goerli
agent: msg.sender, // address allowed to execute transaction on destination side in addition to relayers
recovery: msg.sender, // fallback address to send funds to if execution fails on destination side
forceSlow: false, // option to force slow path instead of paying 0.05% fee on fast liquidity transfers
receiveLocal: false, // option to receive the local bridge-flavored asset instead of the adopted asset
callback: address(0), // zero address because we're not using a callback
callbackFee: 0, // fee paid to relayers for the callback; no fees on testnet
relayerFee: 0, // fee paid to relayers for the forward call; no fees on testnet
destinationMinOut: 0 // not sending funds so minimum can be 0
});

XCallArgs memory xcallArgs = XCallArgs({
params: callParams,
transactingAsset: address(0), // 0 address is the native gas token
transactingAmount: 0, // not sending funds with this calldata-only xcall
originMinOut: 0 // not sending funds so minimum can be 0
});

connext.xcall(xcallArgs);
}
}

Example 2: Cross-Chain Transfer

This XTransfer contract implements a cross-chain token transfer. There's no target contract on the destination side so only one contract is needed.

XTransfer Contract

pragma solidity ^0.8.14;

import {IConnextHandler} from "@connext/nxtp-contracts/contracts/core/connext/interfaces/IConnextHandler.sol";
import {CallParams, XCallArgs} from "@connext/nxtp-contracts/contracts/core/connext/libraries/LibConnextStorage.sol";
import {ERC20} from "@solmate/tokens/ERC20.sol";

contract XTransfer {
// ConnextHandler contract on origin domain
IConnextHandler public connext = IConnextHandler(0xB4C1340434920d70aD774309C75f9a4B679d801e);

// TEST ERC20 token on origin domain
ERC20 public token = ERC20(0x7ea6eA49B0b0Ae9c5db7907d139D9Cd3439862a1);

// Function that the user will call
function transfer(address recipient, uint256 amount) external {
require(
token.allowance(msg.sender, address(this)) >= amount,
"User must approve amount to this contract"
);

// User's funds are transferred to this contract
token.transferFrom(msg.sender, address(this), amount);

// This contract approves spend to the Connext contract
token.approve(address(connext), amount);

CallParams memory callParams = CallParams({
to: recipient, // wallet receiving the funds on the destination
callData: "", // empty here because we're only sending funds
originDomain: 1735353714, // from Goerli
destinationDomain: 1735356532, // to Optimism-Goerli
agent: msg.sender, // address allowed to execute transaction on destination side in addition to relayers
recovery: msg.sender, // fallback address to send funds to if execution fails on destination side
forceSlow: false, // option to force slow path instead of paying 0.05% fee on fast path transfers
receiveLocal: false, // option to receive the local bridge-flavored asset instead of the adopted asset
callback: address(0), // zero address because we're not using a callback
callbackFee: 0, // fee paid to relayers; relayers don't take any fees on testnet
relayerFee: 0, // fee paid to relayers; relayers don't take any fees on testnet
destinationMinOut: (amount / 100) * 99 // minimum amount acceptable due to slippage from the AMM (1% here)
});

XCallArgs memory xcallArgs = XCallArgs({
params: callParams,
transactingAsset: address(token), // the token being transferred to the target contract
transactingAmount: amount, // amount of ERC20 to transfer
originMinOut: (amount / 100) * 99 // minimum amount acceptable due to slippage from the AMM (1% here)
});

connext.xcall(xcallArgs);
}
}

Since funds are going to be routed through Connext's contracts, the user must first approve a spending allowance of the TEST ERC20 to XTransfer. The require clause checks for this allowance.

You can use Etherscan and write to the TEST ERC20's approve function to do this:

TestERC20 Etherscan Approve

The full token flow moves from User's wallet -> XTransfer -> ConnextHandler -> recipient.


Next Steps

»xApp Starter Kit