Sending cross-chain message
Introduce
To send a cross-chain message, you need to call the sendMessage function on the source chain contract. This function will transmit a payload to a specified endpoint on the destination chain. Once the message is received, the xOracleCall function on the destination chain contract will be executed as a callback to handle the payload.
Contract A (Source Chain): Sending a message to contract B (called endpoint) on the destination chain.
/**
* @dev Send message to destination chain
* @param _payload payload to calldata to endpoint
* @param _endpoint endpoint address on destination chain
* @param _dstChainId destination chain id
*/
function sendMessage(
bytes memory _payload,
address _endpoint,
uint64 _dstChainId
)
Contract B (Destination Chain): Handling the fulfill message callback.
/**
* @dev Callback from xOracleMessage (sent from another chain)
* @param _payload payload
*/
function xOracleCall(bytes memory _payload)
The send message fee is paid with a native token (e.g., ETH). You can obtain the fee amount using the getFee
function.
function getFee(uint64 dstChainId) external view returns(uint256)
These implementations facilitate communication between smart contracts across different blockchain networks, enabling cross-chain functionality through payload transmission and receipt handling.
Let's start
Example: Crosschain ERC20 Token
This example demonstrates how to implement a crosschain ERC20 token that can be transferred between blockchain networks with xOracle Message.
Import Necessary Contracts and Interfaces.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IXOracleMessage.sol";
Implement
contractor
and initialize. In the constructor, initialize the ERC20 token with name, symbol, xOracleMessage address, and initial supply.
constructor(
string memory _name,
string memory _symbol,
address _xOracleMessage,
uint256 _initialSupply
) ERC20(_name, _symbol) {
require(_xOracleMessage != address(0), "invalid address");
chainId = uint64(block.chainid);
xOracleMessage = _xOracleMessage;
// mint initial supply
_mint(msg.sender, _initialSupply);
}
Implement
sendCrosschain
function to send tokens across chains.
/**
* @dev Send token to another chain
* @param _dstChainId destination chain id
* @param _receiver receiver address
* @param _amount amount to transfer
*/
function sendCrosschain(uint64 _dstChainId, address _receiver, uint256 _amount) external payable {
require(balanceOf(msg.sender) >= _amount, "ERC20: transfer amount exceeds balance");
address endpoint = endpointTokens[_dstChainId];
require(endpoint != address(0), "chainId not supported");
// check fee
uint256 fee = IXOracleMessage(xOracleMessage).getFee(_dstChainId);
require(msg.value >= fee, "insufficient fee");
// send message
bytes memory payload = abi.encode(chainId, msg.sender, _receiver, _amount);
IXOracleMessage(xOracleMessage).sendMessage{ value: msg.value }(payload, endpoint, _dstChainId);
// burn from sender
_burn(msg.sender, _amount);
emit SendCrosschain(_dstChainId, msg.sender, _receiver, _amount);
}
Implement
getFee
function to get the required fee from the xOracle message contract.
/**
* @dev Get fee for sending crosschain
* @param _dstChainId destination chain id
*/
function getFee(uint64 _dstChainId) public view returns (uint256) {
return IXOracleMessage(xOracleMessage).getFee(_dstChainId);
}
Implement
xOracleCall
function to handle incoming messages from other chains.
/**
* @dev Callback from xOracleMessage (sent from another chain)
* @param _payload payload
*/
function xOracleCall(bytes memory _payload) external {
// check security callback
require(msg.sender == xOracleMessage, "only xOracleMessage callback");
// decode payload
(
uint64 _srcChainId,
address _from,
address _receiver,
uint256 _amount
) = abi.decode(_payload, (uint64, address, address, uint256));
// mint to receiver
_mint(_receiver, _amount);
emit ReceivedCrosschain(_srcChainId, _from, _receiver, _amount);
}
Implement
setEndpointToken
to register the endpoint address and destination chain id.
/**
* @dev Set endpoint token address which is deployed on another chain
* @param _dstChainId destination chain id
* @param _token token address
*/
function setEndpointToken(uint64 _dstChainId, address _token) external onlyOwner {
endpointTokens[_dstChainId] = _token;
emit SetEndpointToken(_dstChainId, _token);
}
Full Code
Here is the full example code, including the necessary imports and functions.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IXOracleMessage.sol";
contract CrosschainERC20Token is ERC20, Ownable {
uint64 public immutable chainId;
address public xOracleMessage;
mapping (uint64 => address) public endpointTokens;
event SendCrosschain(uint64 dstChainId, address from, address receiver, uint256 amount);
event ReceivedCrosschain(uint64 srcChainId, address from, address receiver, uint256 amount);
event SetEndpointToken(uint64 dstChainId, address token);
constructor(
string memory _name,
string memory _symbol,
address _xOracleMessage,
uint256 _initialSupply
) ERC20( _name, _symbol) {
require(_xOracleMessage != address(0), "invalid address");
chainId = uint64(block.chainid);
xOracleMessage = _xOracleMessage;
// mint initial supply
_mint(msg.sender, _initialSupply);
}
/**
* @dev Send token to another chain
* @param _dstChainId destination chain id
* @param _receiver receiver address
* @param _amount amount to transfer
*/
function sendCrosschain(uint64 _dstChainId, address _receiver, uint256 _amount) external payable {
require(balanceOf(msg.sender) >= _amount, "ERC20: transfer amount exceeds balance");
address endpoint = endpointTokens[_dstChainId];
require(endpoint != address(0), "chainId not supported");
// check fee
uint256 fee = IXOracleMessage(xOracleMessage).getFee(_dstChainId);
require(msg.value >= fee, "insufficient fee");
// send message
bytes memory payload = abi.encode(chainId, msg.sender, _receiver, _amount);
IXOracleMessage(xOracleMessage).sendMessage{ value: msg.value }(payload, endpoint, _dstChainId);
// burn from sender
_burn(msg.sender, _amount);
emit SendCrosschain(_dstChainId, msg.sender, _receiver, _amount);
}
/**
* @dev Callback from xOracleMessage (sent from another chain)
* @param _payload payload
*/
function xOracleCall(bytes memory _payload) external {
// check security callback
require(msg.sender == xOracleMessage, "only xOracleMessage callback");
// decode payload
(
uint64 _srcChainId,
address _from,
address _receiver,
uint256 _amount
) = abi.decode(_payload, (uint64, address, address, uint256));
// mint to receiver
_mint(_receiver, _amount);
emit ReceivedCrosschain(_srcChainId, _from, _receiver, _amount);
}
/**
* @dev Set endpoint token address which is deployed on another chain
* @param _dstChainId destination chain id
* @param _token token address
*/
function setEndpointToken(uint64 _dstChainId, address _token) external onlyOwner {
endpointTokens[_dstChainId] = _token;
emit SetEndpointToken(_dstChainId, _token);
}
/**
* @dev Faucet for receive 1,000 tokens
*/
function faucet() external {
uint256 faucetAmount = 1000 * 10**18;
_mint(msg.sender, faucetAmount);
}
/**
* @dev Get fee for sending crosschain
* @param _dstChainId destination chain id
*/
function getFee(uint64 _dstChainId) public view returns (uint256) {
return IXOracleMessage(xOracleMessage).getFee(_dstChainId);
}
}
For more details, you can refer to the full code on GitHub.
Deployment notes
Deploy
CrosschainERC20Token
contract on both chains.
Example deployment:
Holesky Chain (chainId 17000)
Sepolia Chain (chainId 11155111)
Configure
setEndpointToken
to contracts on both chains to register the other's endpoint address and chain id.
Holesky Chain Configuration
// crosschainERC20Token on holesky chain
await crosschainERC20Token.setEndpointToken(
11155111, // sepolia chain id
"0x9712698c1c91e3e5c39e3c63fdb94e39684183fd" // CrosschainERC20Token deployed address on sepolia
);
Sepolia Chain Configuration
// crosschainERC20Token on sepolia chain
await crosschainERC20Token.setEndpointToken(
17000, // holesky chain id
"0x9057a36856116a7100a3cb9c7f676d1477b71c43" // CrosschainERC20Token deployed address on holesky
);
Get xOracle message fee and send tokens crosschain transfer from Holesky to Sepolia.
Sending Tokens from Holesky to Sepolia
// send from holesky to sepolia
const destChainId = 11155111; // sepolia
const receiver = signer.address;
const amount = '500000000000000000000'; // 500
const fee = await crosschainERC20Token.getFee(destChainId);
await crosschainERC20Token.sendCrosschain(
destChainId,
receiver,
amount,
{ value: fee }
);
Playground
The contract CrosschainERC20Token.sol
is deployed on testnet chains. You can interact with the CrosschainERC20Token contract on the block explorer, by following these steps:
Go to
CrosschainERC20Token
contract on block explorer.On the
Write Contract
tab, call thefaucet
function to receive faucet 1,000 tokens.On the
Read Contract
tab, call thegetFee
function to get required xOracle message fee with the following parameter:_dstChainId
: The destination chain ID (e.g., Sepolia chain ID 11155111). This will return the required xOracle message fee in wei.On the
Write Contract
tab, call thesendCrosschain
function with the following parameters:payableAmount (ether)
: The xOracle message fee converted to ether (wei divided by 10^18 e.g., 10000000000000000 wei / 10^18 = 0.01)_dstChainId
: The destination chain ID (e.g., Sepolia chain ID 11155111)._receiver
: The wallet address to receive the tokens on the destination chain._amount
: The amount of tokens to transfer in wei. (e.g., 500 tokens * 10^18 = 500000000000000000000)
Playground deployed address
Last updated