A tutorial that teaches how to use LayerZero V2 to perform cross-chain messaging from Base Goerli testnet to Optimism Goerli testnet.
curl -L https://foundry.paradigm.xyz | bash
foundryup
, to install the latest (nightly) build of FoundrylzReceive
function of the endpoint on the destination chain once a message has been verified by the Security Stack.
src/Counter.sol
, test/Counter.t.sol
, and script/Counter.s.sol
boilerplate files that were generated with the project, as you will not be needing them.forge install
.
To install LayerZero smart contracts and their dependencies, run the following commands:
foundry.toml
file by appending the following lines:
_lzSend
: A function used to send an omnichain message_lzReceive
: A function used to receive an omnichain messagesrc/
directory named ExampleContract.sol
, and add the following content:
ExampleContract
that extends the OApp
contract standard.
The contract’s constructor expects two arguments:
_endpoint
: The LayerZero Endpoint address
for the chain the smart contract is deployed to._owner
: The address
of the owner of the smart contract._lzSend
)_lzSend
function inherited from the OApp contract.
Add a new custom function named sendMessage
to your smart contract that has the following content:
sendMessage
function above calls the inherited _lzSend
function, while passing in the following expected data:
Name | Type | Description |
---|---|---|
_dstEid | uint32 | The endpoint ID of the destination chain to send the message to. |
_payload | bytes | The message (encoded) to send. |
_options | bytes | Additional options when sending the message, such as how much gas should be used when receiving the message. |
_fee | MessagingFee | The calculated fee for sending the message. |
_refundAddress | address | The address that will receive any excess fee values sent to the endpoint in case the _lzSend execution reverts. |
_quote
)_lzSend
function expects an estimated gas fee to be provided when sending a message (_fee
).
Therefore, sending a message using the sendMessage
function of your contract, you first need to estimate the associated gas fees.
There are multiple fees incurred when sending a message across chains using LayerZero, including: paying for gas on the source chain, fees paid to DVNs validating the message, and gas on the destination chain. Luckily, LayerZero bundles all of these fees together into a single fee to be paid by the msg.sender
, and LayerZero Endpoints expose a _quote
function to estimate this fee.
Add a new function to your ExampleContract
contract called estimateFee
that calls the _quote
function, as shown below:
estimateFee
function above calls the inherited _quote
function, while passing in the following expected data:
Name | Type | Description |
---|---|---|
_dstEid | uint32 | The endpoint ID of the destination chain the message will be sent to. |
_payload | bytes | The message (encoded) that will be sent. |
_options | bytes | Additional options when sending the message, such as how much gas should be used when receiving the message. |
_payInLzToken | bool | Boolean flag for which token to use when returning the fee (native or ZRO token). |
estimateFee
function should always be called immediately before calling sendMessage
to accurately estimate associated gas fees._lzReceive
)_lzReceive
function inherited from the OApp contract.
Add the following code snippet to your ExampleContract
contract to override the _lzReceive
function:
_lzReceive
function receives the following arguments when receiving a message:
Name | Type | Description |
---|---|---|
_origin | Origin | The origin information containing the source endpoint and sender address. |
_guid | bytes32 | The unique identifier for the received LayerZero message. |
payload | bytes | The payload of the received message (encoded). |
_executor | address | The address of the Executor for the received message. |
_extraData | bytes | Additional arbitrary data provided by the corresponding Executor. |
data
that you can read from later to fetch the latest message.
Add the data
field as a member variable to your contract:
_lzReceive
function allows you to provide any custom logic you wish when receiving messages, including making a call back to the source chain by invoking _lzSend
. Visit the LayerZero Message Design Patterns for common messaging flows.cast wallet import
command to import the private key of the wallet into Foundry’s securely encrypted keystore:
deployer
account in your Foundry project, run:
.env
file in the home directory of your project, and add the RPC URLs and LayerZero Endpoint information for both Base Goerli and Optimism Goerli testnets:
.env
file has been created, run the following command to load the environment variables in the current command line session:
forge create
command. The command requires you to specify the smart contract you want to deploy, an RPC URL of the network you want to deploy to, and the account you want to deploy with.
ExampleContract
smart contract to the Base Goerli testnet, run the following command:
ExampleContract
smart contract to the Optimism Goerli testnet, run the following command:
setPeer
function on the contract.
The setPeer
function expects the following arguments:
Name | Type | Description |
---|---|---|
_eid | uint32 | The endpoint ID of the destination chain. |
_peer | bytes32 | The contract address of the OApp contract on the destination chain. |
cast
command-line tool that can be used to interact with deployed smart contracts and call their functions.
To set the peer of your ExampleContract
contracts, you can use cast
to call the setPeer
function while providing the endpoint ID and address (in bytes) of the deployed contract on the respective destination chain.
To set the peer of the Base Goerli contract to the Optimism Goerli contract, run the following command:
<BASE_GOERLI_CONTRACT_ADDRESS>
with the contract address of your deployed ExampleContract
contract on Base Goerli, and<OPTIMISM_GOERLI_CONTRACT_ADDRESS>
with the contract address (as bytes) of your deployed ExampleContract
contract on Optimism Goerli before running the provided cast
command.<OPTIMISM_GOERLI_CONTRACT_ADDRESS>
with the contract address of your deployed ExampleContract
contract on Optimism Goerli, and<BASE_GOERLI_CONTRACT_ADDRESS>
with the contract address (as bytes) of your deployed ExampleContract
contract on Base Goerli before running the provided cast
command.ExampleContract
contract can be done in three steps:
estimateFee
function to estimate the gas fee for sending a messagesendMessage
function to send a messageestimateFee
and sendMessage
custom functions of the ExampleContract
contract both require a message options (_options
) argument to be provided.
Message options allow you to specify arbitrary logic as part of the message transaction, such as the gas amount the Executor pays for message delivery, the order of message execution, or dropping an amount of gas to a destination address.
LayerZero provides a Solidity library and TypeScript SDK for building these message options.
As an example, below is a Foundry script that uses OptionsBuilder from the Solidity library to generate message options (as bytes
) that set the gas amount that the Executor will pay upon message delivery to 200000
wei:
cast
command to call the estimateFee()
function of the ExampleContract
contract.
To estimate the gas fee for sending a message from Base Goerli to Optimism Goerli, run the following command:
<BASE_GOERLI_CONTRACT_ADDRESS>
with the contract address of your deployed ExampleContract
contract on Base Goerli before running the provided cast
command.estimateFee(uint32, string, bytes, bool)
, while providing the required arguments, including: the endpoint ID of the destination chain, the text to send, and the message options (generated in the last section).
sendMessage
and provide the value returned as the msg.value
.
For example, to send a message from Base Goerli to Optimism Goerli with an estimated gas fee, run the following command:
<BASE_GOERLI_CONTRACT_ADDRESS>
with the contract address of your deployed ExampleContract
contract on Base Goerli, and <GAS_ESTIMATE_IN_WEI>
with the gas estimate (in wei) returned by the call to estimateFee, before running the provided cast
command.