Разработка omnichain-токена (OFT)
Стандартный подход к multi-chain токену: wrap/bridge модель. Оригинальный токен на Ethereum, на каждой другой цепи — wrapped версия через bridge. Проблема: ликвидность фрагментирована, canonical supply неочевидна, пользователь держит «USDC-Polygon» отдельно от «USDC-Arbitrum», bridge риски мультиплицируются с количеством chain.
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 передаёт сообщение на Optimism
4. OFT контракт на Optimism минтит X токенов (увеличивает supply)
5. Суммарный supply across chains не изменился
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) {}
// Минт только на home chain (обычно)
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Деплой одного и того же контракта на каждой целевой цепи. Разница только в _lzEndpoint — адрес LayerZero endpoint специфичен для каждой цепи.
// Deployment script (Hardhat/Foundry)
const LZ_ENDPOINTS = {
ethereum: "0x1a44076050125825900e736c501f859c50fE728c",
arbitrum: "0x1a44076050125825900e736c501f859c50fE728c", // V2 одинаков
optimism: "0x1a44076050125825900e736c501f859c50fE728c",
polygon: "0x1a44076050125825900e736c501f859c50fE728c",
base: "0x1a44076050125825900e736c501f859c50fE728c",
}
// LayerZero V2: эндпоинт одинаковый на всех 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 (сколько нативного газа нужно заплатить)
const sendParam = {
dstEid: LZ_EIDS.optimism,
to: ethers.zeroPadValue(recipientAddress, 32),
amountLD: ethers.parseEther("100"), // amount in local 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 address (если 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: все chains работают с меньшим числом (обычно 6), конвертируя при отправке.
// OFT v2: sharedDecimals по умолчанию 6
// При отправке с EVM (18 decimals) → dust removed
// Пример: отправка 1.000000000000000001 MTK (18 decimals)
// Reальная сумма которая прибудет: 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 amount.
DVN: настройка безопасности
LayerZero V2 позволяет настраивать DVN (Decentralized Verifier Network) — кто верифицирует cross-chain сообщения.
// Кастомная 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 = дороже fee, но выше безопасность.
Мониторинг и обработка failed messages
Сообщение может зависнуть если destination chain momentarily недоступен или gas недостаточен. LayerZero V2 хранит failed messages, их можно 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 дней). Список целевых цепей, tokenomics (где минтить initial supply, нужен ли adapter для существующего токена), требования к security (DVN конфигурация, multisig).
Разработка (1-2 недели). OFT контракты → Foundry тесты для каждого сценария (send, receive, failed message retry) → deployment scripts для всех цепей → wire-up (setPeer на каждой).
Тестирование (1 неделя). Testnet деплой на все целевые цепи → функциональное тестирование cross-chain transfers → проверка decimals conversion → нагрузочный тест с concurrent messages.
Mainnet деплой и верификация. Верификация контрактов на Etherscan/explorer каждой цепи. Тестовая small-amount транзакция перед анонсом.
OFT для 3-5 цепей с базовым функционалом — 3-4 недели. С OFTAdapter для существующего токена, кастомными DVN и мониторинг дашбордом — 5-7 недель.







