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
| Chain | Name | EID | Chain ID |
|---|---|---|---|
| Ethereum | ethereum-mainnet | 30101 | 1 |
| Arbitrum | arbitrum-mainnet | 30110 | 42161 |
| Base | base-mainnet | 30184 | 8453 |
| Optimism | optimism-mainnet | 30111 | 10 |
| Polygon | polygon-mainnet | 30109 | 137 |
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);
}
}
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
| Approach | Use Case |
|---|---|
| Mock endpoint | Testing business logic (token burns, mints, state changes) |
| Fork testing | Testing 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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
getEidForChainName(string) | Get EID for any chain |
getEndpointForChainName(string) | Get endpoint for any chain |
getDVNForChainName(string, string) | Get DVN for any chain |
Discovery
| Method | Description |
|---|---|
getSupportedChainNames() | Get all supported chain names |
getAvailableDVNs() | Get all DVN names |
getSortedDVNs(string[]) | Get sorted DVN addresses for UlnConfig |