Cross-chain Ping Pong
Overview
In this section, we will create a cross-chain ping pong dApp using Router CrossTalk. Using this dApp, you can send any message (ping) from an EVM-based source chain to an EVM-based destination chain and receive an acknowledgment (pong) back to the source chain.
Step-by-Step Guide
Step 1) Installing the dependencies
Install the evm-gateway contracts with either of the following commands:
yarn add @routerprotocol/evm-gateway-contracts
npm install @routerprotocol/evm-gateway-contracts
Make sure you're using the latest version of the Gateway contracts.
Step 2) Instantiating the contract
//SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0 <0.9.0;
import "@routerprotocol/evm-gateway-contracts/contracts/IDapp.sol";
import "@routerprotocol/evm-gateway-contracts/contracts/IGateway.sol";
import "@routerprotocol/evm-gateway-contracts/contracts/Utils.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"
contract PingPong {
}
- Import the
IGateway.sol,IDapp.solandUtils.solfrom@routerprotocol/evm-gateway-contracts/contracts. - Import the
SafeERC20.solfrom@openzeppelin/contracts/token/ERC20/utils. - Inherit the
IDappcontract into your main contract (PingPong).
Step 3) Creating state variables and the constructor
address public owner;
uint64 public currentRequestId;
// srcChainId + requestId => pingFromSource
mapping(string => mapping(uint64 => string)) public pingFromSource;
// requestId => ackMessage
mapping(uint64 => string) public ackFromDestination;
// instance of the Router's gateway contract
IGateway public gatewayContract;
// custom error so that we can emit a custom error message
error CustomError(string message);
// event we will emit while sending a ping to destination chain
event PingFromSource(
string indexed srcChainId,
uint64 indexed requestId,
string message
);
event NewPing(uint64 indexed requestId);
constructor(address payable gatewayAddress, string memory feePayerAddress) {
owner = msg.sender;
gatewayContract = IGateway(gatewayAddress);
gatewayContract.setDappMetadata(feePayerAddress);
}
- Create a variable
ownerof typeaddresswhich will be used for access control. - Create a variable
currentRequestIdof typeuint64which will act as a counter for requests routed from source chain. We'll use this variable for fetching ping from the source chain on the destination side and ack from the destination chain on the source side. - Create a mapping
pingFromSourcewith keyssrcChainIdandrequestIdto fetch the string received from the source chain on the destination side. - Create a mapping
ackFromDestinationwithrequestIdas a key to the acknowledgment string received from destination chain on the source side. - Create an instance to the
gatewayContractof typeIGateway. This will be the contract that will route the message to the destination chain. - Create a
CustomErrorvariable which can be used to throw custom errors. - Create an event
NewPingwith parameterrequestIdthat will be emitted whenever a new request is created. - Create an event
PingFromSourcewith parameters -srcChainId,requestIdandmessage. It will be emitted when a cross-chain request is received on the destination chain. - Create the constructor with
gatewayAddressand thefeePayerAddressin string format.
Step 4) Setting the fee payer address
function setDappMetadata(
string memory FeePayer
) public {
require(msg.sender == owner, "Only owner can set the metadata");
gatewayContract.setDappMetadata(FeePayer);
}
- To facilitate cross-chain transactions, it is necessary to pay the fees on the Router chain. This can be achieved using the
setDappMetadatafunction available in the Gateway contracts. The function takes afeePayerAddressparameter, which represents the account responsible for covering the transaction fees for any cross-chain requests originating from the dApp. - Once the
feePayerAddressis set, the designated fee payer must approve the request to act as the fee payer on the Router chain. Without this approval, dApps will not be able to execute any cross-chain transactions. - It's important to note that any fee refunds resulting from these transactions will be credited back to the dApp's
feePayerAddresson the Router chain.
Step 5) Setting the Gateway address
function setGateway(address gateway) external {
require(msg.sender == owner, "only owner");
gatewayContract = IGateway(gateway);
}
This is an administrative function which sets the address of the Gateway contract. This function should be invoked whenever Router's Gateway contract gets updated.
Step 6) Sending a ping to the destination chain
function iPing(
string calldata destChainId,
string calldata destinationContractAddress,
string calldata str,
bytes calldata requestMetadata
) public payable {
currentRequestId++;
bytes memory packet = abi.encode(currentRequestId, str);
bytes memory requestPacket = abi.encode(destinationContractAddress, packet);
gatewayContract.iSend{ value: msg.value }(
1,
0,
string(""),
destChainId,
requestMetadata,
requestPacket
);
emit NewPing(currentRequestId);
}
Create a function named
iPing: This will be used to send a ping (message) to the destination chain. The parameters for this function includes:1)
destChainId- Network ID of the destination chain in string format.2)
destinationContractAddress- Address of the destination contract inbytesformat.3)
str- This is the message that we want to send to the destination chain contract.4)
requestMetadata- Abi-encoded metadata based on the source and destination chains. To get the request metadata, the following function can be used:```javascript
function getRequestMetadata(
uint64 destGasLimit,
uint64 destGasPrice,
uint64 ackGasLimit,
uint64 ackGasPrice,
uint128 relayerFees,
uint8 ackType,
bool isReadCall,
bytes memory asmAddress
) public pure returns (bytes memory) {
bytes memory requestMetadata = abi.encodePacked(
destGasLimit,
destGasPrice,
ackGasLimit,
ackGasPrice,
relayerFees,
ackType,
isReadCall,
asmAddress
);
return requestMetadata;
}
```More details on
requestMetadatacan be found here.Update
currentRequestId: When a user calls theiPingfunction, thecurrentRequestIdshould be incremented.Create the payload packet: For our ping pong dApp, the payload should contain the ping message and the
requestId. We'll need to abi-encode these two parameters and set it as the payload packet.Create the request packet: Abi-encode the
destinationContractAddressand the payload packet we created in the previous step and set it as the request packet.Calling the Gateway Contract to generate a cross-chain request: Call the
iSendfunction of the Gateway contract with the required parameters. The documentation for this function can be found here.
Step 7) Handling a cross-chain request
Now that we have setup the contract to send a ping from the source chain, we need to implement an iReceive function handle the request on the destination chain. The iReceive function will include the following signature:
function iReceive(
string memory requestSender,
bytes memory packet,
string memory srcChainId
) external returns (uint64, string memory) {
require(msg.sender == address(gatewayContract), "only gateway");
(uint64 requestId, string memory sampleStr) = abi.decode(
packet,
(uint64, string)
);
pingFromSource[srcChainId][requestId] = sampleStr;
emit PingFromSource(srcChainId, requestId, sampleStr);
return (requestId, sampleStr);
}
- It is important to name the function
iReceiveand ensure that its signature, including the name and parameters, remains the same. This is because the Gateway contract on the destination chain will call this function, and any changes to the name or parameters will result in a failed call. Further details on the parameters required for this function can be found here . - Ensure that only the Gateway contract can call the function, as no other contract or wallet should have access to it.
- To ensure that the request is received only from the application contract on the source chain, the application can create a mapping of allowed contract addresses for each chain ID. Then, in the
iReceivefunction, the application can check that therequestSenderis the same as the address stored in the mapping for the specific chain ID. To keep this contract as simple as possible, this condition has not been implemented here. - Decode the packet using abi decoding and store it in
requestIdandsampleStrvariables. - Check if the string received in non-empty. If it is empty, throw a custom error which will trigger a failure acknowledgment to the Router chain.
- Set the string message in
pingFromSourcemapping and emit thePingFromSourceevent withsrcChainId,requestIdand the string message. Finally, return therequestIdand the received message from the function. This will trigger a success acknowledgment to the Router chain.
Now that we have handled the request on the destination chain, we need to handle the acknowledgment on the source chain.
Step 8) Handling the acknowledgment received from destination chain
When the cross-chain request is executed on the destination chain, the destination contract triggers an acknowledgment to the source chain. This acknowledgment can be handled using the following function:
function iAck(
uint256 requestIdentifier,
bool execFlag,
bytes memory execData
) external {
(uint64 requestId, string memory ackMessage) = abi.decode(
execData,
(uint64, string)
);
ackFromDestination[requestId] = ackMessage;
}
- The function named
iAckshould be created with the same function signature as specified in the documentation. This function is called by the Gateway contract on the source chain and the function name and parameters should not be changed as it would result in a failed call. Further information about this function can be found here.
The
requestIdentifierparameter received in theiAckfunction contains the nonce that was generated by the Gateway contract when the request was initiated on the source chain.The
execFlagtells the execution status of the cross-chain request on the destination chain andexecDataconsists of the abi-encoded value returned from theiReceivefunction.If the execution is successful on the destination chain:
execFlag-[true]execData-(abi.encode(<return_value>))
Since the return value is
uint256, thisexecDatacan be decoded using abi decoding in the following way:uint256 val = abi.decode(execData, (uint256));If the execution fails on the destination chain:
execFlag-[false]execData-[abi.encode(<error>)]
Full Contract Example
//SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0 <0.9.0;
import "@routerprotocol/evm-gateway-contracts/contracts/IGateway.sol";
/// @title PingPong
/// @author Yashika Goyal
/// @notice This is a cross-chain ping pong smart contract to demonstrate how one can
/// utilise Router CrossTalk for cross-chain transactions.
contract PingPong {
address public owner;
uint64 public currentRequestId;
// srcChainId + requestId => pingFromSource
mapping(string => mapping(uint64 => string)) public pingFromSource;
// requestId => ackMessage
mapping(uint64 => string) public ackFromDestination;
// instance of the Router's gateway contract
IGateway public gatewayContract;
// custom error so that we can emit a custom error message
error CustomError(string message);
// event we will emit while sending a ping to destination chain
event PingFromSource(
string indexed srcChainId,
uint64 indexed requestId,
string message
);
event NewPing(uint64 indexed requestId);
// events we will emit while handling acknowledgment
event ExecutionStatus(uint256 indexed eventIdentifier, bool isSuccess);
event AckFromDestination(uint64 indexed requestId, string ackMessage);
constructor(address payable gatewayAddress, string memory feePayerAddress) {
owner = msg.sender;
gatewayContract = IGateway(gatewayAddress);
gatewayContract.setDappMetadata(feePayerAddress);
}
/// @notice function to set the fee payer address on Router Chain.
/// @param feePayerAddress address of the fee payer on Router Chain.
function setDappMetadata(string memory feePayerAddress) external {
require(msg.sender == owner, "only owner");
gatewayContract.setDappMetadata(feePayerAddress);
}
/// @notice function to set the Router Gateway Contract.
/// @param gateway address of the gateway contract.
function setGateway(address gateway) external {
require(msg.sender == owner, "only owner");
gatewayContract = IGateway(gateway);
}
/// @notice function to generate a cross-chain request to ping a destination chain contract.
/// @param destChainId chain ID of the destination chain in string.
/// @param destinationContractAddress contract address of the contract that will handle this
/// @param str string to be pinged to destination
/// @param requestMetadata abi-encoded metadata according to source and destination chains
function iPing(
string calldata destChainId,
string calldata destinationContractAddress,
string calldata str,
bytes calldata requestMetadata
) public payable {
currentRequestId++;
bytes memory packet = abi.encode(currentRequestId, str);
bytes memory requestPacket = abi.encode(destinationContractAddress, packet);
gatewayContract.iSend{ value: msg.value }(
1,
0,
string(""),
destChainId,
requestMetadata,
requestPacket
);
emit NewPing(currentRequestId);
}
/// @notice function to get the request metadata to be used while initiating cross-chain request
/// @return requestMetadata abi-encoded metadata according to source and destination chains
function getRequestMetadata(
uint64 destGasLimit,
uint64 destGasPrice,
uint64 ackGasLimit,
uint64 ackGasPrice,
uint128 relayerFees,
uint8 ackType,
bool isReadCall,
bytes memory asmAddress
) public pure returns (bytes memory) {
bytes memory requestMetadata = abi.encodePacked(
destGasLimit,
destGasPrice,
ackGasLimit,
ackGasPrice,
relayerFees,
ackType,
isReadCall,
asmAddress
);
return requestMetadata;
}
/// @notice function to handle the cross-chain request received from some other chain.
/// @param requestSender address of the contract on source chain that initiated the request.
/// @param packet the payload sent by the source chain contract when the request was created.
/// @param srcChainId chain ID of the source chain in string.
function iReceive(
string memory requestSender,
bytes memory packet,
string memory srcChainId
) external returns (uint64, string memory) {
require(msg.sender == address(gatewayContract), "only gateway");
(uint64 requestId, string memory sampleStr) = abi.decode(
packet,
(uint64, string)
);
pingFromSource[srcChainId][requestId] = sampleStr;
emit PingFromSource(srcChainId, requestId, sampleStr);
return (requestId, sampleStr);
}
/// @notice function to handle the acknowledgment received from the destination chain
/// back on the source chain.
/// @param requestIdentifier event nonce which is received when we create a cross-chain request
/// We can use it to keep a mapping of which nonces have been executed and which did not.
/// @param execFlag a boolean value suggesting whether the call was successfully
/// executed on the destination chain.
/// @param execData returning the data returned from the handleRequestFromSource
/// function of the destination chain.
function iAck(
uint256 requestIdentifier,
bool execFlag,
bytes memory execData
) external {
(uint64 requestId, string memory ackMessage) = abi.decode(
execData,
(uint64, string)
);
ackFromDestination[requestId] = ackMessage;
}
}