diff --git a/protocols/layerzero-v2/README.md b/protocols/layerzero-v2/README.md index 8c37b16..75dd801 100644 --- a/protocols/layerzero-v2/README.md +++ b/protocols/layerzero-v2/README.md @@ -3,4 +3,5 @@ - [Обзор и архитектура](./architecture/README.md) - [Взаимодействие с протоколом LayerZero v2. Часть 1. Простой OApp в Remix](./oapp/README.md) - [Взаимодействие с протоколом LayerZero v2. Часть 2. OFT-токен](./oft-token/README.md) -- [Взаимодействие с протоколом LayerZero v2. Часть 3. Параметры (options), особенности, PreCrime](./options/README.md) \ No newline at end of file +- [Взаимодействие с протоколом LayerZero v2. Часть 3. Параметры (options), особенности, PreCrime](./options/README.md) +- [Взаимодействие с протоколом LayerZero v2. Часть 4. Omnichain Queries (lzRead)](./lz-read/README.md) \ No newline at end of file diff --git a/protocols/layerzero-v2/lz-read/LzReadConfig.sol b/protocols/layerzero-v2/lz-read/LzReadConfig.sol new file mode 100644 index 0000000..b71095d --- /dev/null +++ b/protocols/layerzero-v2/lz-read/LzReadConfig.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; +import { ReadLibConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/readlib/ReadLibBase.sol"; +import { SetConfigParam } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { UniswapV3ObserveRead } from "./UniswapV3ObserveRead.sol"; + +/// @dev Minimal interface to transfer OApp ownership (OApp inherits Ownable). +interface IOwnableOApp { + function transferOwnership(address newOwner) external; +} + +/// @dev Minimal interface to set OApp delegate (for endpoint configuration). +interface IOAppDelegate { + function setDelegate(address delegate) external; +} + +/// @dev Minimal interface to set read channel on lzRead OApp (channel id + active flag). +interface IOAppReadChannel { + function setReadChannel(uint32 channelId, bool active) external; +} + +/** + * @title LzReadConfig + * @notice Config contract for lzRead OApp: configures the OApp on the LayerZero endpoint (libraries, DVN, executor) + * and sets enforced options on the OApp. Deploy this first, then deploy the OApp with this contract + * as owner and delegate so it can perform configuration. + * @dev Endpoint configuration requires caller to be OApp or its delegate. + * OApp configuration (setEnforcedOptions) requires caller to be OApp owner. This contract is both. + */ +contract LzReadConfig is Ownable { + ILayerZeroEndpointV2 public immutable endpoint; + + /// @notice Parameters for ReadLib config on the endpoint (executor, DVNs). Matches ReadLibConfig fields. + struct ReadLibConfigParams { + address executor; + uint8 requiredDVNCount; + uint8 optionalDVNCount; + uint8 optionalDVNThreshold; + address[] requiredDVNs; + address[] optionalDVNs; + } + + /// @notice Parameters for enforced options on the OApp. Pass eid (e.g. readChannel) so one config contract can set options for any OApp that gave it delegate. + struct EnforcedOptionsParams { + uint32 eid; + uint16 msgType; + bytes options; + } + + constructor(address _endpoint) Ownable(msg.sender) { + endpoint = ILayerZeroEndpointV2(_endpoint); + } + + /** + * @notice Configures the OApp on the endpoint: send/receive libraries and ReadLib config. + * @param _oapp Address of the lzRead OApp. This contract must be the OApp's owner and delegate. + * @param _readChannel Read channel ID for this OApp (can differ per OApp). + * @param _readLib Message library address for read (e.g. ReadLib1002 for the target chain). + * @param _libConfig Executor, required/optional DVNs and threshold for ReadLib. + * @param _receiveGracePeriod Grace period (seconds) for receive library activation; 0 = immediate. + */ + function configureEndpoint( + address _oapp, + uint32 _readChannel, + address _readLib, + ReadLibConfigParams calldata _libConfig, + uint256 _receiveGracePeriod + ) public onlyOwner { + endpoint.setSendLibrary(_oapp, _readChannel, _readLib); + endpoint.setReceiveLibrary(_oapp, _readChannel, _readLib, _receiveGracePeriod); + + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = SetConfigParam({ + eid: _readChannel, + configType: 1, // LZ_READ_LID_CONFIG_TYPE + config: abi.encode(ReadLibConfig({ + executor: _libConfig.executor, + requiredDVNCount: _libConfig.requiredDVNCount, + optionalDVNCount: _libConfig.optionalDVNCount, + optionalDVNThreshold: _libConfig.optionalDVNThreshold, + requiredDVNs: _libConfig.requiredDVNs, + optionalDVNs: _libConfig.optionalDVNs + })) + }); + endpoint.setConfig(_oapp, _readLib, params); + } + + /** + * @notice Sets enforced options on the OApp. Caller must be OApp owner (this contract). + * @param _oapp Address of the lzRead OApp. + * @param _params eid (e.g. readChannel), msgType and encoded options. Passing eid lets one configurator set options for any OApp that set it as delegate. + */ + function setEnforcedOptions( + address _oapp, + EnforcedOptionsParams calldata _params + ) public onlyOwner { + EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1); + enforcedOptions[0] = EnforcedOptionParam({ + eid: _params.eid, + msgType: _params.msgType, + options: _params.options + }); + UniswapV3ObserveRead(payable(_oapp)).setEnforcedOptions(enforcedOptions); + } + + /** + * @notice Performs full configuration in one call: endpoint (libraries + ReadLib config) and enforced options. + * @param _oapp Address of the lzRead OApp. + * @param _readChannel Read channel ID for this OApp. + * @param _readLib Message library address for read. + * @param _libConfig Executor and DVN config for the endpoint. + * @param _receiveGracePeriod Grace period for receive library (0 = immediate). + * @param _enforced Enforced options (eid, msgType, encoded options bytes). + */ + function configureFull( + address _oapp, + uint32 _readChannel, + address _readLib, + ReadLibConfigParams calldata _libConfig, + uint256 _receiveGracePeriod, + EnforcedOptionsParams calldata _enforced + ) external onlyOwner { + configureEndpoint(_oapp, _readChannel, _readLib, _libConfig, _receiveGracePeriod); + setEnforcedOptions(_oapp, _enforced); + } + + /** + * @notice Sets the delegate for an OApp. Callable only by this config's owner. + * @dev Use to change who can configure the OApp on the endpoint (e.g. switch delegate to another config). + * Caller (this config) must be the OApp's current owner. + * @param _oapp OApp address. + * @param _delegate New delegate address (e.g. this config to keep config, or address(0) to remove). + */ + function setOAppDelegate(address _oapp, address _delegate) external onlyOwner { + IOAppDelegate(_oapp).setDelegate(_delegate); + } + + /** + * @notice Sets the read channel on an OApp. Callable only by this config's owner. + * @dev Use to change the read channel ID or disable receive (active = false). + * @param _oapp OApp address (e.g. UniswapV3ObserveRead). + * @param _channelId Read channel ID. + * @param _active true = set peer to OApp (receive responses), false = disable (peer bytes32(0)). + */ + function setOAppReadChannel(address _oapp, uint32 _channelId, bool _active) external onlyOwner { + IOAppReadChannel(_oapp).setReadChannel(_channelId, _active); + } + + /** + * @notice Transfers ownership of an OApp to a new address. Callable only by this config's owner. + * @dev Use when the OApp owner is this config and you want to pass ownership to another address (e.g. multisig). + * @param _oapp OApp address (must have this config as current owner). + * @param _newOwner New owner address. + */ + function transferOAppOwnership(address _oapp, address _newOwner) external onlyOwner { + IOwnableOApp(_oapp).transferOwnership(_newOwner); + } +} diff --git a/protocols/layerzero-v2/lz-read/README.md b/protocols/layerzero-v2/lz-read/README.md new file mode 100644 index 0000000..6264299 --- /dev/null +++ b/protocols/layerzero-v2/lz-read/README.md @@ -0,0 +1,248 @@ +# Взаимодействие с протоколом LayerZero v2. Часть 4. Omnichain Queries (lzRead) + +**Автор:** [Алексей Куценко](https://github.com/bimkon144) 👨💻 + +Если вы уже разобрались с классическими сообщениями LayerZero (push-модель: отправили сообщение из одной сети и получили в другой сети), следующий шаг — научиться читать состояние других сетей, не разворачивая там свои контракты и не гоняя туда-сюда два сообщения. +Для этого в LayerZero v2 есть lzRead — это request–response (pull) паттерн: контракт в исходной сети отправляет запрос (`lzSend`), а ответ возвращается обратно в исходную сеть и обрабатывается в `lzReceive`. + + + +В статье рассмотрим, как устроен lzRead, из каких контрактов он состоит, как написать и настроить контракт для получения цен из пула Uniswap V3 — с разбором кода и деплоем в [Remix](https://remix.ethereum.org/). + +**Терминология:** + +- **Исходная сеть (origin chain)** — сеть, где развернут ваш контракт, который будет запрашивать данные из другой сети. +- **Сеть данных (data chain / target chain)** — сеть, из которой вы читаете данные. +- **Endpoint** — системный смарт-контракт в каждой сети от LayerZero, через который проходят входящие и исходящие сообщения. +- **EID (Endpoint ID)** — числовой идентификатор сети в протоколе LayerZero. +- **Read Channel** — отдельный канал сообщений именно для чтений; его ID и поддерживаемые пути приведены в [таблицах деплоев](https://docs.layerzero.network/v2/deployments/read-contracts). +- **DVN (Decentralized Verifier Network)** — сеть верификаторов, подтверждающих корректность ответа. +- **ReadLib1002** — message-library для чтений; для lzRead нужны совместимые библиотеки и DVN с доступом к архивным нодам. + +--- + +## Как устроен lzRead + +lzRead позволяет контракту запрашивать и получать состояние из других блокчейнов. В основе лежит идея **BQL (Blockchain Query Language)** — единый способ формулировать запросы (что читать, из какой сети, на каком блоке/времени), получать и при необходимости обрабатывать ответы. + + + +По шагам: + +1. **Формирование запроса** — приложение собирает запрос: какие данные нужны, из какой целевой сети, на каком блоке или времени. Запрос кодируется в стандартную команду по схеме BQL. +2. **Отправка запроса** — команда отправляется через Endpoint LayerZero по отдельному read-каналу (не обычному messaging). По каналу явно передается, что это запрос с ожиданием ответа, а не просто смена состояния. +3. **Получение и верификация данных (DVN data fetch and verification)** — DVN принимают запрос, забирают данные с архивной ноды требуемой сети и при необходимости применяют off-chain compute: **lzMap** (преобразование ответов из одной или нескольких сетей) и **lzReduce** (агрегация нескольких ответов в один). Каждый DVN формирует криптографический хеш результата для проверки целостности. +_В этой статье мы делаем один запрос в одну сеть, поэтому Compute не настраиваем; как задать lzMap/lzReduce для сценариев с несколькими сетями или агрегацией — можно посмотреть в [документации lzRead](https://docs.layerzero.network/v2/developers/evm/lzread/overview#lzmap)._ +4. **Доставка ответа (Response handling)** — после верификации нужным числом DVN Endpoint доставляет итоговый ответ обратно в исходную сеть. Контракт-получатель обрабатывает его в `_lzReceive()`: декодирует payload и использует полученные данные. + +--- + +## Архитектура контрактов + + + +Чтобы контракт мог отправлять read-запросы и получать ответы, нужно наследоваться от [OAppRead.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppRead.sol#L4). Цепочка наследования: + +- [OAppRead.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppRead.sol#L4) -> [OApp.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OApp.sol) +- OApp -> [OAppReceiver.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppReceiver.sol) и [OAppSender.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppSender.sol) +- OAppReceiver, OAppSender -> [OAppCore.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol) +- OAppCore -> [Ownable](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol) (OpenZeppelin) + +Контракты простые — имеет смысл просмотреть их перед следующим этапом. + +Чтобы реализовать lzRead, нужен контракт, наследующий OAppRead и реализующий три части: формирование запроса, оценка комиссии, обработка ответа. В следующем разделе — пример такого контракта и его методы. + +--- + +## Пример OApp контракта (UniswapV3ObserveRead.sol) + +Мы уже написали готовый контракт [UniswapV3ObserveRead.sol](./UniswapV3ObserveRead.sol). Он запрашивает с другой сети (data chain) результат вызова `observe()` у пула Uniswap V3 — накопленные за указанный период значения тика и ликвидности; по ним можно вычислить **TWAP** (Time-Weighted Average Price) — среднюю цену актива за период без развертывания контракта в сети пула. Ответ доставляется обратно в наш контракт в origin. Контракт наследует **OAppRead** и **OAppOptionsType3**. + +- **OAppRead** — отправка read-запроса и прием ответа в `_lzReceive`. +- **OAppOptionsType3** — [библиотека](https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OAppOptionsType3.sol) для опций сообщений. Owner задает принудительные опции (enforced) через **`setEnforcedOptions(EnforcedOptionParam[])`** для пар `(eid, msgType)`; они хранятся в `enforcedOptions[eid][msgType]`. **`combineOptions(eid, msgType, _extraOptions)`** собирает итоговые опции: объединяет эти enforced с опциями вызывающего — параметром `_extraOptions` в `quoteObserve`/`readObserve` (на стороне executor значения складываются) и передает результат в `_lzSend` и `_quote`. Для lzRead опции в формате Type3: газ на доставку ответа, размер ответа в байтах (response size) и value для executor; сборка — `addExecutorLzReadOption(gas, responseSizeBytes, value)`. Если enforced уже заданы с достаточным газом и response size, можно вызывать с `_extraOptions = 0x`, иначе — передать закодированные опции. + +При деплое передаем пять аргументов: + +```solidity +constructor( + address _endpoint, + uint32 _readChannel, + uint32 _targetEid, + address _targetPoolAddress, + address _config // контракт LzReadConfig — деплоим первым +) OAppRead(_endpoint, _config) Ownable(_config) { + READ_CHANNEL = _readChannel; + targetEid = _targetEid; + targetPoolAddress = _targetPoolAddress; + _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); +} +``` + +- **_endpoint** — адрес Endpoint в сети деплоя (origin). Берется из [Chains](https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts) для выбранной origin-сети. +- **_readChannel** — идентификатор read-канала. Берется из [таблицы](https://docs.layerzero.network/v2/deployments/read-contracts) по паре origin и data chain. +- **_targetEid** — EID целевой сети (откуда читаем пул). Берется из [Chains](https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts) для выбранной data chain. +- **_targetPoolAddress** — адрес пула Uniswap V3 в data chain. +- **_config** — адрес контракта [LzReadConfig.sol](./LzReadConfig.sol) (деплоить первым). Передаётся в OAppRead и в Ownable: конфиг сразу становится владельцем OApp, адрес на OApp не хранится. После деплоя на OApp вызовите `setDelegate(_config)`. Позже (от владельца конфига): сменить делегата — `setOAppDelegate(oapp, delegate)`; задать/сменить read-канал — `setOAppReadChannel(oapp, channelId, active)` (active = false чтобы отключить приём); передать владение — `transferOAppOwnership(oapp, newOwner)`. + +Внутри сохраняются `READ_CHANNEL`, `targetEid`, `targetPoolAddress` и вызывается `_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)))` — так мы говорим протоколу, что ответы по этому read-каналу доставлять на этот контракт. + +### Формирование запроса на чтение + +Метод собирает read-команду с помощью библиотеки [ReadCodecV1](https://github.com/LayerZero-Labs/devtools/blob/39dc7f88a1627db4217144e50ee2f07b39935741/packages/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol#L26) (кодирование и декодирование вызовов). + +Цель — закодировать вызов `observe(secondsAgos)` на пуле Uniswap V3. + +```solidity +function getCmd(uint32[] calldata secondsAgos) public view returns (bytes memory) { + bytes memory callData = + abi.encodeWithSelector(IUniswapV3PoolObserve.observe.selector, secondsAgos); + + EVMCallRequestV1[] memory req = new EVMCallRequestV1[](1); + req[0] = EVMCallRequestV1({ + appRequestLabel: 1, // метка запроса + targetEid: targetEid, // EID сети, откуда читать данные + isBlockNum: false, // читать по времени (true = по номеру блока) + blockNumOrTimestamp: uint64(block.timestamp), // таймстамп, на котором будет считывание данных + confirmations: 15, // сколько подтверждений блока нужно + to: targetPoolAddress, // адрес контракта в data chain + callData: callData // закодированный метод который будет вызван для получения информации + }); + + return ReadCodecV1.encode(0, req); // версия 0, один запрос без Compute +} +``` + +- **secondsAgos** — массив «сколько секунд назад» для `observe`; например `[3600,0]` — данные за последний час и «сейчас». + +В массив можно добавить несколько запросов (в т. ч. в разные сети). + +### Оценка комиссии за запрос + +Перед отправкой запроса вызываем view-функцию, чтобы узнать, сколько нативного токена (или LZ token) нужно отправить вместе с `readObserve`. + +```solidity +function quoteObserve( + uint32[] calldata secondsAgos, // те же, что пойдут в readObserve + bytes calldata _extraOptions, // закодированные опции для executor + bool _payInLzToken // true = платить в LZ token, false = в нативном токене сети +) external view returns (MessagingFee memory fee); +``` + +**Зачем `_payInLzToken`:** вы заранее говорите, чем будете платить при вызове `readObserve` — нативным токеном сети или LZ token. В `fee` возвращаются оба значения (`nativeFee` и `lzTokenFee`); используйте то, что соответствует вашему выбору: при `false` смотрите `fee.nativeFee` и передаете эту сумму в `msg.value` в `readObserve`, при `true` — `fee.lzTokenFee`, а оплата LZ token идет через механизм протокола (approve + списание). В статье дальше предполагается оплата нативным токеном (`_payInLzToken = false`). + +Возвращается структура `fee` (поля `nativeFee`, `lzTokenFee`). + +### Отправка read-запроса: `readObserve` + +Отправляет сформированную командой `getCmd(secondsAgos)` read-запрос в read-канал. Вызывать **payable**, с `msg.value >= fee.nativeFee` (значение из `quoteObserve`). + +```solidity +function readObserve( + uint32[] calldata secondsAgos, // массив для observe, например [3600, 0] + bytes calldata _extraOptions // те же опции, что в quoteObserve (см. выше; при тесте 0x) +) external payable returns (MessagingReceipt memory receipt); +``` + +Возвращается `MessagingReceipt` (например, для отслеживания в сканере). Ответ придет асинхронно в `_lzReceive`. + +### Получение ответа: `_lzReceive` + +Вызывается протоколом, когда верифицированный ответ доставлен в origin. В `_message` приходят закодированные возвращаемые значения `observe()`: два массива `(int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulativeX128s)`. + +```solidity +function _lzReceive( + Origin calldata, // метаданные сообщения (srcEid, sender, nonce) + bytes32, // guid сообщения + bytes calldata _message, // ответ: abi-encoded (int56[], uint160[]) + address, // executor + bytes calldata +) internal override { + (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = + abi.decode(_message, (int56[], uint160[])); + emit ObserveResult(tickCumulatives, secondsPerLiquidityCumulativeX128s); +} +``` + +Декодируем payload и эмитим событие `ObserveResult` — по нему можно убедиться, что данные пришли. + +## Конфигурация приложения + +Настройка бывает двух видов: на **endpoint** (библиотеки send/receive, конфиг ReadLib с executor и требования к DVN) и на **OApp** (enforced options, при необходимости смена read-канала). + +Вызывать настройку на endpoint может только сам OApp или его **делегат**; настройку на OApp (например `setEnforcedOptions`) — только **владелец** OApp. По этой причине, на `UniswapV3ObserveRead.sol` в конструкторе назначаем владельцем и делегатом наш контракт конфига. + +Контракт конфига помогает настроить эти параметры [LzReadConfig.sol](./LzReadConfig.sol). + +Настраивать может деплоер, сам контракт (вызов от его адреса) или делегат — через `setDelegate(address _delegate)` из [OAppCore.sol](https://github.com/LayerZero-Labs/LayerZero-v2/blob/ab9b083410b9359285a5756807e1b6145d4711a7/packages/layerzero-v2/evm/oapp/contracts/oapp/OAppCore.sol). + +**Порядок деплоя и настройки:** + +1. Задеплоить [LzReadConfig.sol](./LzReadConfig.sol) с одним аргументом: `_endpoint` (адрес Endpoint для сети из [таблицы](https://docs.layerzero.network/v2/deployments/read-contracts)). +2. Задеплоить [UniswapV3ObserveRead.sol](./UniswapV3ObserveRead.sol), передав в аргументы тот же адрес endpoint и адрес только что задеплоенного контракта конфига. +3. **Одним вызовом настроить и endpoint, и OApp** — метод конфига `configureFull(_oapp, _readChannel, _readLib, _libConfig, _receiveGracePeriod, _enforced)` задаёт на endpoint библиотеки send/receive и конфиг ReadLib, а на OApp — enforced options (газ и размер ответа для lzRead). +Аргументы: + - **\_oapp** — адрес задеплоенного OApp (UniswapV3ObserveRead). Берёте из Remix после деплоя. + - **\_readChannel** — идентификатор read-канала для пары сетей (origin → data chain). Берётся из [таблицы Read Data Channels](https://docs.layerzero.network/v2/deployments/read-contracts) по вашей сети и целевой. + - **\_readLib** — адрес библиотеки Read (например ReadLib1002). Тоже из [той же таблицы](https://docs.layerzero.network/v2/deployments/read-contracts) для вашей сети. + - **\_libConfig** — конфиг ReadLib на endpoint: `(executor, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs[], optionalDVNs[])`. Адреса **executor**, **requiredDVNs** и **optionalDVNs** и массив требуемых DVNs вы можете выбрать из [таблицы](https://docs.layerzero.network/v2/deployments/read-contracts). + - **\_receiveGracePeriod** — задержка активации receive-библиотеки в секундах; обычно **0** (сразу). + - **\_enforced** — принудительные опции для lzRead: структура из трех полей. **eid** (uint32) = тот же readChannel; **msgType** (uint16) = 1 для lzRead; **options** (bytes) — закодированные опции (gas, размер ответа в байтах, value). + - **Поле options:** К сожалению в библиотеках layerZero нет доступа к вспомогательному методу кодировки этих параметров, поэтому я создал для вас [тулзу]((./tools/options-encoder.html)) кодировки, которой вы можете воспользоваться для удобства. Скачиваем файл тулзы и открываем в браузере. + +_В конструкторе OApp уже вызывается `_setPeer(READ_CHANNEL, ...)`. Менять read-канал или отключать приём ответов можно через конфиг: владелец конфига вызывает `LzReadConfig.setOAppReadChannel(адрес OApp, channelId, active)` (active = false чтобы отключить)._ + +_Альтернатива нашему мануальному способу деплою, конфигурации и способу чтению данных является уже репозиторием с уже готовым набором скриптов — [LayerZero CLI](https://docs.layerzero.network/v2/get-started/create-lz-oapp/start)._ + +## Практика + +Представим: в нашей сети (origin) **Base Sepolia** нет цены на токен, которая нужна контракту. Через lzRead можно запросить данные о цене с другой сети — например, с контракта пула Uniswap V3 в сети **Ethereum Sepolia**, где этот токен уже торгуется. + +Ниже — пошаговый порядок: деплой контракта в origin, оценка комиссии через `quoteObserve`, отправка read-запроса через `readObserve`, проверка транзакции в LayerZero Scan и проверка результата (событие `ObserveResult`). + +_Можно повторить с указанными ниже адресами или подставить свои из [списка](https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts), убедившись, что для пары сетей есть Read Path в [Read Data Channels](https://docs.layerzero.network/v2/deployments/read-contracts) и в data chain есть подходящий пул Uniswap V3 с ликвидностью._ + +_Совет:_ после деплоя сразу делайте **Pin contract for current workspace** (значок рядом с адресом контракта в Remix), а адреса копируйте — при смене сети развернутые контракты сбрасываются. Чтобы вызвать методы уже развернутого контракта, во вкладке **Contract** выберите контракт и вставьте его адрес в **At Address**. + +В примере: **origin** = Base Sepolia, **data chain** = Ethereum Sepolia. + +1. Откройте [Remix](https://remix.ethereum.org/) и добавьте контракты [LzReadConfig.sol](./LzReadConfig.sol) и [UniswapV3ObserveRead.sol](./UniswapV3ObserveRead.sol). +2. Собираем все данные для деплоя: +- Endpoint берем для сети origin с [этой](https://docs.layerzero.network/v2/deployments/deployed-contracts) таблицы; +- EID для data chain берем [тут](https://docs.layerzero.network/v2/deployments/deployed-contracts); +- targetPoolAddress для target сети можем найти через [Uniswap deployments](https://docs.uniswap.org/contracts/v3/reference/deployments/ethereum-deployments). Убедитесь, что вызов `observe` на пуле возвращает данные; +- readChannel находим в этой [таблице](https://docs.layerzero.network/v2/deployments/read-contracts), указывая сеть origin и сеть target data; +- readLib для origin сети находим [тут](https://docs.layerzero.network/v2/deployments/read-contracts); +- libConfigParams для origin сети тоже находим [тут](https://docs.layerzero.network/v2/deployments/read-contracts). Он включает в себя такие параметры как: executor, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs, optionalDVNs; +- enforced использует уже известный readChannel, msgType = 1, и options, который мы кодируем через специальную созданную тулзу; + + + +3. Задеплойте сначала [LzReadConfig.sol](./LzReadConfig.sol) (аргумент: endpoint), затем [UniswapV3ObserveRead.sol](./UniswapV3ObserveRead.sol) (endpoint, readChannel, targetEid, targetPoolAddress, адрес LzReadConfig). + + +4. Далее, на LzReadConfig вызовите `configureFull(OApp, readChannel, readLib, libConfigParams, 0, enforcedParams)`. + +5. Оцените комиссию: `quoteObserve(secondsAgos, extraOptions, false)` на задеплоенном контракте `UniswapV3ObserveRead.sol`. Например, `secondsAgos = [3600,0]` для TWAP за последний час. `extraOptions` можно передать `0x` — enforced options уже заданы. + +6. В Remix в поле **Value** укажите `fee.nativeFee` (в Wei) и вызовите `readObserve(secondsAgos, extraOptions)`. + +7. После подтверждения транзакции, в сканере по адресу `UniswapV3ObserveRead.sol` можно найти состояние вашего запроса [testnet.layerzeroscan.com](https://testnet.layerzeroscan.com/). +. + +8. После статуса **Delivered** в origin будет вызван `_lzReceive` и эмитировано событие `ObserveResult`. Проверить можно по ссылке на Response transaction в разделе логов. + + +## Заключение + +С lzRead ваш контракт в одной сети может запросить данные в другой и получить ответ обратно — без деплоя контрактов там и без двух отдельных сообщений туда-сюда. Вы формируете запрос, отправляете его по read-каналу и обрабатываете ответ в `_lzReceive`. Так же, как мы ранее говорили, есть дополнительный функционал — Compute ([lzMap](https://docs.layerzero.network/v2/developers/evm/lzread/overview#lzmap), [lzReduce](https://docs.layerzero.network/v2/developers/evm/lzread/overview#lzreduce)). + +--- + +## Ссылки + +- [Docs: Omnichain Queries (lzRead)](https://docs.layerzero.network/v2/developers/evm/lzread/overview) +- [Read Data Channels](https://docs.layerzero.network/v2/deployments/read-contracts) +- [EVM DVN and Executor Configuration](https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config) +- [The lzRead Deep Dive (MapReduce, BQL)](https://layerzero.network/blog/the-lzread-deep-dive) +- [GitHub: LayerZero v2](https://github.com/LayerZero-Labs/LayerZero-v2) +- [LayerZeroScan](https://layerzeroscan.com/) / [Testnet LayerZeroScan](https://testnet.layerzeroscan.com/) diff --git a/protocols/layerzero-v2/lz-read/UniswapV3ObserveRead.sol b/protocols/layerzero-v2/lz-read/UniswapV3ObserveRead.sol new file mode 100644 index 0000000..cd77501 --- /dev/null +++ b/protocols/layerzero-v2/lz-read/UniswapV3ObserveRead.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ILayerZeroEndpointV2, MessagingFee, MessagingReceipt, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol"; +import { ReadCodecV1, EVMCallRequestV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; +import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol"; +import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; + +/** + * @title UniswapV3ObserveRead + * @notice Contract for querying Uniswap V3 pool observe() on any chain via lzRead and receiving the result on the deployment chain (origin). + * @dev Deploy on any chain (origin). Configure targetEid and targetPoolAddress to read observe(uint32[] secondsAgos) from a pool on another chain (data chain). + */ +interface IUniswapV3PoolObserve { + function observe(uint32[] calldata secondsAgos) + external + view + returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); +} + +contract UniswapV3ObserveRead is OAppRead, OAppOptionsType3 { + /// @notice Emitted when observe() result is received from the target chain. + event ObserveResult(int56[] tickCumulatives, uint160[] secondsPerLiquidityCumulativeX128s); + + /// @notice Message type ID used in OApp options for this read flow. + uint8 private constant READ_MSG_TYPE = 1; + /// @notice Read channel ID: identifies the lzRead channel used to send requests and receive responses (must match LayerZero config). + uint32 public READ_CHANNEL; + + /// @notice LayerZero EID of the chain where the Uniswap V3 pool lives (data chain). + uint32 public immutable targetEid; + /// @notice Uniswap V3 pool address on the target chain. + address public immutable targetPoolAddress; + + constructor( + address _endpoint, + uint32 _readChannel, + uint32 _targetEid, + address _targetPoolAddress, + address _config + ) OAppRead(_endpoint, _config) Ownable(_config) { + READ_CHANNEL = _readChannel; + targetEid = _targetEid; + targetPoolAddress = _targetPoolAddress; + _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))); + } + + /** + * @notice Builds the read command: call observe(secondsAgos) on the pool on the target chain. + * @param secondsAgos Array of seconds "ago" (e.g. [3600, 0] for 1-hour TWAP). + */ + function getCmd(uint32[] calldata secondsAgos) public view returns (bytes memory) { + bytes memory callData = + abi.encodeWithSelector(IUniswapV3PoolObserve.observe.selector, secondsAgos); + + EVMCallRequestV1[] memory req = new EVMCallRequestV1[](1); + req[0] = EVMCallRequestV1({ + appRequestLabel: 1, + targetEid: targetEid, + isBlockNum: false, + blockNumOrTimestamp: uint64(block.timestamp), + confirmations: 15, + to: targetPoolAddress, + callData: callData + }); + + return ReadCodecV1.encode(0, req); + } + + /** + * @notice Sends a read request for observe() to the target chain. + * @param secondsAgos Array of seconds for observe (e.g. [3600, 0]). + * @param _extraOptions Additional message options (gas, fee). + */ + function readObserve( + uint32[] calldata secondsAgos, + bytes calldata _extraOptions + ) external payable returns (MessagingReceipt memory receipt) { + bytes memory cmd = getCmd(secondsAgos); + return _lzSend( + READ_CHANNEL, + cmd, + combineOptions(READ_CHANNEL, READ_MSG_TYPE, _extraOptions), + MessagingFee(msg.value, 0), + payable(msg.sender) + ); + } + + /** + * @notice Quotes the fee for the read observe() call. + */ + function quoteObserve( + uint32[] calldata secondsAgos, + bytes calldata _extraOptions, + bool _payInLzToken + ) external view returns (MessagingFee memory fee) { + bytes memory cmd = getCmd(secondsAgos); + return _quote(READ_CHANNEL, cmd, combineOptions(READ_CHANNEL, READ_MSG_TYPE, _extraOptions), _payInLzToken); + } + + /** + * @notice Handles the observe() response: tickCumulatives and secondsPerLiquidityCumulativeX128s. + */ + function _lzReceive( + Origin calldata, + bytes32, + bytes calldata _message, + address, + bytes calldata + ) internal override { + (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = + abi.decode(_message, (int56[], uint160[])); + emit ObserveResult(tickCumulatives, secondsPerLiquidityCumulativeX128s); + } + + function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner { + _setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0)); + READ_CHANNEL = _channelId; + } + + receive() external payable {} +} diff --git a/protocols/layerzero-v2/lz-read/images/configuration.png b/protocols/layerzero-v2/lz-read/images/configuration.png new file mode 100644 index 0000000..cadd042 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/configuration.png differ diff --git a/protocols/layerzero-v2/lz-read/images/layerzero-scan-delivered.png b/protocols/layerzero-v2/lz-read/images/layerzero-scan-delivered.png new file mode 100644 index 0000000..f2788a0 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/layerzero-scan-delivered.png differ diff --git a/protocols/layerzero-v2/lz-read/images/layerzero-scan-response.png b/protocols/layerzero-v2/lz-read/images/layerzero-scan-response.png new file mode 100644 index 0000000..cc0a1d3 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/layerzero-scan-response.png differ diff --git a/protocols/layerzero-v2/lz-read/images/lzRead_diagram.svg b/protocols/layerzero-v2/lz-read/images/lzRead_diagram.svg new file mode 100644 index 0000000..28f002e --- /dev/null +++ b/protocols/layerzero-v2/lz-read/images/lzRead_diagram.svg @@ -0,0 +1,117 @@ + diff --git a/protocols/layerzero-v2/lz-read/images/oapp-architecture.png b/protocols/layerzero-v2/lz-read/images/oapp-architecture.png new file mode 100644 index 0000000..c9145ec Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/oapp-architecture.png differ diff --git a/protocols/layerzero-v2/lz-read/images/preview.png b/protocols/layerzero-v2/lz-read/images/preview.png new file mode 100644 index 0000000..f4985e3 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/preview.png differ diff --git a/protocols/layerzero-v2/lz-read/images/remix-first-deploy.png b/protocols/layerzero-v2/lz-read/images/remix-first-deploy.png new file mode 100644 index 0000000..65106e5 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/remix-first-deploy.png differ diff --git a/protocols/layerzero-v2/lz-read/images/remix-observe-result-logs.png b/protocols/layerzero-v2/lz-read/images/remix-observe-result-logs.png new file mode 100644 index 0000000..ace8ee7 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/remix-observe-result-logs.png differ diff --git a/protocols/layerzero-v2/lz-read/images/remix-quote-observe.png b/protocols/layerzero-v2/lz-read/images/remix-quote-observe.png new file mode 100644 index 0000000..d1a3da1 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/remix-quote-observe.png differ diff --git a/protocols/layerzero-v2/lz-read/images/remix-read-observe-value.png b/protocols/layerzero-v2/lz-read/images/remix-read-observe-value.png new file mode 100644 index 0000000..2542405 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/remix-read-observe-value.png differ diff --git a/protocols/layerzero-v2/lz-read/images/remix-second-deploy.png b/protocols/layerzero-v2/lz-read/images/remix-second-deploy.png new file mode 100644 index 0000000..24976c5 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/remix-second-deploy.png differ diff --git a/protocols/layerzero-v2/lz-read/images/set-config.png b/protocols/layerzero-v2/lz-read/images/set-config.png new file mode 100644 index 0000000..a04de43 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/set-config.png differ diff --git a/protocols/layerzero-v2/lz-read/images/setconfig-dependencies.png b/protocols/layerzero-v2/lz-read/images/setconfig-dependencies.png new file mode 100644 index 0000000..4272e62 Binary files /dev/null and b/protocols/layerzero-v2/lz-read/images/setconfig-dependencies.png differ diff --git a/protocols/layerzero-v2/lz-read/tools/options-encoder.html b/protocols/layerzero-v2/lz-read/tools/options-encoder.html new file mode 100644 index 0000000..6812bf5 --- /dev/null +++ b/protocols/layerzero-v2/lz-read/tools/options-encoder.html @@ -0,0 +1,151 @@ + + +
+ + +Encodes options for configureFull / setEnforcedOptions (format: TYPE_3, worker 1, option_type 5, gas uint128, calldataSize uint32, value uint128).