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

Пошаговый обзор контракта стандартного Моста Optimism

Solidity
Мост
уровень 2
Intermediate
Ori Pomerantz
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

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

Уровень 2

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

Поток вывода

Уровень 2

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

Уровень 1

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

Код уровня 1

Это код, который выполняется на 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;

/* Interface Imports */
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

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

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

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

Контракт Моста L1

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

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

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

/* Interface Imports */
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. Вы можете прочитать больше об этом здесь.

/* Library Imports */
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";

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

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

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

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

  1. Revert (откат)
  2. Вернуть false

Обработка обоих случаев усложнила бы наш код, поэтому вместо этого мы используем SafeERC20 от OpenZeppelin (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][адрес токена L2]. Значение по умолчанию равно нулю. В хранилище записываются только ячейки, которым присвоено другое значение.


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

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

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

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

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

    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 от OpenZeppelin.

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

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

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

            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
суммаmsg.valueКоличество отправленных wei (которые уже отправлены на Мост)
_data_dataДополнительные данные для присоединения к депозиту
        // Отправить calldata на L2
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

Выпустить событие, чтобы уведомить любое децентрализованное приложение, которое отслеживает этот перевод.

Эти две функции являются обертками _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. Мост переводит токены (самому себе) в рамках процесса депозита.

Первый шаг может выполняться в отдельной транзакции от двух последних. Однако упреждающее выполнение (front-running) не является проблемой, поскольку две функции, которые вызывают _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 от OpenZeppelin (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 от OpenZeppelin. Этот контракт просто не предоставляет их внешне, потому что условия для выпуска и сжигания токенов столь же разнообразны, как и количество способов использования ERC-20.

Код Моста L2

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

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

/* Interface Imports */
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, который намного сложнее подделать (насколько я знаю, невозможно).


        // Конструируем calldata для 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 контракта L2 ERC-20 соответствует источнику токенов на 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 и как писать понятный и безопасный код на Solidity.

Больше моих работ смотрите здесь (opens in a new tab).

Последнее обновление страницы: 3 апреля 2026 г.

Было ли это руководство полезным?