Bridge any Token to Berachain with LayerZero V2
Deploying Omnichain Tokens Using LayerZero V2
Introduction to OFTs
Omnichain Fungible Tokens (OFTs) are a new token standard pioneered by LayerZero for cross-chain assets. OFTs allow fungible tokens to be transferred across different blockchains without the need for asset wrapping or liquidity pools, all whilst maintaining a unified supply.
LayerZero is a general-purpose cross-chain messaging protocol, but in this article, we will focus on its bridging applications, specifically OFTs.
LayerZero V2
Launched in January 2024, LayerZero V2 brings significant upgrades by separating message verification from execution (these were bundled together in LayerZero V1 using Relayers):
- Application owners can choose a set of Decentralized Verifier Networks (DVNs) to verify message payloads; and
- Execution of messages on destination chains is handled by Executors
Among other improvements, these changes enhance the scalability and customization available to omnichain app developers. See this article for a deeper dive.
In this article, we show you how to leverage LayerZero OFTs to bridge $UNI
tokens from Sepolia Testnet to the Berachain Testnet.
📋 Requirements
Before we move on, make sure you have the following installed and set up on your computer.
- Node
v20.11.0
or greater - pnpm
- A wallet configured with Berachain bArtio Network and Sepolia Testnet
$BERA
or Berachain Testnet Tokens in that wallet — see Berachain Faucet$ETH
on Sepolia Testnet in that wallet — see Sepolia Faucet- Obtain Sepolia
$UNI
— see Uniswap (0.0001 is used in this guide)
Foundry
This guide requires Foundry to be installed. In a terminal window, run:
curl -L https://foundry.paradigm.xyz | bash;
foundryup;
# foundryup installs the 'forge' and 'cast' binaries, used later
For more installation instructions, see Foundry’s installation guide. For more details using Foundry on Berachain, see this guide.
Creating Our LayerZero Project
First, we’re going to set up your development environment using Foundry.
Start by creating a new project folder and initializing Foundry:
forge init layerzero-oft --no-git --no-commit;
cd layerzero-oft;
# We observe the following basic layout
# .
# ├── foundry.toml
# ├── script
# │ └── Counter.s.sol
# ├── src
# │ └── Counter.sol
# └── test
# └── Counter.t.sol
Installing Dependencies
We will be leveraging a number of dependencies related to LayerZero and OpenZeppelin tooling, so install them below:
# FROM: ./layerzero-oft
pnpm init;
pnpm add -D @layerzerolabs/lz-evm-oapp-v2 @layerzerolabs/toolbox-foundry @layerzerolabs/lz-evm-protocol-v2 @layerzerolabs/lz-evm-messagelib-v2 @layerzerolabs/lz-definitions @openzeppelin/contracts --ignore-workspace;
Replace the contents of foundry.toml
with the following to remap our dependencies:
[profile.default]
src = "src"
out = "out"
libs = [
'node_modules/@layerzerolabs/toolbox-foundry/lib',
'node_modules',
]
remappings = [
'forge-std/=node_modules/@layerzerolabs/toolbox-foundry/lib/forge-std',
'@layerzerolabs/=node_modules/@layerzerolabs/',
'@openzeppelin/=node_modules/@openzeppelin/',
]
Overview of Interactions
Before we write the contracts, let’s briefly go over the interactions involved in the process of bridging the $UNI
token from Sepolia to Berachain:
- On the Source Chain, the User executes a bridging tx on the
OFT Adapter
contract (created by us) $UNI
is locked in theOFT Adapter
- The LayerZero Endpoint contract sends a message containing instructions to mint
$UNI
tokens on the Destination Chain - The message is verified by a DVN
- An Executor calls
lzReceive()
on the Endpoint contract at the Destination Chain, receiving the message $lzUNI
(an OFT created by us) is minted and sent to the User
Writing the Smart Contracts
We’re going to need a number of smart contract deployments to facilitate the process above, on both the Source and Destination Chains.
Create a new file ./src/MyAdapter.sol
and paste in the following:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import {OFTAdapter} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTAdapter.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MyAdapter is OFTAdapter {
constructor(
address _token, // a deployed, already existing ERC20 token address
address _layerZeroEndpoint, // local endpoint address
address _delegate // token owner used as a delegate in LayerZero Endpoint
) OFTAdapter(_token, _layerZeroEndpoint, _delegate) Ownable(_delegate) {}
}
This contract simply implements the OFTAdapter
contract, which we recall is the contract on the Source Chain responsible for locking tokens and extending cross-chain functionality to existing tokens. Learn more about OFT Adapters here.
Additionally, a contract delegate
is set, granting this address the ability to handle critical tasks such as setting configurations.
Next, create a file ./src/MyOFT.sol
and paste in the following:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {OFT} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol";
contract MyOFT is OFT {
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}
}
This contract implements the OFT
contract, and is where we define the token details on the Destination Chain (e.g. name
, symbol
). Again, a delegate
is defined.
(You may delete src/Counter.sol
, test/Counter.t.sol
and script/Counter.s.sol
which were generated with the project).
Deployment and Execution
Step 1: Deploy OFT Adapter to Sepolia
Create a /.env
file at the project root with the following and populate your PRIVATE_KEY
to start:
PRIVATE_KEY=
SEPOLIA_ADAPTER_ADDRESS=
BERACHAIN_OFT_ADDRESS=
We’re going to create a couple of Foundry scripts to help with deployment. Start by creating ./script/MyAdapter.s.sol
and populate it with the following:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import {Script} from "forge-std/Script.sol";
import "../src/MyAdapter.sol";
contract MyAdapterScript is Script {
address constant UNI_TOKEN = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
address constant LAYERZERO_ENDPOINT =
0x6EDCE65403992e310A62460808c4b910D972f10f;
function run() public {
// Setup
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
// Deploy
MyAdapter myAdapter = new MyAdapter(
UNI_TOKEN,
LAYERZERO_ENDPOINT,
vm.addr(privateKey) // Address of private key
);
vm.stopBroadcast();
}
}
Running the following command will deploy the MyAdapter
contract on Sepolia Testnet.
(optionally add flags — etherscan-api-key YOUR_ETHERSCAN_API_KEY — verify
to verify the contract):
# FROM: ./layerzero-oft
forge script script/MyAdapter.s.sol --rpc-url https://rpc.sepolia.org/ --broadcast
# [Example Output]:
# ##### sepolia
# ✅ [Success]Hash: 0x16702f69752f1f7243793202435da6bd54d2ebec89294728c2bf0d55584ed732
# Contract Address: 0xB66e0518570eA48286983322fc8F85301f955406
# Block: 5525968
# Paid: 0.007448778064556076 ETH (2482926 gas * 3.000000026 gwei)
Update SEPOLIA_ADAPTER_ADDRESS
in your .env
file with the address of your MyAdapter
deployment.
Step 2: Deploy OFT to Berachain
Next, create ./script/MyOFT.s.sol
and populate it with the following:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import {Script} from "forge-std/Script.sol";
import "../src/MyOFT.sol";
contract MyOFTScript is Script {
address constant LAYERZERO_ENDPOINT =
0x6EDCE65403992e310A62460808c4b910D972f10f;
uint32 constant SEPOLIA_ENDPOINT_ID = 40161;
function run() public {
// Setup
address SEPOLIA_ADAPTER_ADDRESS = vm.envAddress(
"SEPOLIA_ADAPTER_ADDRESS"
);
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
// Deploy
MyOFT myOFT = new MyOFT(
"Layer Zero UNI",
"lzUNI",
LAYERZERO_ENDPOINT,
vm.addr(privateKey) // Wallet address of signer
);
// Hook up Berachain OFT to Sepolia's adapter
myOFT.setPeer(
SEPOLIA_ENDPOINT_ID,
bytes32(uint256(uint160(SEPOLIA_ADAPTER_ADDRESS)))
);
vm.stopBroadcast();
}
}
Next, execute your script to 1) deploy your OFT contract to Berachain, and 2) make it aware of its Sepolia peer contract:
# FROM: ./layerzero-oft
forge script script/MyOFT.s.sol --rpc-url https://bartio.rpc.berachain.com/ --broadcast
# [Example Output]
# ##### 80085
# ✅ [Success]Hash: 0x16cf8daa6f335fb65dedee8e722c01adb45b87aeccad0d6dc751d6c04c466a5f
# Contract Address: 0x42993d9A691636cbb23C201729b36B5C6e750733
# Block: 1147280
# Paid: 0.040444634965884468 ETH (2842719 gas * 14.227447372 gwei)
#
# ##### 80085
# ✅ [Success]Hash: 0xd670e01e028fe50d9e8c323007a777590d202fada28d80d6a9f9973abcb8b607
# Block: 1147281
# Paid: 0.000682799744815656 ETH (47666 gas * 14.324670516 gwei)
Update BERACHAIN_OFT_ADDRESS
in your .env
file with the address of your MyOFT
deployment.
Step 3: Bridge Tokens from Sepolia to Berachain
Now, we’re going to tie all of the components together and bridge $UNI
from Sepolia to Berachain.
Create ./script/Bridge.s.sol
and populate it with the following:
pragma solidity ^0.8.22;
import "forge-std/Script.sol";
import {IOFT, SendParam} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/interfaces/IOFT.sol";
import {IOAppCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/interfaces/IOAppCore.sol";
import {MessagingFee} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol";
import {OptionsBuilder} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/libs/OptionsBuilder.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IAdapter is IOAppCore, IOFT {}
contract SendOFTScript is Script {
using OptionsBuilder for bytes;
uint32 constant BERACHAIN_ENPOINT_ID = 40291;
address constant SEPOLIA_UNI_ADDRESS =
0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984;
function run() external {
address SEPOLIA_ADAPTER_ADDRESS = vm.envAddress(
"SEPOLIA_ADAPTER_ADDRESS"
);
address BERACHAIN_OFT_ADDRESS = vm.envAddress("BERACHAIN_OFT_ADDRESS");
uint256 privateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(privateKey);
address signer = vm.addr(privateKey);
// Get the Adapter contract instance
IAdapter sepoliaAdapter = IAdapter(SEPOLIA_ADAPTER_ADDRESS);
// Hook up Sepolia Adapter to Berachain's OFT
sepoliaAdapter.setPeer(
BERACHAIN_ENPOINT_ID,
bytes32(uint256(uint160(BERACHAIN_OFT_ADDRESS)))
);
// Define the send parameters
uint256 tokensToSend = 0.0001 ether; // 0.0001 $UNI tokens
bytes memory options = OptionsBuilder
.newOptions()
.addExecutorLzReceiveOption(200000, 0);
SendParam memory sendParam = SendParam(
BERACHAIN_ENPOINT_ID,
bytes32(uint256(uint160(signer))),
tokensToSend,
tokensToSend,
options,
"",
""
);
// Quote the send fee
MessagingFee memory fee = sepoliaAdapter.quoteSend(sendParam, false);
console.log("Native fee: %d", fee.nativeFee);
// Approve the OFT contract to spend UNI tokens
IERC20(SEPOLIA_UNI_ADDRESS).approve(
SEPOLIA_ADAPTER_ADDRESS,
tokensToSend
);
// Send the tokens
sepoliaAdapter.send{value: fee.nativeFee}(sendParam, fee, signer);
console.log("Tokens bridged successfully!");
}
}
Let’s break down what this script does:
- We first attach to an instance of the deployed Sepolia Adapter
- We then call
setPeer
to inform the Adapter of the OFT contract on Berachain - Options are defined to define the gas amounts the Executor pays for message delivery (read more here), prepare a message payload, and request a
fee
quote for this transaction - We approve the Adapter contract to spend our
$UNI
tokens - Finally, the Adapter’s
send
method is invoked with the message payload andfee
Run the script with the following:
# FROM: ./layerzero-oft
forge script script/Bridge.s.sol --rpc-url https://rpc.sepolia.org/ --broadcast
After a few moments to wait for the a LayerZero Executor to pick up the request, we will see the bridged $lzUNI
tokens show up in our Berachain wallet!
Recap
Congratulations, you’ve successfully bridged an existing token to Berachain using LayerZero OFTs 🎉
To learn more about OFTs and all that cross-chain messaging enables, we invite you to check out the LayerZero docs.
Disclaimer: The OFT and OFT Adapter are solutions for protocol developers to bring their protocol’s token to new chains. Unsanctioned OFT deployments of existing tokens are unlikely to gain support.
🐻 Full Code Repository
If you want to see the final code and see other guides, check out Berachain LayerZero Guide Code.
🛠️ Want To Build More?
Want to build more on Berachain and see more examples. Take a look at our Berachain GitHub Guides Repo for a wide variety of implementations that include NextJS, Hardhat, Viem, Foundry, and more.
If you’re looking to dive deeper into the details, take a look at our Berachain Docs.
Looking For Dev Support?
Make sure to join our Berachain Discord server and check out our developer channels to ask questions.
❤️ Don’t forget to show some love for this article 👏🏼