Ping is a contract on some domain and Pong is a contract on some other domain. The user sends a ping and a pong will be sent back!
This example demonstrates how to use nested xcalls and a single direction emulating "callback" behavior.
Ping Contract
The Ping contract contains an external startPingPong function that the user will call to initiate the flow. It also implements IXReceiver because it will act as the "target" of an xcall from Pong. The xReceive function here is essentially a callback function - we can verify that something happened in Pong's domain and handle any results passed back.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.15;import {IConnext} from"@connext/interfaces/core/IConnext.sol";import {IXReceiver} from"@connext/interfaces/core/IXReceiver.sol";/** * @title Ping * @notice Ping side of a PingPong example. */contractPingisIXReceiver {// The Connext contract on this domain IConnext publicimmutable connext;// Number of pings this contract has receiveduint256public pings;constructor(address_connext) { connext =IConnext(_connext); }/** * @notice Starts the ping pong s. equence. * @param destinationDomain The destination domain ID. * @param target Address of the Pong contract on the destination domain. * @param relayerFee The fee offered to relayers. */functionstartPingPong(address target,uint32 destinationDomain,uint256 relayerFee ) externalpayable {require( msg.value == relayerFee,"Must send gas equal to the specified relayer fee" );// Include the relayerFee so Pong will use the same fee // Include the address of this contract so Pong will know where to send the "callback"bytesmemory callData = abi.encode(pings,address(this), relayerFee); connext.xcall{value: relayerFee}( destinationDomain,// _destination: domain ID of the destination chain target,// _to: address of the target contract (Pong)address(0),// _asset: use address zero for 0-value transfers msg.sender,// _delegate: address that can revert or forceLocal on destination0,// _amount: 0 because no funds are being transferred0,// _slippage: can be anything between 0-10000 because no funds are being transferred callData // _callData: the encoded calldata to send ); }/** @notice The receiver function as required by the IXReceiver interface. * @dev The "callback" function for this example. Will be triggered after Pong xcalls back. */functionxReceive(bytes32_transferId,uint256_amount,address_asset,address_originSender,uint32_origin,bytesmemory_callData ) externalreturns (bytesmemory) {uint256 _pongs = abi.decode(_callData, (uint256)); pings++; }}
Pong Contract
Pong will send a nested xcall back to Ping, including some information that can be acted on.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.15;import {IConnext} from"@connext/interfaces/core/IConnext.sol";import {IXReceiver} from"@connext/interfaces/core/IXReceiver.sol";interface IPong {functionsendPong(uint32 destinationDomain,address target,uint256 relayerFee ) externalpayable;}/** * @title Pong * @notice Pong side of a PingPong example. */contractPongisIXReceiver {// The Connext contract on this domain IConnext publicimmutable connext;// Number of pongs this contract has receiveduint256public pongs;constructor(address_connext) { connext =IConnext(_connext); }/** * @notice Sends a pong to the Ping contract. * @param destinationDomain The destination domain ID. * @param target Address of the Ping contract on the destination domain. * @param relayerFee The fee offered to relayers. */functionsendPong(uint32 destinationDomain,address target,uint256 relayerFee ) internal {// Include some data we can use back on Pingbytesmemory callData = abi.encode(pongs); connext.xcall{value: relayerFee}( destinationDomain,// _destination: Domain ID of the destination chain target,// _to: address of the target contract (Ping)address(0),// _asset: use address zero for 0-value transfers msg.sender,// _delegate: address that can revert or forceLocal on destination0,// _amount: 0 because no funds are being transferred0,// _slippage: can be anything between 0-10000 because no funds are being transferred callData // _callData: the encoded calldata to send ); }/** * @notice The receiver function as required by the IXReceiver interface. * @dev The Connext bridge contract will call this function. */functionxReceive(bytes32_transferId,uint256_amount,address_asset,address_originSender,uint32_origin,bytesmemory_callData ) externalreturns (bytesmemory) {// Because this call is *not* authenticated, the _originSender will be the Zero Address// Ping's address was sent with the xcall so it can be decoded and used for the nested xcall (uint256 _pings,address _pingContract,uint256 _relayerFee ) = abi.decode(_callData, (uint256,address,uint256)); pongs++;// This contract sends a nested xcall with the same relayerFee value used for Ping. That means// it must own at least that much in native gas to pay for the next xcall.require(address(this).balance >= _relayerFee,"Not enough gas to pay for relayer fee" );// The nested xcallsendPong(_origin, _pingContract, _relayerFee); }/** * @notice This contract can receive gas to pay for nested xcall relayer fees. */receive() externalpayable {}fallback() externalpayable {}}
An important note for Pong is that sendPong is notpayable and neither is xReceive. So in order for the 2nd xcall to work with relayerFees, the someone has to send native gas on destination to Pong. In practice, this can take the form of a "gas tank" mechanism that can be filled by users or subsidized by protocols.
Connext is working on an upgrade that will soon allow nested relayer fees to be deducted from the transacting asset, eliminating the need to fund receivers in their native gas token.