deploy-and-configure-oft-multichain
Deploy and configure Omnichain Fungible Tokens (OFTs) across multiple blockchains, set up peer connections, and enforce token options using LayerZero protocols.
Instructions
Deploys an OFT contract to multiple chains, sets up peer connections, and configures enforced options.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| decimals | No | Number of decimals for the token (default: 18) | |
| initialTotalSupply | Yes | Total supply of the token in human-readable format (e.g., '1000000') | |
| owner | No | Optional owner address. Defaults to OWNER_ADDRESS from .env if not provided. | |
| targetChains | Yes | List of chain names to deploy and configure the OFT on (e.g., ['ArbitrumSepolia', 'baseSepolia']) | |
| tokenName | Yes | Name of the token (e.g., MyToken) | |
| tokenSymbol | Yes | Symbol of the token (e.g., MYT) |
Implementation Reference
- src/index.ts:101-468 (handler)The main handler function that performs multi-chain deployment of OFT contracts via CREATE2 factories, bidirectional peering setup using setPeer, and enforced options configuration using setEnforcedOptions for LayerZero cross-chain functionality.async (params: z.infer<typeof deployAndConfigureOftParams>, _extra) => { const results: string[] = []; const deployedContractsSummary: Array<{ chainName: string; contractAddress: string | null; deploymentStatus: string; error?: string; }> = []; // A. Initial Checks & Setup results.push("Phase A: Initial Checks & Setup started."); const envOwnerAddress = process.env.OWNER_ADDRESS; if (!envOwnerAddress) { results.push("Error: OWNER_ADDRESS is not set in environment variables."); return { content: [ { type: "text", text: `Error: OWNER_ADDRESS is not set in environment variables.`, }, ], isError: true, }; } results.push(`OWNER_ADDRESS found: ${envOwnerAddress}`); const MyOFT = JSON.parse(await readFile(oftPath, "utf8")); const OFT_ABI = MyOFT.abi; let OFT_BYTECODE = MyOFT.bytecode.object; if ( OFT_ABI === "ABI_PLACEHOLDER" || OFT_BYTECODE === "BYTECODE_PLACEHOLDER" || OFT_ABI === undefined || OFT_BYTECODE === undefined || OFT_ABI.length === 0 || OFT_BYTECODE.length === 0 ) { results.push( "Error: OFT_ABI or OFT_BYTECODE are placeholders. Please replace them in layerzero-mcp.ts." ); return { content: [ { type: "text", text: `Error: OFT_ABI or OFT_BYTECODE are placeholders. Please replace them in layerzero-mcp.ts.`, }, ], isError: true, }; } results.push("OFT_ABI and OFT_BYTECODE are not placeholders."); if (OFT_BYTECODE && !OFT_BYTECODE.startsWith('0x')) { OFT_BYTECODE = '0x' + OFT_BYTECODE; } if (!params.targetChains || params.targetChains.length === 0) { // Zod schema min(1) should prevent this, but good to double check results.push("Error: targetChains array is empty."); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: Error: targetChains array is empty. Please provide at least one target chain.`, }, ], isError: true, }; } results.push( `Target chains for deployment: ${params.targetChains.join(", ")}` ); interface DeployedContractData { chainName: string; address: string; contractInstance: ethers.Contract; lzEid: number; networkConfig: NetworkConfig; } const deployedContractsData: DeployedContractData[] = []; results.push("Initialization complete."); // B. Deployment Phase results.push("\nPhase B: Deployment Phase started."); const deployOwner = params.owner || envOwnerAddress; results.push(`Deployment owner set to: ${deployOwner}`); for (const chainName of params.targetChains) { results.push(`Attempting deployment on ${chainName}...`); try { const signer = await getSigner(chainName); const networkConfig = getNetworkConfig(chainName); results.push( ` Signer and network config obtained for ${chainName}. RPC: ${networkConfig.rpc}, LZ EID: ${networkConfig.lzEid}` ); const parsedSupply = ethers.parseUnits(params.initialTotalSupply, 0); results.push( ` Token: ${params.tokenName} (${params.tokenSymbol}), Supply: ${ params.initialTotalSupply } (parsed: ${parsedSupply.toString()}), Decimals: ${params.decimals}` ); const factoryAddress = FACTORY_ADDRESSES[chainName]; if (!factoryAddress) { throw new Error(`Factory address not defined for ${chainName}`); } const factoryJSON = JSON.parse(await readFile(factoryPath, "utf8")); const factory = new Contract(factoryAddress, factoryJSON.abi, signer); results.push(" Contract factory created."); // Derive a consistent salt from token name and chain (e.g., to keep same address across chains) const salt = ethers.keccak256( ethers.toUtf8Bytes(`${params.tokenName}:${params.tokenSymbol}`) ); // Encode constructor args for MyOFT const constructorArgs = new ethers.Interface(OFT_ABI).encodeDeploy([ params.tokenName, params.tokenSymbol, parsedSupply, networkConfig.lzEndpoint, deployOwner, ]); // Combine bytecode + constructor args const bytecodeWithArgs = ethers.solidityPacked( ["bytes", "bytes"], [OFT_BYTECODE, constructorArgs] ); results.push( ` Deploying with CREATE2 on ${chainName} using salt: ${salt}` ); const tx = await factory.deploy(bytecodeWithArgs, salt); await tx.wait(); const computedAddress = await factory.lastDeployedAddress(); results.push( ` SUCCESS: Deployed via CREATE2 at computed address: ${computedAddress}` ); const contractInstance = new ethers.Contract( computedAddress, OFT_ABI, signer ); deployedContractsData.push({ chainName, address: computedAddress, contractInstance, lzEid: networkConfig.lzEid, networkConfig, }); deployedContractsSummary.push({ chainName, contractAddress: computedAddress, deploymentStatus: "Success", }); } catch (error: any) { const constructorArgs = new ethers.Interface(OFT_ABI).encodeDeploy([ params.tokenName, params.tokenSymbol, ethers.parseUnits(params.initialTotalSupply, 0), getNetworkConfig(chainName).lzEndpoint, deployOwner, ]); // Combine bytecode + constructor args const bytecodeWithArgs = ethers.solidityPacked( ["bytes", "bytes"], [OFT_BYTECODE, constructorArgs] ); const errorMessage = ` FAILURE: Deploying on ${chainName}: ${ error.message || error.toString() }, salt: ${ethers.keccak256( ethers.toUtf8Bytes(`${params.tokenName}:${params.tokenSymbol}`) )}, bytecode: ${bytecodeWithArgs}`; results.push(errorMessage); console.error(errorMessage, error); deployedContractsSummary.push({ chainName, contractAddress: null, deploymentStatus: "Failed", error: error.message || error.toString(), }); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMessage}`, }, ], isError: true, }; } } results.push("Deployment Phase completed."); // C. Peering Phase results.push("\nPhase C: Peering Phase started."); const peeringResults: string[] = []; if (deployedContractsData.length < 2) { const msg = "Skipping peering phase: Less than 2 contracts successfully deployed."; results.push(msg); peeringResults.push(msg); } else { results.push( `Found ${deployedContractsData.length} successfully deployed contracts for peering.` ); for (const contractAData of deployedContractsData) { for (const contractBData of deployedContractsData) { if (contractAData.chainName === contractBData.chainName) continue; const logPrefix = ` Peering ${contractAData.chainName} (EID: ${contractAData.lzEid}) with ${contractBData.chainName} (EID: ${contractBData.lzEid}, Address: ${contractBData.address}):`; try { const peerAddressBytes32 = formatAddressForLayerZero( contractBData.address ); peeringResults.push( `${logPrefix} Formatting peer address ${contractBData.address} to ${peerAddressBytes32}` ); const tx = await contractAData.contractInstance.setPeer( contractBData.lzEid, peerAddressBytes32 ); peeringResults.push( `${logPrefix} setPeer transaction sent. Waiting for confirmation... TxHash: ${tx.hash}` ); await tx.wait(); const successMsg = `${logPrefix} SUCCESS.`; results.push(successMsg); peeringResults.push(successMsg); } catch (error: any) { const errorMsg = `${logPrefix} FAILURE: ${ error.message || error.toString() }`; results.push(errorMsg); peeringResults.push(errorMsg); console.error(errorMsg, error); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMsg}`, }, ], isError: true, }; } } } } results.push("Peering Phase completed."); // D. Enforced Options Phase results.push("\nPhase D: Enforced Options Phase started."); const enforcedOptionsResults: string[] = []; const standardOptionsHex = "0x00030100110100000000000000000000000000030d40"; // 200k gas limit results.push( `Standard options hex for enforced options: ${standardOptionsHex}` ); if (deployedContractsData.length === 0) { const msg = "Skipping enforced options phase: No contracts successfully deployed."; results.push(msg); enforcedOptionsResults.push(msg); } else { for (const contractData of deployedContractsData) { const logPrefix = ` Configuring enforced options on ${contractData.chainName} (Address: ${contractData.address}):`; try { const optionsToSet: Array<{ eid: number; msgType: number; options: string; }> = []; for (const peerData of deployedContractsData) { if (contractData.chainName === peerData.chainName) continue; optionsToSet.push({ eid: peerData.lzEid, msgType: 1, options: standardOptionsHex, }); } if (optionsToSet.length > 0) { enforcedOptionsResults.push( `${logPrefix} Preparing to set options for ${ optionsToSet.length } peers: ${optionsToSet.map((o) => `EID ${o.eid}`).join(", ")}` ); const tx = await contractData.contractInstance.setEnforcedOptions( optionsToSet ); enforcedOptionsResults.push( `${logPrefix} setEnforcedOptions transaction sent. Waiting for confirmation... TxHash: ${tx.hash}` ); await tx.wait(); const successMsg = `${logPrefix} SUCCESS: Set enforced options for ${optionsToSet.length} peers.`; results.push(successMsg); enforcedOptionsResults.push(successMsg); } else { const noPeersMsg = `${logPrefix} No peers to set enforced options for.`; results.push(noPeersMsg); enforcedOptionsResults.push(noPeersMsg); } } catch (error: any) { const errorMsg = `${logPrefix} FAILURE: ${ error.message || error.toString() }`; results.push(errorMsg); enforcedOptionsResults.push(errorMsg); console.error(errorMsg, error); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMsg}`, }, ], isError: true, }; } } } results.push("Enforced Options Phase completed."); // E. Return Value results.push("\nPhase E: Preparing results."); let overallStatus = "Deployment and configuration process completed."; if ( deployedContractsSummary.some((s) => s.deploymentStatus === "Failed") || results.some((r) => r.includes("FAILURE")) ) { overallStatus += " Some errors occurred. Check detailed logs."; } else if (deployedContractsData.length === 0) { overallStatus = "Deployment failed on all target chains."; } else { overallStatus = "Successfully deployed and configured on all attempted chains where deployment succeeded."; } results.push(`Overall Status: ${overallStatus}`); return { content: [ { type: "text", text: JSON.stringify( { overallStatus, deployedContracts: deployedContractsSummary, peeringResults, enforcedOptionsResults, detailedExecutionLog: results, }, null, 2 ), // Pretty print JSON }, ], }; }
- src/index.ts:67-95 (schema)Zod schema defining the input parameters for the deploy-and-configure-oft-multichain tool.const deployAndConfigureOftParams = z.object({ tokenName: z.string().describe("Name of the token (e.g., MyToken)"), tokenSymbol: z.string().describe("Symbol of the token (e.g., MYT)"), initialTotalSupply: z .string() .describe( "Total supply of the token in human-readable format (e.g., '1000000')" ), decimals: z .number() .int() .min(0) .max(18) .optional() .default(18) .describe("Number of decimals for the token (default: 18)"), targetChains: z .array(z.enum(networkKeysForEnum)) .min(1) .describe( "List of chain names to deploy and configure the OFT on (e.g., ['ArbitrumSepolia', 'baseSepolia'])" ), owner: z .string() .optional() .describe( "Optional owner address. Defaults to OWNER_ADDRESS from .env if not provided." ), });
- src/index.ts:97-469 (registration)MCP server tool registration call that associates the tool name, description, input schema, and handler function.server.tool( "deploy-and-configure-oft-multichain", "Deploys an OFT contract to multiple chains, sets up peer connections, and configures enforced options.", deployAndConfigureOftParams.shape, async (params: z.infer<typeof deployAndConfigureOftParams>, _extra) => { const results: string[] = []; const deployedContractsSummary: Array<{ chainName: string; contractAddress: string | null; deploymentStatus: string; error?: string; }> = []; // A. Initial Checks & Setup results.push("Phase A: Initial Checks & Setup started."); const envOwnerAddress = process.env.OWNER_ADDRESS; if (!envOwnerAddress) { results.push("Error: OWNER_ADDRESS is not set in environment variables."); return { content: [ { type: "text", text: `Error: OWNER_ADDRESS is not set in environment variables.`, }, ], isError: true, }; } results.push(`OWNER_ADDRESS found: ${envOwnerAddress}`); const MyOFT = JSON.parse(await readFile(oftPath, "utf8")); const OFT_ABI = MyOFT.abi; let OFT_BYTECODE = MyOFT.bytecode.object; if ( OFT_ABI === "ABI_PLACEHOLDER" || OFT_BYTECODE === "BYTECODE_PLACEHOLDER" || OFT_ABI === undefined || OFT_BYTECODE === undefined || OFT_ABI.length === 0 || OFT_BYTECODE.length === 0 ) { results.push( "Error: OFT_ABI or OFT_BYTECODE are placeholders. Please replace them in layerzero-mcp.ts." ); return { content: [ { type: "text", text: `Error: OFT_ABI or OFT_BYTECODE are placeholders. Please replace them in layerzero-mcp.ts.`, }, ], isError: true, }; } results.push("OFT_ABI and OFT_BYTECODE are not placeholders."); if (OFT_BYTECODE && !OFT_BYTECODE.startsWith('0x')) { OFT_BYTECODE = '0x' + OFT_BYTECODE; } if (!params.targetChains || params.targetChains.length === 0) { // Zod schema min(1) should prevent this, but good to double check results.push("Error: targetChains array is empty."); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: Error: targetChains array is empty. Please provide at least one target chain.`, }, ], isError: true, }; } results.push( `Target chains for deployment: ${params.targetChains.join(", ")}` ); interface DeployedContractData { chainName: string; address: string; contractInstance: ethers.Contract; lzEid: number; networkConfig: NetworkConfig; } const deployedContractsData: DeployedContractData[] = []; results.push("Initialization complete."); // B. Deployment Phase results.push("\nPhase B: Deployment Phase started."); const deployOwner = params.owner || envOwnerAddress; results.push(`Deployment owner set to: ${deployOwner}`); for (const chainName of params.targetChains) { results.push(`Attempting deployment on ${chainName}...`); try { const signer = await getSigner(chainName); const networkConfig = getNetworkConfig(chainName); results.push( ` Signer and network config obtained for ${chainName}. RPC: ${networkConfig.rpc}, LZ EID: ${networkConfig.lzEid}` ); const parsedSupply = ethers.parseUnits(params.initialTotalSupply, 0); results.push( ` Token: ${params.tokenName} (${params.tokenSymbol}), Supply: ${ params.initialTotalSupply } (parsed: ${parsedSupply.toString()}), Decimals: ${params.decimals}` ); const factoryAddress = FACTORY_ADDRESSES[chainName]; if (!factoryAddress) { throw new Error(`Factory address not defined for ${chainName}`); } const factoryJSON = JSON.parse(await readFile(factoryPath, "utf8")); const factory = new Contract(factoryAddress, factoryJSON.abi, signer); results.push(" Contract factory created."); // Derive a consistent salt from token name and chain (e.g., to keep same address across chains) const salt = ethers.keccak256( ethers.toUtf8Bytes(`${params.tokenName}:${params.tokenSymbol}`) ); // Encode constructor args for MyOFT const constructorArgs = new ethers.Interface(OFT_ABI).encodeDeploy([ params.tokenName, params.tokenSymbol, parsedSupply, networkConfig.lzEndpoint, deployOwner, ]); // Combine bytecode + constructor args const bytecodeWithArgs = ethers.solidityPacked( ["bytes", "bytes"], [OFT_BYTECODE, constructorArgs] ); results.push( ` Deploying with CREATE2 on ${chainName} using salt: ${salt}` ); const tx = await factory.deploy(bytecodeWithArgs, salt); await tx.wait(); const computedAddress = await factory.lastDeployedAddress(); results.push( ` SUCCESS: Deployed via CREATE2 at computed address: ${computedAddress}` ); const contractInstance = new ethers.Contract( computedAddress, OFT_ABI, signer ); deployedContractsData.push({ chainName, address: computedAddress, contractInstance, lzEid: networkConfig.lzEid, networkConfig, }); deployedContractsSummary.push({ chainName, contractAddress: computedAddress, deploymentStatus: "Success", }); } catch (error: any) { const constructorArgs = new ethers.Interface(OFT_ABI).encodeDeploy([ params.tokenName, params.tokenSymbol, ethers.parseUnits(params.initialTotalSupply, 0), getNetworkConfig(chainName).lzEndpoint, deployOwner, ]); // Combine bytecode + constructor args const bytecodeWithArgs = ethers.solidityPacked( ["bytes", "bytes"], [OFT_BYTECODE, constructorArgs] ); const errorMessage = ` FAILURE: Deploying on ${chainName}: ${ error.message || error.toString() }, salt: ${ethers.keccak256( ethers.toUtf8Bytes(`${params.tokenName}:${params.tokenSymbol}`) )}, bytecode: ${bytecodeWithArgs}`; results.push(errorMessage); console.error(errorMessage, error); deployedContractsSummary.push({ chainName, contractAddress: null, deploymentStatus: "Failed", error: error.message || error.toString(), }); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMessage}`, }, ], isError: true, }; } } results.push("Deployment Phase completed."); // C. Peering Phase results.push("\nPhase C: Peering Phase started."); const peeringResults: string[] = []; if (deployedContractsData.length < 2) { const msg = "Skipping peering phase: Less than 2 contracts successfully deployed."; results.push(msg); peeringResults.push(msg); } else { results.push( `Found ${deployedContractsData.length} successfully deployed contracts for peering.` ); for (const contractAData of deployedContractsData) { for (const contractBData of deployedContractsData) { if (contractAData.chainName === contractBData.chainName) continue; const logPrefix = ` Peering ${contractAData.chainName} (EID: ${contractAData.lzEid}) with ${contractBData.chainName} (EID: ${contractBData.lzEid}, Address: ${contractBData.address}):`; try { const peerAddressBytes32 = formatAddressForLayerZero( contractBData.address ); peeringResults.push( `${logPrefix} Formatting peer address ${contractBData.address} to ${peerAddressBytes32}` ); const tx = await contractAData.contractInstance.setPeer( contractBData.lzEid, peerAddressBytes32 ); peeringResults.push( `${logPrefix} setPeer transaction sent. Waiting for confirmation... TxHash: ${tx.hash}` ); await tx.wait(); const successMsg = `${logPrefix} SUCCESS.`; results.push(successMsg); peeringResults.push(successMsg); } catch (error: any) { const errorMsg = `${logPrefix} FAILURE: ${ error.message || error.toString() }`; results.push(errorMsg); peeringResults.push(errorMsg); console.error(errorMsg, error); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMsg}`, }, ], isError: true, }; } } } } results.push("Peering Phase completed."); // D. Enforced Options Phase results.push("\nPhase D: Enforced Options Phase started."); const enforcedOptionsResults: string[] = []; const standardOptionsHex = "0x00030100110100000000000000000000000000030d40"; // 200k gas limit results.push( `Standard options hex for enforced options: ${standardOptionsHex}` ); if (deployedContractsData.length === 0) { const msg = "Skipping enforced options phase: No contracts successfully deployed."; results.push(msg); enforcedOptionsResults.push(msg); } else { for (const contractData of deployedContractsData) { const logPrefix = ` Configuring enforced options on ${contractData.chainName} (Address: ${contractData.address}):`; try { const optionsToSet: Array<{ eid: number; msgType: number; options: string; }> = []; for (const peerData of deployedContractsData) { if (contractData.chainName === peerData.chainName) continue; optionsToSet.push({ eid: peerData.lzEid, msgType: 1, options: standardOptionsHex, }); } if (optionsToSet.length > 0) { enforcedOptionsResults.push( `${logPrefix} Preparing to set options for ${ optionsToSet.length } peers: ${optionsToSet.map((o) => `EID ${o.eid}`).join(", ")}` ); const tx = await contractData.contractInstance.setEnforcedOptions( optionsToSet ); enforcedOptionsResults.push( `${logPrefix} setEnforcedOptions transaction sent. Waiting for confirmation... TxHash: ${tx.hash}` ); await tx.wait(); const successMsg = `${logPrefix} SUCCESS: Set enforced options for ${optionsToSet.length} peers.`; results.push(successMsg); enforcedOptionsResults.push(successMsg); } else { const noPeersMsg = `${logPrefix} No peers to set enforced options for.`; results.push(noPeersMsg); enforcedOptionsResults.push(noPeersMsg); } } catch (error: any) { const errorMsg = `${logPrefix} FAILURE: ${ error.message || error.toString() }`; results.push(errorMsg); enforcedOptionsResults.push(errorMsg); console.error(errorMsg, error); return { content: [ { type: "text", text: `Error: Failed to Deploy OFT: ${errorMsg}`, }, ], isError: true, }; } } } results.push("Enforced Options Phase completed."); // E. Return Value results.push("\nPhase E: Preparing results."); let overallStatus = "Deployment and configuration process completed."; if ( deployedContractsSummary.some((s) => s.deploymentStatus === "Failed") || results.some((r) => r.includes("FAILURE")) ) { overallStatus += " Some errors occurred. Check detailed logs."; } else if (deployedContractsData.length === 0) { overallStatus = "Deployment failed on all target chains."; } else { overallStatus = "Successfully deployed and configured on all attempted chains where deployment succeeded."; } results.push(`Overall Status: ${overallStatus}`); return { content: [ { type: "text", text: JSON.stringify( { overallStatus, deployedContracts: deployedContractsSummary, peeringResults, enforcedOptionsResults, detailedExecutionLog: results, }, null, 2 ), // Pretty print JSON }, ], }; } );
- src/utils.ts:72-77 (helper)Helper function to create an ethers Wallet signer connected to the provider for the specified network, used in deployment and configuration transactions.export async function getSigner(networkName: string): Promise<ethers.Wallet> { const networkConfig = getNetworkConfig(networkName); const provider = new JsonRpcProvider(networkConfig.rpc); const wallet = new Wallet(PRIVATE_KEY!, provider); return wallet; }
- src/utils.ts:84-86 (helper)Helper function to format Ethereum addresses to 32-byte padded strings required for LayerZero peer addresses.export function formatAddressForLayerZero(address: string): string { return zeroPadValue(address, 32); }