Bridge any Token to Berachain with LayerZero V2

Beary Cucumber
Beary Cucumber
8 min read
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):

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.

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:

Overview of Interactions
  1. On the Source Chain, the User executes a bridging tx on the OFT Adapter contract (created by us)
  2. $UNI is locked in the OFT Adapter
  3. The LayerZero Endpoint contract sends a message containing instructions to mint $UNI tokens on the Destination Chain
  4. The message is verified by a DVN
  5. An Executor calls lzReceive() on the Endpoint contract at the Destination Chain, receiving the message
  6. $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 and fee

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.

guides/apps/layerzero-oft at main · berachain/guides
A demonstration of different contracts, languages, and libraries that work with Berachain EVM. …

🛠️ 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.

GitHub — berachain/guides: A demonstration of different contracts, languages, and libraries that…
A demonstration of different contracts, languages, and libraries that work with Berachain EVM. — berachain/guides

If you’re looking to dive deeper into the details, take a look at our Berachain Docs.

Berachain Docs
Berachain protocol core docs for learning how the blockchain works, developer guides, and understanding how to manage…

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 👏🏼