Skip to main content
Version: Endpoint V2

Foundry

Foundry is a fast, portable, and modular toolkit for Ethereum application development. For LayerZero development, the lz-address-book provides a pure Solidity workflow with all protocol addresses embedded as compile-time constants.

Installation

Install the address book with a single command:

forge install LayerZero-Labs/lz-address-book

Add these remappings to remappings.txt in your project root:

@layerzerolabs/lz-evm-protocol-v2/=lib/lz-address-book/lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/
@layerzerolabs/lz-evm-messagelib-v2/=lib/lz-address-book/lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib/
@layerzerolabs/oft-evm/=lib/lz-address-book/lib/devtools/packages/oft-evm/
@layerzerolabs/oapp-evm/=lib/lz-address-book/lib/devtools/packages/oapp-evm/
@openzeppelin/contracts/=lib/lz-address-book/lib/openzeppelin-contracts/contracts/
solidity-bytes-utils/=lib/lz-address-book/lib/solidity-bytes-utils/
forge-std/=lib/forge-std/src/
lz-address-book/=lib/lz-address-book/src/

Verify the installation:

forge build

The address book bundles all LayerZero V2 dependencies (protocol contracts, OApp/OFT packages, OpenZeppelin) and embeds deployment addresses for 320+ EVM chains.

LZAddressContext

LZAddressContext provides dynamic address lookups based on the current chain context. Set your chain once, then fetch any LayerZero address without hardcoding.

Basic Usage

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Test, console} from "forge-std/Test.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";

contract AddressLookupTest is Test {
LZAddressContext ctx;

function setUp() public {
ctx = new LZAddressContext();
}

function test_getAddresses() public {
// Set chain context (choose one method)
ctx.setChain("arbitrum-mainnet"); // by name
// ctx.setChainByEid(30110); // by LayerZero EID
// ctx.setChainByChainId(42161); // by native chain ID

// Get protocol addresses
address endpoint = ctx.getEndpointV2();
address sendLib = ctx.getSendUln302();
address receiveLib = ctx.getReceiveUln302();
address executor = ctx.getExecutor();
address dvn = ctx.getDVNByName("LayerZero Labs");

console.log("Endpoint:", endpoint);
console.log("DVN:", dvn);

// Verify against known addresses
assertEq(endpoint, 0x1a44076050125825900e736c501f859c50fE728c);
}
}

Chain Identifiers

ChainNameEIDChain ID
Ethereumethereum-mainnet301011
Arbitrumarbitrum-mainnet3011042161
Basebase-mainnet301848453
Optimismoptimism-mainnet3011110
Polygonpolygon-mainnet30109137

Use ctx.getSupportedChainNames() to list all 320+ supported chains.

Cross-Chain Lookups

Query addresses on remote chains without switching context:

// Get destination chain info without switching context
uint32 baseEid = ctx.getEidForChainName("base-mainnet");
address baseEndpoint = ctx.getEndpointForChainName("base-mainnet");
address baseDvn = ctx.getDVNForChainName("LayerZero Labs", "base-mainnet");

DVN Discovery

// List all available DVNs
string[] memory allDvns = ctx.getAvailableDVNs();

// List DVNs for current chain
(string[] memory names, address[] memory addrs) = ctx.getDVNsForCurrentChain();

// Get sorted DVN addresses for UlnConfig (required to be ascending order)
string[] memory dvnNames = new string[](2);
dvnNames[0] = "LayerZero Labs";
dvnNames[1] = "Nethermind";
address[] memory sortedDvns = ctx.getSortedDVNs(dvnNames);

Deployment Scripts

Auto-detect the current chain and deploy to any LayerZero-supported network with the same script:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";
import {MyOFT} from "../src/MyOFT.sol";

