Розробка omnichain-токена (OFT)
Стандартний підхід до multi-chain токена: wrap/bridge модель. Оригінальний токен на Ethereum, на кожній іншій цепи — wrapped версія через bridge. Проблема: ліквідність фрагментована, canonical supply неочевидна, користувач держит «USDC-Polygon» окремо від «USDC-Arbitrum», bridge ризики мультиплікуються з кількістю цепей.
OFT (Omnichain Fungible Token) від LayerZero вирішує це інакше: один контракт на кожній цепи, єдиний supply, перевід між цепями працює через burn-and-mint без wrapping. Токен на Arbitrum — це той же токен, не wrapped копія.
Як працює LayerZero OFT
Burn-and-mint механізм
Transfer: Arbitrum → Optimism
1. Користувач викликає oft.send() на Arbitrum
2. OFT контракт спалює X токенів на Arbitrum (зменшує supply)
3. LayerZero relayer передає mensaje на Optimism
4. OFT контракт на Optimism мінтить X токенів (збільшує supply)
5. Загальний supply поперед цепями не змінився
vs Bridge (wrap) модель:
1. Lock X токенів на source у bridge контракті
2. Mint X wrapped токенів на destination
Проблема: bridge контракт — single point of failure для всього locked supply
В OFT немає центрального bridge контракту з locked активами. Немає «TVL у bridge» який можна взломати. Можна взломати тільки LayerZero messaging layer — це окремий ризик, не «всё відразу».
OFT v2 реалізація
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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, // LayerZero endpoint для цієї цепи
address _delegate // owner/admin
) OFT(_name, _symbol, _lzEndpoint, _delegate) {}
// Mint тільки на home chain (звичайно)
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Розгортаємо той же контракт на кожній target цепи. Різниця тільки в _lzEndpoint — адреса LayerZero endpoint специфічна для кожної цепи.
// Deployment скрипт (Hardhat/Foundry)
const LZ_ENDPOINTS = {
ethereum: "0x1a44076050125825900e736c501f859c50fE728c",
arbitrum: "0x1a44076050125825900e736c501f859c50fE728c", // V2 один
optimism: "0x1a44076050125825900e736c501f859c50fE728c",
polygon: "0x1a44076050125825900e736c501f859c50fE728c",
base: "0x1a44076050125825900e736c501f859c50fE728c",
}
// LayerZero V2: endpoint один на всіх EVM-сітях
// Але EID (Endpoint ID) унікальний для кожної цепи
const LZ_EIDS = {
ethereum: 30101,
arbitrum: 30110,
optimism: 30111,
polygon: 30109,
base: 30184,
}
Конфігурація peers (wire-up)
Після розгортання на всі цепи — зв'язуємо контракти між собою. Кожен OFT повинен знати адреси своїх peer'ів.
import { ethers } from "ethers"
import { Options } from "@layerzerolabs/lz-v2-utilities"
// На Arbitrum: установити peer для Optimism
const oftArbitrum = new ethers.Contract(OFT_ARBITRUM, OFT_ABI, signerArbitrum)
// Адреса peer повинна бути bytes32-encoded (лівий padding нулями)
const optimismPeerBytes32 = ethers.zeroPadValue(OFT_OPTIMISM, 32)
await oftArbitrum.setPeer(
LZ_EIDS.optimism, // destination EID
optimismPeerBytes32 // адреса peer на destination
)
// Аналогічно на Optimism: установити peer для Arbitrum
const oftOptimism = new ethers.Contract(OFT_OPTIMISM, OFT_ABI, signerOptimism)
await oftOptimism.setPeer(LZ_EIDS.arbitrum, ethers.zeroPadValue(OFT_ARBITRUM, 32))
Не автоматично — потрібно викликати setPeer для кожної пари цепей. При N цепях: N*(N-1) викликів (кожна цеп знає всіх інших).
Відправлення токенів між цепями
// Користувач: відправити 100 MTK з Arbitrum на Optimism
const oft = new ethers.Contract(OFT_ARBITRUM, OFT_ABI, signer)
// 1. Отримати quote (скільки нативного gas потрібно заплатити)
const sendParam = {
dstEid: LZ_EIDS.optimism,
to: ethers.zeroPadValue(recipientAddress, 32),
amountLD: ethers.parseEther("100"), // сума у локальних decimals
minAmountLD: ethers.parseEther("99"), // 1% slippage tolerance
extraOptions: "0x",
composeMsg: "0x",
oftCmd: "0x",
}
const [nativeFee, lzTokenFee] = await oft.quoteSend(sendParam, false)
console.log(`Fee: ${ethers.formatEther(nativeFee)} ETH`)
// 2. Відправити (msg.value = nativeFee для оплати LayerZero)
const tx = await oft.send(
sendParam,
{ nativeFee, lzTokenFee: 0n },
signer.address, // refund адреса (якщо overpay)
{ value: nativeFee }
)
const receipt = await tx.wait()
// Токени на Arbitrum спалені
// Через ~15-60 секунд з'являються на Optimism
Decimals: потенційна пастка
Різні цепи можуть мати різні native decimals. Solana використовує 6 decimals (lamports), EVM — 18. OFT v2 вирішує це через shared decimals: всі цепи працюють з меншим числом (звичайно 6), конвертуючи при відправленні.
// OFT v2: sharedDecimals за замовченням 6
// При відправленні з EVM (18 decimals) → dust видалено
// Приклад: відправлення 1.000000000000000001 MTK (18 decimals)
// Реальна сума яка прибуде: 1.000000 MTK (6 shared decimals)
// 0.000000000000000001 MTK залишається у відправника як "dust"
// Завжди перевіряйте: quoteSend → amountReceivedLD
// amountReceivedLD = фактично отримана сума з урахуванням конвертації
Важливо для UI: показувати користувачу amountReceivedLD, не оригінальну суму.
OFTAdapter: для існуючих токенів
Якщо токен вже існує на Ethereum та його контракт не можна змінити (немає mint/burn функцій у потрібному контексті) — використовуємо OFTAdapter. На Ethereum: Lock&Release (токени locked у adapter). На інших цепях: OFT з burn&mint.
import { OFTAdapter } from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTAdapter.sol";
contract MyTokenAdapter is OFTAdapter {
constructor(
address _token, // існуючий ERC-20 токен
address _lzEndpoint,
address _delegate
) OFTAdapter(_token, _lzEndpoint, _delegate) {}
}
На інших цепях розгортаємо звичайний OFT (з mint/burn). Тільки Ethereum використовує lock модель — інші цепи використовують burn/mint.
OFTAdapter компроміс: Ethereum-locked токени — це знову bridge ризик. Якщо adapter взломан — supply на інших цепях незабезпечений. Для критичних проектів: multisig або timelock на adapter, cap на locked суму.
DVN: конфігурація безпеки
LayerZero V2 дозволяє конфігурувати DVN (Decentralized Verifier Network) — хто верифікує cross-chain mensaje.
// Кастомна DVN конфігурація
// За замовченням: LayerZero DVN (один верифікатор)
// Посилена: потребуємо 2-of-3 верифікацію
const enforcedOptions = [
{
eid: LZ_EIDS.optimism,
msgType: 1,
options: Options.newOptions()
.addExecutorLzReceiveOption(200_000, 0) // 200k gas на destination
.toHex()
}
]
// Установити через OApp config
await oft.setEnforcedOptions(enforcedOptions)
Для production OFT з великим TVL: конфігурувати мінімум два незалежних DVN (LayerZero + Google Cloud DVN або Polyhedra). Компроміс: більше DVN = дорожча комісія, але вища безпека.
Моніторинг та обробка failed mensaje
Mensaje може зависнути якщо destination цеп тимчасово недоступна або gas недостатньо. LayerZero V2 зберігає failed mensaje, можна retry.
// LayerZero Scan API для моніторингу
const response = await fetch(
`https://scan.layerzero-api.com/v1/messages/tx/${txHash}`
)
const { data } = await response.json()
if (data.status === 'FAILED') {
console.log(`Message failed: ${data.failReason}`)
// Викликаємо lzEndpoint.retryMessage() або
// lzEndpoint.forceResumeReceive() якщо потрібно пропустити
}
Для UX: показувати користувачу статус cross-chain транзакції через LayerZero Scan embeddable widget або кастомний polling.
Робочий процес
Підготовка (3-5 днів). Список target цепей, tokenomics (де мінтити initial supply, adapter потрібен для існуючого токена), вимоги безпеки (DVN конфігурація, multisig).
Розробка (1-2 тижні). OFT контракти → Foundry тести для кожного сценарію (send, receive, failed mensaje retry) → deployment скрипти для всіх цепей → wire-up (setPeer на кожній).
Тестування (1 тиждень). Testnet розгортання на всі target цепи → функціональне тестування cross-chain transfers → перевірка decimals конвертації → load тест з concurrent mensaje.
Mainnet розгортання та верифікація. Верифікація контрактів на Etherscan/explorer кожної цепи. Тестова small-amount транзакція перед анонсом.
OFT для 3-5 цепей з базовим функціоналом — 3-4 тижні. З OFTAdapter для існуючого токена, кастомними DVN та monitoring дашбордом — 5-7 тижнів.







