Перейти к основному контенту

Разбор контракта стандартного моста Optimism

Solidity
мост
уровень 2 (l2)
Средний уровень
Ори Померанц
30 марта 2022 г.
29 минут на чтение

Optimism (opens in a new tab) — это оптимистичный роллап. Оптимистичные роллапы могут обрабатывать транзакции по гораздо более низкой цене, чем основная сеть Ethereum (также известная как уровень 1 (l1)), поскольку транзакции обрабатываются только несколькими узлами, а не каждым узлом в сети. В то же время все данные записываются на l1, поэтому все можно доказать и восстановить со всеми гарантиями целостности и доступности Мейннета.

Чтобы использовать активы l1 в Optimism (или любом другом l2), активы необходимо перевести через мост. Один из способов добиться этого — заблокировать активы (ETH и токены ERC-20 являются наиболее распространенными) на l1 и получить эквивалентные активы для использования на l2. В конечном итоге тот, у кого они окажутся, может захотеть перевести их обратно на l1 через мост. При этом активы сжигаются на l2, а затем возвращаются пользователю на l1.

Именно так работает стандартный мост Optimism (opens in a new tab). В этой статье мы рассмотрим исходный код этого моста, чтобы понять, как он работает, и изучим его как пример хорошо написанного кода на Solidity.

Потоки управления

У моста есть два основных потока:

  • Депозит (с l1 на l2)
  • Вывод (с l2 на l1)

Поток депозита

Уровень 1 (l1)

  1. При внесении депозита в ERC-20 вкладчик дает мосту разрешение на расходование вносимой суммы.
  2. Вкладчик вызывает мост l1 (depositERC20, depositERC20To, depositETH или depositETHTo).
  3. Мост l1 вступает во владение переводимым активом.
    • ETH: Актив переводится вкладчиком в рамках вызова.
    • ERC-20: Актив переводится мостом самому себе с использованием разрешения, предоставленного вкладчиком.
  4. Мост l1 использует механизм кросс-доменных сообщений для вызова finalizeDeposit на мосту l2.

Уровень 2 (l2)

  1. Мост l2 проверяет законность вызова finalizeDeposit:
    • Поступил от контракта кросс-доменных сообщений.
    • Изначально исходил от моста на l1.
  2. Мост l2 проверяет, является ли контракт токена ERC-20 на l2 правильным:
    • Контракт l2 сообщает, что его аналог на l1 совпадает с тем, от которого поступили токены на l1.
    • Контракт l2 сообщает, что он поддерживает правильный интерфейс (с использованием ERC-165 (opens in a new tab)).
  3. Если контракт l2 правильный, он вызывается, чтобы чеканить соответствующее количество токенов на соответствующий адрес. Если нет, запускается процесс вывода, чтобы позволить пользователю востребовать токены на l1.

Поток вывода

Уровень 2 (l2)

  1. Пользователь, осуществляющий вывод, вызывает мост l2 (withdraw или withdrawTo).
  2. Мост l2 сжигает соответствующее количество токенов, принадлежащих msg.sender.
  3. Мост l2 использует механизм кросс-доменных сообщений для вызова finalizeETHWithdrawal или finalizeERC20Withdrawal на мосту l1.

Уровень 1 (l1)

  1. Мост l1 проверяет законность вызова finalizeETHWithdrawal или finalizeERC20Withdrawal:
    • Поступил от механизма кросс-доменных сообщений.
    • Изначально исходил от моста на l2.
  2. Мост l1 переводит соответствующий актив (ETH или ERC-20) на соответствующий адрес.

Код уровня 1 (l1)

Это код, который выполняется на l1, в основной сети Ethereum.

IL1ERC20Bridge

Этот интерфейс определен здесь (opens in a new tab). Он включает функции и определения, необходимые для перевода токенов ERC-20 через мост.

// SPDX-License-Identifier: MIT

Большая часть кода Optimism выпущена под лицензией MIT (opens in a new tab).

pragma solidity >0.5.0 <0.9.0;

На момент написания статьи последней версией Solidity является 0.8.12. Пока не будет выпущена версия 0.9.0, мы не знаем, совместим ли этот код с ней или нет.

В терминологии моста Optimism депозит означает перевод с l1 на l2, а вывод означает перевод с l2 на l1.

        address indexed _l1Token,
        address indexed _l2Token,