contract DeployOFT is Script {
function run() external {
LZAddressContext ctx = new LZAddressContext();
ctx.setChainByChainId(block.chainid); // Auto-detect from RPC

address endpoint = ctx.getEndpointV2();
address owner = msg.sender;

vm.startBroadcast();
MyOFT oft = new MyOFT("My Token", "MTK", endpoint, owner);
vm.stopBroadcast();

console.log("Deployed to:", address(oft));
console.log("Chain:", ctx.getCurrentChainName());
console.log("Endpoint:", endpoint);
}
}

Deploy to any chain:

forge script script/DeployOFT.s.sol --rpc-url arbitrum --broadcast --account deployer
forge script script/DeployOFT.s.sol --rpc-url base --broadcast --account deployer

Configuration Scripts

Configure DVNs, libraries, and peers using the address book. You can either configure individual pathways or use an all-in-one script.

All-in-One Multi-Chain Configuration

For production deployments, use a single script to configure all chains. Run this script once per chain—it auto-detects via block.chainid and configures all pathways:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";
import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import {SetConfigParam} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol";
import {UlnConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
import {ExecutorConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";
import {IOAppCore} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol";
import {IOAppOptionsType3, EnforcedOptionParam} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppOptionsType3.sol";
import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

/// @title ConfigureOApp
/// @notice Configure an OApp across multiple chains - run once per chain
/// @dev Auto-detects chain via block.chainid, configures all pathways
contract ConfigureOApp is Script {
using OptionsBuilder for bytes;

struct ChainConfig {
string chainName;
address oapp;
uint64 confirmations;
bytes sendOptions;
}

struct LocalContext {
address oapp;
address endpoint;
address sendLib;
address receiveLib;
address executor;
address[] dvns;
uint64 confirmations;
}

string constant DVN_1 = "LayerZero Labs";
string constant DVN_2 = "Nethermind";

function run() external {
// ============================================================
// STEP 1: Define all chain configurations (MODIFY FOR YOUR DEPLOYMENT)
// ============================================================
ChainConfig[] memory chains = new ChainConfig[](3);

chains[0] = ChainConfig({
chainName: "arbitrum-mainnet",
oapp: 0x1111111111111111111111111111111111111111, // Your Arbitrum OApp
confirmations: 1,
sendOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(80_000, 0)
});

chains[1] = ChainConfig({
chainName: "base-mainnet",
oapp: 0x2222222222222222222222222222222222222222, // Your Base OApp
confirmations: 1,
sendOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(80_000, 0)
});

chains[2] = ChainConfig({
chainName: "optimism-mainnet",
oapp: 0x3333333333333333333333333333333333333333, // Your Optimism OApp
confirmations: 1,
sendOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(80_000, 0)
});

// ============================================================
// STEP 2: Find current chain and setup context
// ============================================================
(uint256 localIndex, LocalContext memory local, uint32[] memory eids) = _setup(chains);

if (local.endpoint == address(0)) {
console.log("Chain", block.chainid, "not in config. Skipping.");
return;
}

console.log("=== Configuring chain", block.chainid, "===");
console.log("OApp:", local.oapp);

// ============================================================
// STEP 3: Configure pathways to all remote chains
// ============================================================
vm.startBroadcast();

for (uint256 i = 0; i < chains.length; i++) {
if (i == localIndex) continue;
_configurePathway(local, eids[i], chains[i]);
console.log("Configured pathway to", chains[i].chainName);
}

vm.stopBroadcast();
console.log("=== Done ===");
}

function _setup(ChainConfig[] memory chains)
internal
returns (uint256 localIndex, LocalContext memory local, uint32[] memory eids)
{
LZAddressContext ctx = new LZAddressContext();
eids = new uint32[](chains.length);
localIndex = type(uint256).max;

for (uint256 i = 0; i < chains.length; i++) {
ctx.setChain(chains[i].chainName);
eids[i] = ctx.getCurrentEID();
if (ctx.getCurrentChainId() == block.chainid) {
localIndex = i;
}
}

if (localIndex == type(uint256).max) return (0, local, eids);

ctx.setChain(chains[localIndex].chainName);

string[] memory dvnNames = new string[](2);
dvnNames[0] = DVN_1;
dvnNames[1] = DVN_2;

local = LocalContext({
oapp: chains[localIndex].oapp,
endpoint: ctx.getEndpointV2(),
sendLib: ctx.getSendUln302(),
receiveLib: ctx.getReceiveUln302(),
executor: ctx.getExecutor(),
dvns: ctx.getSortedDVNs(dvnNames),
confirmations: chains[localIndex].confirmations
});
}

function _configurePathway(LocalContext memory local, uint32 remoteEid, ChainConfig memory remote) internal {
ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(local.endpoint);

// 1. Set libraries
endpoint.setSendLibrary(local.oapp, remoteEid, local.sendLib);
endpoint.setReceiveLibrary(local.oapp, remoteEid, local.receiveLib, 0);

// 2. Set send config
SetConfigParam[] memory sendParams = new SetConfigParam[](2);
sendParams[0] = SetConfigParam({
eid: remoteEid,
configType: 1,
config: abi.encode(ExecutorConfig({maxMessageSize: 10000, executor: local.executor}))
});
sendParams[1] = SetConfigParam({
eid: remoteEid,
configType: 2,
config: abi.encode(UlnConfig({
confirmations: remote.confirmations,
requiredDVNCount: 2,
optionalDVNCount: 0,
optionalDVNThreshold: 0,
requiredDVNs: local.dvns,
optionalDVNs: new address[](0)
}))
});
endpoint.setConfig(local.oapp, local.sendLib, sendParams);

// 3. Set receive config
SetConfigParam[] memory recvParams = new SetConfigParam[](1);
recvParams[0] = SetConfigParam({
eid: remoteEid,
configType: 2,
config: abi.encode(UlnConfig({
confirmations: local.confirmations,
requiredDVNCount: 2,
optionalDVNCount: 0,
optionalDVNThreshold: 0,
requiredDVNs: local.dvns,
optionalDVNs: new address[](0)
}))
});
endpoint.setConfig(local.oapp, local.receiveLib, recvParams);

// 4. Set enforced options
EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1);
enforcedOptions[0] = EnforcedOptionParam({eid: remoteEid, msgType: 1, options: remote.sendOptions});
IOAppOptionsType3(local.oapp).setEnforcedOptions(enforcedOptions);

// 5. Set peer
IOAppCore(local.oapp).setPeer(remoteEid, bytes32(uint256(uint160(remote.oapp))));
}
}

Run on each chain:

forge script script/ConfigureOApp.s.sol --rpc-url arbitrum --broadcast --account deployer
forge script script/ConfigureOApp.s.sol --rpc-url base --broadcast --account deployer
forge script script/ConfigureOApp.s.sol --rpc-url optimism --broadcast --account deployer

Individual Configuration Scripts

For more granular control, configure each component separately:

Set DVN and Executor Config

Configure DVNs, block confirmations, and executor settings for a single pathway:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";
import {ILayerZeroEndpointV2, SetConfigParam} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import {UlnConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol";
import {ExecutorConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol";

contract SetConfig is Script {
uint32 constant EXECUTOR_CONFIG_TYPE = 1;
uint32 constant ULN_CONFIG_TYPE = 2;

function run() external {
LZAddressContext ctx = new LZAddressContext();
ctx.setChainByChainId(block.chainid);

// Get local protocol addresses
address endpoint = ctx.getEndpointV2();
address sendLib = ctx.getSendUln302();
address executor = ctx.getExecutor();

// Get DVN addresses (sorted for UlnConfig - required by protocol)
string[] memory dvnNames = new string[](2);
dvnNames[0] = "LayerZero Labs";
dvnNames[1] = "Nethermind";
address[] memory dvns = ctx.getSortedDVNs(dvnNames);

// Remote chain config
uint32 remoteEid = ctx.getEidForChainName("base-mainnet");

// Build UlnConfig - specifies DVNs and confirmations
UlnConfig memory ulnConfig = UlnConfig({
confirmations: 15, // Block confirmations before DVN verification
requiredDVNCount: 2, // Number of required DVNs
optionalDVNCount: 0, // Number of optional DVNs
optionalDVNThreshold: 0, // Threshold for optional DVNs
requiredDVNs: dvns, // Must be sorted in ascending order
optionalDVNs: new address[](0)
});

// Build ExecutorConfig - specifies executor and message limits
ExecutorConfig memory execConfig = ExecutorConfig({
maxMessageSize: 10000, // Max payload size in bytes
executor: executor // Executor address from address book
});

// Encode configs into SetConfigParam array
SetConfigParam[] memory params = new SetConfigParam[](2);
params[0] = SetConfigParam(remoteEid, EXECUTOR_CONFIG_TYPE, abi.encode(execConfig));
params[1] = SetConfigParam(remoteEid, ULN_CONFIG_TYPE, abi.encode(ulnConfig));

address oapp = vm.envAddress("OAPP_ADDRESS");

vm.startBroadcast();
ILayerZeroEndpointV2(endpoint).setConfig(oapp, sendLib, params);
vm.stopBroadcast();

console.log("Configured OApp:", oapp);
console.log("Remote EID:", remoteEid);
}
}

Run on any chain:

forge script script/SetConfig.s.sol --rpc-url arbitrum --broadcast --account deployer

Set Peers

Register remote OApp addresses to enable cross-chain messaging:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";
import {OApp} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";

contract SetPeers is Script {
function run() external {
LZAddressContext ctx = new LZAddressContext();
ctx.setChainByChainId(block.chainid);

address oapp = vm.envAddress("OAPP_ADDRESS");

// Remote OApp addresses (from your deployment artifacts)
address basePeer = vm.envAddress("BASE_PEER");
address arbPeer = vm.envAddress("ARB_PEER");

// Get EIDs from address book (no hardcoding needed)
uint32 baseEid = ctx.getEidForChainName("base-mainnet");
uint32 arbEid = ctx.getEidForChainName("arbitrum-mainnet");

vm.startBroadcast();

// Convert address to bytes32 (required for non-EVM compatibility)
OApp(oapp).setPeer(baseEid, bytes32(uint256(uint160(basePeer))));
OApp(oapp).setPeer(arbEid, bytes32(uint256(uint160(arbPeer))));

vm.stopBroadcast();

console.log("Set peer for Base EID:", baseEid);
console.log("Set peer for Arbitrum EID:", arbEid);
}
}
warning

setPeer opens the messaging channel. Configure libraries, DVNs, and enforced options before calling setPeer to ensure proper security settings are in place.

Fork Testing

For multi-chain fork tests, persist the context across fork switches:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Test, console} from "forge-std/Test.sol";
import {LZAddressContext} from "lz-address-book/helpers/LZAddressContext.sol";
import {MyOFT} from "../src/MyOFT.sol";

contract MultichainForkTest is Test {
LZAddressContext ctx;

uint256 arbFork;
uint256 baseFork;

MyOFT arbOFT;
MyOFT baseOFT;

address owner = makeAddr("owner");

function setUp() public {
ctx = new LZAddressContext();
ctx.makePersistent(vm); // Critical: call BEFORE creating forks

// Create forks
arbFork = vm.createFork(vm.rpcUrl("arbitrum-mainnet"));
baseFork = vm.createFork(vm.rpcUrl("base-mainnet"));

// Deploy on Arbitrum
vm.selectFork(arbFork);
ctx.setChain("arbitrum-mainnet");
arbOFT = new MyOFT("MyOFT", "MOFT", ctx.getEndpointV2(), owner);

// Deploy on Base
vm.selectFork(baseFork);
ctx.setChain("base-mainnet");
baseOFT = new MyOFT("MyOFT", "MOFT", ctx.getEndpointV2(), owner);
}

function test_crossChainSetup() public {
// Test on Arbitrum fork
vm.selectFork(arbFork);
ctx.setChain("arbitrum-mainnet");

assertEq(address(arbOFT.endpoint()), ctx.getEndpointV2());

// Test on Base fork
vm.selectFork(baseFork);
ctx.setChain("base-mainnet");

assertEq(address(baseOFT.endpoint()), ctx.getEndpointV2());
}
}

Unit Testing with Mock Endpoints

For unit tests that verify business logic without network dependencies, use Foundry's vm.mockCall to mock the LayerZero endpoint. This approach tests your contract's logic in isolation.

Setup with Mocked Endpoint

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {Test, console} from "forge-std/Test.sol";
import {MyOFT} from "../src/MyOFT.sol";
import {SendParam, OFTReceipt} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import {MessagingFee, MessagingReceipt} from "@layerzerolabs/oft-evm/contracts/OFTCore.sol";
import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
import {Origin} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import {OFTMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol";

contract OFTUnitTest is Test {
using OptionsBuilder for bytes;

MyOFT aOFT;
MyOFT bOFT;
address mockEndpoint;
address owner;
address userA;
address userB;

uint32 constant A_EID = 1;
uint32 constant B_EID = 2;

function setUp() public {
owner = makeAddr("owner");
userA = makeAddr("userA");
userB = makeAddr("userB");
mockEndpoint = makeAddr("mockEndpoint");

// Mock setDelegate call made by OApp constructor
vm.mockCall(mockEndpoint, abi.encodeWithSignature("setDelegate(address)"), abi.encode());

// Deploy OFTs with mocked endpoint
vm.prank(owner);
aOFT = new MyOFT("Chain A OFT", "aOFT", mockEndpoint, owner);

vm.prank(owner);
bOFT = new MyOFT("Chain B OFT", "bOFT", mockEndpoint, owner);

// Set peers
vm.prank(owner);
aOFT.setPeer(B_EID, bytes32(uint256(uint160(address(bOFT)))));

vm.prank(owner);
bOFT.setPeer(A_EID, bytes32(uint256(uint160(address(aOFT)))));

// Mint tokens to userA for testing
aOFT.mint(userA, 100 ether);
}
}

Testing Send Flow

Mock endpoint.send() to test your contract's send logic without real infrastructure:

function test_send() public {
uint256 tokensToSend = 1 ether;

SendParam memory sendParam = SendParam({
dstEid: B_EID,
to: bytes32(uint256(uint160(userB))),
amountLD: tokensToSend,
minAmountLD: tokensToSend,
extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0),
composeMsg: "",
oftCmd: ""
});

// Mock endpoint.send() response
MessagingReceipt memory mockReceipt = MessagingReceipt({
guid: bytes32(uint256(1)),
nonce: 1,
fee: MessagingFee({nativeFee: 0.01 ether, lzTokenFee: 0})
});

vm.mockCall(
mockEndpoint,
0.01 ether,
abi.encodeWithSignature("send((uint32,bytes32,bytes,bytes,bool),address)"),
abi.encode(mockReceipt)
);

// Test send
uint256 balanceBefore = aOFT.balanceOf(userA);

vm.deal(userA, 1 ether);
vm.prank(userA);
aOFT.send{value: 0.01 ether}(
sendParam,
MessagingFee({nativeFee: 0.01 ether, lzTokenFee: 0}),
payable(userA)
);

// Verify tokens burned
assertEq(aOFT.balanceOf(userA), balanceBefore - tokensToSend);
}

Testing Receive Flow

Simulate the endpoint calling your contract's lzReceive:

function test_receive() public {
uint256 tokensToReceive = 3 ether;

// Encode message in OFT format
uint64 amountSD = bOFT.toSD(tokensToReceive);
(bytes memory message,) = OFTMsgCodec.encode(
bytes32(uint256(uint160(userB))),
amountSD,
""
);

// Create origin from source chain
Origin memory origin = Origin({
srcEid: A_EID,
sender: bytes32(uint256(uint160(address(aOFT)))),
nonce: 1
});

uint256 balanceBefore = bOFT.balanceOf(userB);

// Simulate endpoint calling lzReceive
vm.prank(mockEndpoint);
bOFT.lzReceive(origin, bytes32(uint256(1)), message, address(0), "");

// Verify tokens minted
assertEq(bOFT.balanceOf(userB), balanceBefore + tokensToReceive);
}

When to Use Each Approach

ApproachUse Case
Mock endpointTesting business logic (token burns, mints, state changes)
Fork testingTesting integration with real endpoints, DVNs, and executors

Mock endpoint tests run faster and don't require RPC access, making them ideal for CI pipelines and rapid iteration on business logic.

Static Imports

When you know the chain at compile time, use static imports for zero-runtime-overhead lookups:

import {LayerZeroV2ArbitrumMainnet} from "lz-address-book/generated/LZAddresses.sol";

contract MyContract {
address constant ENDPOINT = address(LayerZeroV2ArbitrumMainnet.ENDPOINT_V2);
uint32 constant EID = LayerZeroV2ArbitrumMainnet.EID;
uint256 constant CHAIN_ID = LayerZeroV2ArbitrumMainnet.CHAIN_ID;
}

Stargate Integration

The address book includes Stargate V2 pool addresses for cross-chain asset transfers:

import {STGProtocol, ISTGProtocol} from "lz-address-book/generated/STGProtocol.sol";
import {IOFT, SendParam} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";

contract StargateExample {
function getStargateAsset() external {
STGProtocol stg = new STGProtocol();

// Get USDC on Arbitrum
ISTGProtocol.StargateAsset memory usdc = stg.getAsset("arbitrum-mainnet", "USDC");
// Also available: stg.getAssetByEid(30110, "USDC")
// Also available: stg.getAssetByChainId(42161, "USDC")

// All Stargate contracts implement IOFT
IOFT stargate = IOFT(usdc.oft);
}
}

Common Pitfalls

DVN Name Case Sensitivity

DVN names are case-sensitive. Use exact names:

// ✅ Correct
ctx.getDVNByName("LayerZero Labs");

// ❌ Reverts
ctx.getDVNByName("layerzero labs");
ctx.getDVNByName("LayerZero labs");

Use ctx.getAvailableDVNs() to see exact names.

DVN Sorting

LayerZero requires DVN addresses in ascending order for UlnConfig. Always use the helper:

// ✅ Correct - addresses are sorted
address[] memory sorted = ctx.getSortedDVNs(dvnNames);

// ❌ May fail if not in ascending order
address[] memory manual = new address[](2);
manual[0] = ctx.getDVNByName("LayerZero Labs");
manual[1] = ctx.getDVNByName("Nethermind");

Fork Test Persistence

Always call makePersistent before creating forks:

function setUp() public {
ctx = new LZAddressContext();
ctx.makePersistent(vm); // ✅ BEFORE creating forks

arbFork = vm.createFork(rpc);
}

Address Book Updates

The address book is regenerated every 6 hours via GitHub Actions. To update:

forge update lz-address-book

For stability in production, pin to a specific git tag.

API Reference

Chain Context

MethodDescription
setChain(string)Set context by chain name
setChainByEid(uint32)Set context by LayerZero EID
setChainByChainId(uint256)Set context by native chain ID
makePersistent(Vm)Persist context across fork switches

Protocol Addresses

MethodDescription
getEndpointV2()Get EndpointV2 address
getSendUln302()Get SendUln302 address
getReceiveUln302()Get ReceiveUln302 address
getExecutor()Get Executor address
getDVNByName(string)Get DVN address by provider name

Cross-Chain Lookups

MethodDescription
getEidForChainName(string)Get EID for any chain
getEndpointForChainName(string)Get endpoint for any chain
getDVNForChainName(string, string)Get DVN for any chain

Discovery

MethodDescription
getSupportedChainNames()Get all supported chain names
getAvailableDVNs()Get all DVN names
getSortedDVNs(string[])Get sorted DVN addresses for UlnConfig

Resources