В большинстве случаев адрес ERC-20 на l1 не совпадает с адресом эквивалентного ERC-20 на l2. Вы можете посмотреть список адресов токенов здесь (opens in a new tab). Адрес с chainId 1 находится на l1 (Мейннет), а адрес с chainId 10 — на l2 (Optimism). Два других значения chainId предназначены для тестовой сети Kovan (42) и тестовой сети Optimistic Kovan (69).

        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

К переводам можно добавлять примечания, и в этом случае они добавляются к событиям, которые о них сообщают.

    event ERC20WithdrawalFinalized(
        address indexed _l1Token,
        address indexed _l2Token,
        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

Один и тот же контракт моста обрабатывает переводы в обоих направлениях. В случае моста l1 это означает инициализацию депозитов и завершение выводов.

Эта функция на самом деле не нужна, потому что на l2 это предварительно развернутый контракт, поэтому он всегда находится по адресу 0x4200000000000000000000000000000000000010. Она здесь для симметрии с мостом l2, потому что адрес моста l1 узнать не так просто.

Параметр _l2Gas — это количество газа l2, которое разрешено потратить транзакции. До определенного (высокого) предела это бесплатно (opens in a new tab), поэтому, если контракт ERC-20 не делает ничего действительно странного при чеканке, это не должно быть проблемой. Эта функция заботится об обычном сценарии, когда пользователь переводит активы через мост на тот же адрес в другом блокчейне.

Эта функция почти идентична depositERC20, но она позволяет отправить ERC-20 на другой адрес.

Выводы (и другие сообщения с l2 на l1) в Optimism — это двухэтапный процесс:

  1. Инициирующая транзакция на l2.
  2. Завершающая транзакция или транзакция востребования на l1. Эта транзакция должна произойти после окончания периода оспаривания ошибок (opens in a new tab) для транзакции l2.

IL1StandardBridge

Этот интерфейс определен здесь (opens in a new tab). Этот файл содержит определения событий и функций для ETH. Эти определения очень похожи на те, что определены в IL1ERC20Bridge выше для ERC-20.

Интерфейс моста разделен на два файла, потому что некоторые токены ERC-20 требуют пользовательской обработки и не могут обрабатываться стандартным мостом. Таким образом, пользовательский мост, который обрабатывает такой токен, может реализовать IL1ERC20Bridge и не должен также переводить ETH.

Это событие почти идентично версии ERC-20 (ERC20DepositInitiated), за исключением отсутствия адресов токенов l1 и l2. То же самое верно и для других событий и функций.

CrossDomainEnabled

Этот контракт (opens in a new tab) наследуется обоими мостами (l1 и l2) для отправки сообщений на другой уровень.

// SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;

/* Импорты интерфейсов */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Этот интерфейс (opens in a new tab) сообщает контракту, как отправлять сообщения на другой уровень, используя кросс-доменный мессенджер. Этот кросс-доменный мессенджер — совершенно другая система, и она заслуживает отдельной статьи, которую я надеюсь написать в будущем.

Единственный параметр, который должен знать контракт, — это адрес кросс-доменного мессенджера на этом уровне. Этот параметр устанавливается один раз в конструкторе и никогда не меняется.

Кросс-доменный обмен сообщениями доступен любому контракту в блокчейне, где он запущен (будь то основная сеть Ethereum или Optimism). Но нам нужно, чтобы мост на каждой стороне доверял только определенным сообщениям, если они исходят от моста на другой стороне.

        require(
            msg.sender == address(getCrossDomainMessenger()),
            "OVM_XCHAIN: messenger contract unauthenticated"
        );

Доверять можно только сообщениям от соответствующего кросс-доменного мессенджера (messenger, как вы увидите ниже).


        require(
            getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
            "OVM_XCHAIN: wrong sender of cross-domain message"
        );

Способ, которым кросс-доменный мессенджер предоставляет адрес, отправивший сообщение с другого уровня, — это функция .xDomainMessageSender() (opens in a new tab). Пока она вызывается в транзакции, инициированной сообщением, она может предоставить эту информацию.

Нам нужно убедиться, что полученное нами сообщение пришло от другого моста.

Эта функция возвращает кросс-доменный мессенджер. Мы используем функцию, а не переменную messenger, чтобы позволить контрактам, наследующим от этого, использовать алгоритм для указания того, какой кросс-доменный мессенджер использовать.

Наконец, функция, которая отправляет сообщение на другой уровень.

    ) internal {
        // slither-disable-next-line reentrancy-events, reentrancy-benign

Слизер (opens in a new tab) — это статический анализатор, который Optimism запускает на каждом контракте для поиска уязвимостей и других потенциальных проблем. В данном случае следующая строка вызывает две уязвимости:

  1. События повторного входа (opens in a new tab)
  2. Безопасный повторный вход (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

В этом случае мы не беспокоимся о повторном входе, мы знаем, что getCrossDomainMessenger() возвращает заслуживающий доверия адрес, даже если Слизер никак не может этого знать.

Контракт моста l1

Исходный код этого контракта находится здесь (opens in a new tab).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

Интерфейсы могут быть частью других контрактов, поэтому они должны поддерживать широкий спектр версий Solidity. Но сам мост — это наш контракт, и мы можем быть строгими в отношении того, какую версию Solidity он использует.

/* Импорты интерфейсов */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge и IL1StandardBridge объяснены выше.

import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";

Этот интерфейс (opens in a new tab) позволяет нам создавать сообщения для управления стандартным мостом на l2.

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Этот интерфейс (opens in a new tab) позволяет нам управлять контрактами ERC-20. Вы можете прочитать об этом подробнее здесь.

/* Импорты библиотек */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Как объяснялось выше, этот контракт используется для межсетевого обмена сообщениями.

import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";

Lib_PredeployAddresses (opens in a new tab) содержит адреса контрактов l2, которые всегда имеют один и тот же адрес. Сюда входит стандартный мост на l2.

import { Address } from "@openzeppelin/contracts/utils/Address.sol";

Утилиты адресов ОпенЗеппелин (opens in a new tab). Они используются для различения адресов контрактов и адресов, принадлежащих внешним аккаунтам (EOA).

Обратите внимание, что это не идеальное решение, поскольку нет способа отличить прямые вызовы от вызовов, сделанных из конструктора контракта, но, по крайней мере, это позволяет нам выявлять и предотвращать некоторые распространенные ошибки пользователей.

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

Стандарт ERC-20 (opens in a new tab) поддерживает два способа сообщения контрактом об ошибке:

  1. Откат
  2. Возврат false

Обработка обоих случаев усложнила бы наш код, поэтому вместо этого мы используем SafeERC20 от ОпенЗеппелин (opens in a new tab), который гарантирует, что все сбои приводят к откату (opens in a new tab).

В этой строке мы указываем использовать обертку SafeERC20 каждый раз, когда мы используем интерфейс IERC20.


    /********************************
     * Ссылки на внешние контракты *
     ********************************/

    address public l2TokenBridge;

Адрес L2StandardBridge.


    // Сопоставляет токен l1 с токеном l2 и балансом внесенного токена l1
    mapping(address => mapping(address => uint256)) public deposits;

Двойное отображение (mapping) (opens in a new tab), подобное этому, — это способ определения двумерного разреженного массива (opens in a new tab). Значения в этой структуре данных идентифицируются как deposit[L1 token addr][L2 token addr]. Значение по умолчанию — ноль. В хранилище записываются только те ячейки, которым задано другое значение.


    /***************
     * Конструктор *
     ***************/

    // Этот контракт находится за прокси, поэтому параметры конструктора не будут использоваться.
    constructor() CrossDomainEnabled(address(0)) {}

Мы хотим иметь возможность обновлять этот контракт без необходимости копировать все переменные в хранилище. Для этого мы используем Proxy (opens in a new tab) — прокси-контракт, который использует delegatecall (opens in a new tab) для передачи вызовов отдельному контракту, адрес которого хранится в прокси-контракте (при обновлении вы указываете прокси изменить этот адрес). Когда вы используете delegatecall, хранилище остается хранилищем вызывающего контракта, поэтому значения всех переменных состояния контракта остаются неизменными.

Одним из эффектов этого шаблона является то, что хранилище контракта, который является вызываемым для delegatecall, не используется, и поэтому переданные ему значения конструктора не имеют значения. По этой причине мы можем передать бессмысленное значение конструктору CrossDomainEnabled. Это также причина, по которой инициализация ниже отделена от конструктора.

Этот тест Слизера (opens in a new tab) определяет функции, которые не вызываются из кода контракта и поэтому могут быть объявлены как external вместо public. Стоимость газа для функций external может быть ниже, поскольку им могут быть переданы параметры в данных вызова. Функции, объявленные как public, должны быть доступны изнутри контракта. Контракты не могут изменять свои собственные данные вызова, поэтому параметры должны находиться в памяти. Когда такая функция вызывается извне, необходимо скопировать данные вызова в память, что стоит газа. В данном случае функция вызывается только один раз, поэтому неэффективность не имеет для нас значения.

    function initialize(address _l1messenger, address _l2TokenBridge) public {
        require(messenger == address(0), "Contract has already been initialized.");

Функция initialize должна вызываться только один раз. Если адрес кросс-доменного мессенджера l1 или моста токенов l2 изменится, мы создадим новый прокси и новый мост, который его вызывает. Вряд ли это произойдет, за исключением случаев обновления всей системы, что случается очень редко.

Обратите внимание, что в этой функции нет механизма, ограничивающего то, кто может ее вызывать. Это означает, что теоретически злоумышленник может подождать, пока мы развернем прокси и первую версию моста, а затем использовать фронтраннинг (opens in a new tab), чтобы добраться до функции initialize раньше законного пользователя. Но есть два метода предотвратить это:

  1. Если контракты развертываются не напрямую через EOA, а в транзакции, в которой их создает другой контракт (opens in a new tab), весь процесс может быть атомарным и завершиться до выполнения любой другой транзакции.
  2. Если законный вызов initialize завершается неудачей, всегда можно проигнорировать недавно созданные прокси и мост и создать новые.
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

Это два параметра, которые должен знать мост.

Именно по этой причине нам понадобились утилиты Address от ОпенЗеппелин.

Эта функция существует для целей тестирования. Обратите внимание, что она не появляется в определениях интерфейса — она не предназначена для обычного использования.

Эти две функции являются обертками вокруг _initiateETHDeposit — функции, которая обрабатывает фактический депозит ETH.

Кросс-доменные сообщения работают таким образом, что целевой контракт вызывается с сообщением в качестве его данных вызова. Контракты Solidity всегда интерпретируют свои данные вызова в соответствии со спецификациями ABI (opens in a new tab). Функция Solidity abi.encodeWithSelector (opens in a new tab) создает эти данные вызова.

            IL2ERC20Bridge.finalizeDeposit.selector,
            address(0),
            Lib_PredeployAddresses.OVM_ETH,
            _from,
            _to,
            msg.value,
            _data
        );

Сообщение здесь заключается в вызове функции finalizeDeposit (opens in a new tab) со следующими параметрами:

ПараметрЗначениеЗначение
_l1Tokenaddress(0)Специальное значение, обозначающее ETH (который не является токеном ERC-20) на l1
_l2TokenLib_PredeployAddresses.OVM_ETHКонтракт l2, который управляет ETH в Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (этот контракт предназначен только для внутреннего использования Optimism)
_from_fromАдрес на l1, который отправляет ETH
_to_toАдрес на l2, который получает ETH
amountmsg.valueКоличество отправленных Wei (которые уже были отправлены на мост)
_data_dataДополнительные данные для прикрепления к депозиту
        // Отправить данные вызова на l2
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Отправка сообщения через кросс-доменный мессенджер.

        // slither-disable-next-line reentrancy-events
        emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

Генерация события для информирования любого децентрализованного приложения (dapp), которое прослушивает этот перевод.

Эти две функции являются обертками вокруг _initiateERC20Deposit — функции, которая обрабатывает фактический депозит ERC-20.

Эта функция похожа на _initiateETHDeposit выше, с несколькими важными отличиями. Первое отличие заключается в том, что эта функция получает адреса токенов и сумму для перевода в качестве параметров. В случае с ETH вызов моста уже включает перевод актива на аккаунт моста (msg.value).

        // Когда внесение инициируется на l1, мост l1 переводит средства себе для будущих
        // выводов. safeTransferFrom также проверяет, есть ли у контракта код, поэтому это завершится ошибкой, если
        // _from является EOA или address(0).
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

Переводы токенов ERC-20 следуют другому процессу, нежели ETH:

  1. Пользователь (_from) дает мосту разрешение на перевод соответствующих токенов.
  2. Пользователь вызывает мост с адресом контракта токена, суммой и т. д.
  3. Мост переводит токены (самому себе) в рамках процесса депозита.

Первый шаг может произойти в отдельной транзакции от последних двух. Однако фронтраннинг не является проблемой, поскольку две функции, вызывающие _initiateERC20Deposit (depositERC20 и depositERC20To), вызывают эту функцию только с msg.sender в качестве параметра _from.

Добавление внесенного количества токенов в структуру данных deposits. На l2 может быть несколько адресов, соответствующих одному и тому же токену ERC-20 на l1, поэтому недостаточно использовать баланс моста для токена ERC-20 на l1 для отслеживания депозитов.

Мост l2 отправляет сообщение кросс-доменному мессенджеру l2, что заставляет кросс-доменный мессенджер l1 вызвать эту функцию (конечно, после того, как транзакция, завершающая сообщение (opens in a new tab), будет отправлена на l1).

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Убедитесь, что это законное сообщение, исходящее от кросс-доменного мессенджера и происходящее от моста токенов l2. Эта функция используется для вывода ETH из моста, поэтому мы должны убедиться, что она вызывается только авторизованным вызывающим абонентом.

        // slither-disable-next-line reentrancy-events
        (bool success, ) = _to.call{ value: _amount }(new bytes(0));

Способ перевода ETH заключается в вызове получателя с количеством Wei в msg.value.

        require(success, "TransferHelper::safeTransferETH: ETH transfer failed");

        // slither-disable-next-line reentrancy-events
        emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Генерация события о выводе.

Эта функция похожа на finalizeETHWithdrawal выше, с необходимыми изменениями для токенов ERC-20.

        deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;

Обновление структуры данных deposits.

Существовала более ранняя реализация моста. Когда мы перешли от той реализации к этой, нам пришлось перенести все активы. Токены ERC-20 можно просто перенести. Однако для перевода ETH на контракт вам нужно одобрение этого контракта, что и предоставляет нам donateETH.

Токены ERC-20 на l2

Чтобы токен ERC-20 подходил для стандартного моста, он должен позволять стандартному мосту и только стандартному мосту чеканить токен. Это необходимо, потому что мосты должны гарантировать, что количество токенов, циркулирующих в Optimism, равно количеству токенов, заблокированных внутри контракта моста l1. Если на l2 будет слишком много токенов, некоторые пользователи не смогут перевести свои активы обратно на l1 через мост. Вместо надежного моста мы бы по сути воссоздали банковскую систему с частичным резервированием (opens in a new tab). Если на l1 будет слишком много токенов, некоторые из этих токенов навсегда останутся заблокированными внутри контракта моста, потому что нет способа высвободить их без сжигания токенов l2.

IL2StandardERC20

Каждый токен ERC-20 на l2, который использует стандартный мост, должен предоставлять этот интерфейс (opens in a new tab), в котором есть функции и события, необходимые стандартному мосту.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Стандартный интерфейс ERC-20 (opens in a new tab) не включает функции mint и burn. Эти методы не требуются стандартом ERC-20 (opens in a new tab), который оставляет неуказанными механизмы создания и уничтожения токенов.

import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

Интерфейс ERC-165 (opens in a new tab) используется для указания того, какие функции предоставляет контракт. Вы можете прочитать стандарт здесь (opens in a new tab).

interface IL2StandardERC20 is IERC20, IERC165 {
    function l1Token() external returns (address);

Эта функция предоставляет адрес токена l1, который переводится через мост на этот контракт. Обратите внимание, что у нас нет аналогичной функции в обратном направлении. Нам нужно иметь возможность переводить через мост любой токен l1, независимо от того, планировалась ли поддержка l2 при его реализации или нет.


    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;

    event Mint(address indexed _account, uint256 _amount);
    event Burn(address indexed _account, uint256 _amount);
}

Функции и события для того, чтобы чеканить (создавать) и сжигать (уничтожать) токены. Мост должен быть единственной сущностью, которая может запускать эти функции, чтобы гарантировать правильное количество токенов (равное количеству токенов, заблокированных на l1).

L2StandardERC20

Это наша реализация интерфейса IL2StandardERC20 (opens in a new tab). Если вам не нужна какая-то пользовательская логика, вам следует использовать эту.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

Контракт ERC-20 от ОпенЗеппелин (opens in a new tab). Optimism не верит в изобретение велосипеда, особенно когда велосипед хорошо проверен и должен быть достаточно надежным для хранения активов.

import "./IL2StandardERC20.sol";

contract L2StandardERC20 is IL2StandardERC20, ERC20 {
    address public l1Token;
    address public l2Bridge;

Это два дополнительных параметра конфигурации, которые нам требуются, а ERC-20 обычно нет.

Сначала вызывается конструктор для контракта, от которого мы наследуем (ERC20(_name, _symbol)), а затем устанавливаются наши собственные переменные.

Именно так работает ERC-165 (opens in a new tab). Каждый интерфейс представляет собой ряд поддерживаемых функций и идентифицируется как исключающее ИЛИ (opens in a new tab) селекторов функций ABI (opens in a new tab) этих функций.

Мост l2 использует ERC-165 в качестве проверки работоспособности, чтобы убедиться, что контракт ERC-20, на который он отправляет активы, является IL2StandardERC20.

Примечание: Ничто не мешает мошенническому контракту предоставлять ложные ответы на supportsInterface, поэтому это механизм проверки работоспособности, а не механизм безопасности.

Только мосту l2 разрешено чеканить и сжигать активы.

_mint и _burn на самом деле определены в контракте ERC-20 от ОпенЗеппелин. Этот контракт просто не раскрывает их извне, потому что условия для того, чтобы чеканить и сжигать токены, так же разнообразны, как и количество способов использования ERC-20.

Код моста l2

Это код, который запускает мост в Optimism. Исходный код этого контракта находится здесь (opens in a new tab).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* Импорты интерфейсов */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

Интерфейс IL2ERC20Bridge (opens in a new tab) очень похож на эквивалент l1, который мы видели выше. Есть два существенных отличия:

  1. На l1 вы инициируете депозиты и завершаете выводы. Здесь вы инициируете выводы и завершаете депозиты.
  2. На l1 необходимо различать токены ETH и ERC-20. На l2 мы можем использовать одни и те же функции для обоих, потому что внутренне балансы ETH в Optimism обрабатываются как токен ERC-20 с адресом 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).

Отслеживание адреса моста l1. Обратите внимание, что в отличие от эквивалента l1, здесь нам нужна эта переменная. Адрес моста l1 заранее неизвестен.

Эти две функции инициируют выводы. Обратите внимание, что нет необходимости указывать адрес токена l1. Ожидается, что токены l2 сообщат нам адрес эквивалента l1.

Обратите внимание, что мы не полагаемся на параметр _from, а на msg.sender, который гораздо сложнее подделать (насколько мне известно, невозможно).


        // Сформировать данные вызова для l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
        // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

На l1 необходимо различать ETH и ERC-20.

Эта функция вызывается L1StandardBridge.

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Убедитесь, что источник сообщения является законным. Это важно, потому что эта функция вызывает _mint и может быть использована для выдачи токенов, которые не покрываются токенами, которыми мост владеет на l1.

        // Проверить, что целевой токен совместим, и
        // убедиться, что внесенный токен на l1 соответствует представлению внесенного токена l2 здесь
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

Проверки работоспособности:

  1. Поддерживается правильный интерфейс
  2. Адрес l1 контракта ERC-20 на l2 совпадает с источником токенов на l1
        ) {
            // Когда внесение завершается, мы пополняем аккаунт на l2 на ту же сумму
            // токенов.
            // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Если проверки работоспособности пройдены, завершите депозит:

  1. Чеканить токены
  2. Сгенерировать соответствующее событие

Если пользователь совершил обнаруживаемую ошибку, использовав неправильный адрес токена l2, мы хотим отменить депозит и вернуть токены на l1. Единственный способ сделать это с l2 — отправить сообщение, которому придется ждать периода оспаривания ошибок, но это гораздо лучше для пользователя, чем потерять токены навсегда.

Заключение

Стандартный мост — самый гибкий механизм для перевода активов. Однако из-за того, что он такой универсальный, это не всегда самый простой в использовании механизм. Особенно для выводов большинство пользователей предпочитают использовать сторонние мосты (opens in a new tab), которые не ждут периода оспаривания и не требуют доказательства Меркла для завершения вывода.

Эти мосты обычно работают за счет наличия активов на l1, которые они предоставляют немедленно за небольшую плату (часто меньше стоимости газа для вывода через стандартный мост). Когда мост (или люди, управляющие им) предвидит нехватку активов на l1, он переводит достаточное количество активов с l2. Поскольку это очень большие выводы, стоимость вывода амортизируется на большую сумму и составляет гораздо меньший процент.

Надеемся, эта статья помогла вам лучше понять, как работает уровень 2 (l2) и как писать понятный и безопасный код на Solidity.

Смотрите здесь другие мои работы (opens in a new tab).