Перейти к основному содержанию

Пошаговый обзор контракта стандартного Моста 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 через Мост.

1// SPDX-License-Identifier: MIT

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

1pragma solidity >0.5.0 <0.9.0;

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

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * События *
7 **********/
8
9 event ERC20DepositInitiated(
Показать все

В терминологии Моста Optimism депозит означает перевод с L1 на L2, а вывод — перевод с L2 на L1.

1 address indexed _l1Token,
2 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).

1 address indexed _from,
2 address _to,
3 uint256 _amount,
4 bytes _data
5 );

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

1 event ERC20WithdrawalFinalized(
2 address indexed _l1Token,
3 address indexed _l2Token,
4 address indexed _from,
5 address _to,
6 uint256 _amount,
7 bytes _data
8 );

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

1
2 /********************
3 * Публичные функции *
4 ********************/
5
6 /**
7 * @dev Получить адрес соответствующего контракта Моста L2.
8 * @return Адрес соответствующего контракта Моста L2.
9 */
10 function l2TokenBridge() external returns (address);
Показать все

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

1 /**
2 * @dev внести сумму ERC20 на баланс вызывающего на L2.
3 * @param _l1Token Адрес ERC20 на L1, который мы вносим
4 * @param _l2Token Адрес соответствующего ERC20 на L2
5 * @param _amount Сумма ERC20 для внесения
6 * @param _l2Gas Лимит газа, необходимый для завершения депозита на L2.
7 * @param _data Необязательные данные для пересылки на L2. Эти данные предоставляются
8 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
9 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Показать все

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

1 /**
2 * @dev внести сумму ERC20 на баланс получателя на L2.
3 * @param _l1Token Адрес ERC20 на L1, который мы вносим
4 * @param _l2Token Адрес соответствующего ERC20 на L2
5 * @param _to Адрес L2, на который будет зачислен вывод средств.
6 * @param _amount Сумма ERC20 для внесения.
7 * @param _l2Gas Лимит газа, необходимый для завершения депозита на L2.
8 * @param _data Необязательные данные для пересылки на L2. Эти данные предоставляются
9 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
10 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
Показать все

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

1 /*************************
2 * Межсетевые функции *
3 *************************/
4
5 /**
6 * @dev Завершить вывод с L2 на L1 и зачислить средства на баланс получателя
7 * токена ERC20 на L1.
8 * Этот вызов не удастся, если инициированный вывод с L2 не был завершен.
9 *
10 * @param _l1Token Адрес токена L1 для finalizeWithdrawal.
11 * @param _l2Token Адрес токена L2, на котором был инициирован вывод.
12 * @param _from Адрес L2, инициирующий перевод.
13 * @param _to Адрес L1, на который будет зачислен вывод.
14 * @param _amount Сумма ERC20 для внесения.
15 * @param _data Данные, предоставленные отправителем на L2. Эти данные предоставляются
16 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
17 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Показать все

Вывод средств (и другие сообщения с 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 через Мост.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * События *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Показать все

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

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Публичные функции *
9 ********************/
10
11 /**
12 * @dev Депозит суммы ETH на баланс вызывающего на L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Депозит суммы ETH на баланс получателя на L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Межсетевые функции *
33 *************************/
34
35 /**
36 * @dev Завершить вывод с L2 на L1 и зачислить средства на баланс получателя
37 * токена ETH на L1. Поскольку только xDomainMessenger может вызывать эту функцию, она никогда не будет вызвана
38 * до завершения вывода.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Показать все

CrossDomainEnabled

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

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Interface Imports */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

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

1/**
2 * @title CrossDomainEnabled
3 * @dev Вспомогательный контракт для контрактов, выполняющих междоменные коммуникации
4 *
5 * Используемый компилятор: определяется наследующим контрактом
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Переменные *
10 *************/
11
12 // Контракт-мессенджер, используемый для отправки и получения сообщений из другого домена.
13 address public messenger;
14
15 /***************
16 * Конструктор *
17 ***************/
18
19 /**
20 * @param _messenger Адрес CrossDomainMessenger на текущем уровне.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Показать все

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

1
2 /**********************
3 * Модификаторы функций *
4 **********************/
5
6 /**
7 * Гарантирует, что измененную функцию может вызывать только определенная междоменная учетная запись.
8 * @param _sourceDomainAccount Единственный аккаунт в исходном домене, который аутентифицирован для вызова этой функции.
9 */
10 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Показать все

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

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

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

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

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

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

1
2 _;
3 }
4
5 /**********************
6 * Внутренние функции *
7 **********************/
8
9 /**
10 * Получает мессенджер, обычно из хранилища. Эта функция доступна на случай, если дочернему контракту
11 * потребуется переопределение.
12 * @return Адрес контракта междоменного мессенджера, который следует использовать.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Показать все

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

1
2 /**
3 * Отправляет сообщение на аккаунт в другом домене
4 * @param _crossDomainTarget Предполагаемый получатель в домене назначения
5 * @param _message Данные для отправки цели (обычно calldata для функции с
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit gasLimit для получения сообщения в целевом домене.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Показать все

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

1 ) internal {
2 // 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)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

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

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

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

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

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

1/* Interface Imports */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge и IL1StandardBridge описаны выше.

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

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

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

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

1/* Library Imports */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

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

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

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

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

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

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

1import { 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).

1/**
2 * @title L1StandardBridge
3 * @dev Мост L1 для ETH и ERC20 — это контракт, который хранит депонированные средства L1 и стандартные
4 * токены, используемые на L2. Он синхронизирует соответствующий Мост L2, информируя его о депозитах
5 * и прослушивая его на предмет новых завершенных выводов.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Показать все

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

1
2 /********************************
3 * Ссылки на внешние контракты *
4 ********************************/
5
6 address public l2TokenBridge;

Адрес L2StandardBridge.

1
2 // Сопоставляет токен L1 с токеном L2 для баланса депонированного токена L1
3 mapping(address => mapping(address => uint256)) public deposits;

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

1
2 /***************
3 * Конструктор *
4 ***************/
5
6 // Этот контракт находится за прокси, поэтому параметры конструктора не будут использоваться.
7 constructor() CrossDomainEnabled(address(0)) {}

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

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

1 /******************
2 * Инициализация *
3 ******************/
4
5 /**
6 * @param _l1messenger Адрес мессенджера L1, используемый для межсетевых коммуникаций.
7 * @param _l2TokenBridge Адрес стандартного Моста L2.
8 */
9 // slither-disable-next-line external-function
Показать все

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

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 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 не удался, всегда можно проигнорировать вновь созданный прокси и Мост и создать новые.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

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

1
2 /**************
3 * Внесение депозита *
4 **************/
5
6 /** @dev Модификатор, требующий, чтобы отправитель был EOA. Эту проверку можно обойти с помощью вредоносного
7 * контракта через initcode, но она помогает избежать ошибки пользователя, которой мы хотим избежать.
8 */
9 modifier onlyEOA() {
10 // Используется для прекращения депозитов от контрактов (во избежание случайной потери токенов)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
Показать все

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

1 /**
2 * @dev Эту функцию можно вызвать без данных
3 * для внесения суммы ETH на баланс вызывающего на L2.
4 * Поскольку функция receive не принимает данные, консервативная
5 * сумма по умолчанию пересылается на L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Показать все

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

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
Показать все

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

1 /**
2 * @dev Выполняет логику для депозитов, сохраняя ETH и информируя шлюз L2 ETH о
3 * депозите.
4 * @param _from Аккаунт для снятия депозита на L1.
5 * @param _to Аккаунт для зачисления депозита на L2.
6 * @param _l2Gas Лимит газа, необходимый для завершения депозита на L2.
7 * @param _data Необязательные данные для пересылки на L2. Эти данные предоставляются
8 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
9 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Конструируем calldata для вызова finalizeDeposit
18 bytes memory message = abi.encodeWithSelector(
Показать все

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

1 IL2ERC20Bridge.finalizeDeposit.selector,
2 address(0),
3 Lib_PredeployAddresses.OVM_ETH,
4 _from,
5 _to,
6 msg.value,
7 _data
8 );

Здесь сообщение вызывает функцию 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Дополнительные данные для присоединения к депозиту
1 // Отправить calldata на L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

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

1 /**
2 * @inheritdoc IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }
Показать все

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

1 /**
2 * @dev Выполняет логику для депозитов, информируя контракт депонированного токена L2
3 * о депозите и вызывая обработчик для блокировки средств на L1 (например, transferFrom).
4 *
5 * @param _l1Token Адрес ERC20 на L1, который мы вносим
6 * @param _l2Token Адрес соответствующего ERC20 на L2
7 * @param _from Аккаунт для снятия депозита на L1
8 * @param _to Аккаунт для зачисления депозита на L2
9 * @param _amount Сумма ERC20 для внесения.
10 * @param _l2Gas Лимит газа, необходимый для завершения депозита на L2.
11 * @param _data Необязательные данные для пересылки на L2. Эти данные предоставляются
12 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
13 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
Показать все

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

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

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

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

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

1 // Конструируем calldata для _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Отправить calldata на L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Показать все

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

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Межсетевые функции *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Показать все

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

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

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

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

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

1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Выпустить событие о выводе средств.

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Показать все

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

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

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

1
2 // Когда вывод завершается на L1, Мост L1 переводит средства тому, кто их выводит
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * Временно - миграция ETH *
13 *****************************/
14
15 /**
16 * @dev Добавляет баланс ETH на аккаунт. Это предназначено для того, чтобы
17 * ETH можно было перенести со старого шлюза на новый.
18 * ПРИМЕЧАНИЕ: Это оставлено только для одного обновления, чтобы мы могли получить перенесенный ETH со
19 * старого контракта
20 */
21 function donateETH() external payable {}
22}
Показать все

Существовала более ранняя реализация Моста. Когда мы перешли от той реализации к этой, нам пришлось переместить все активы. Токены 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), который содержит функции и события, необходимые стандартному Мосту.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

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

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

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

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

1
2 function mint(address _to, uint256 _amount) external;
3
4 function burn(address _from, uint256 _amount) external;
5
6 event Mint(address indexed _account, uint256 _amount);
7 event Burn(address indexed _account, uint256 _amount);
8}

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

L2StandardERC20

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

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

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

1import "./IL2StandardERC20.sol";
2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {
4 address public l1Token;
5 address public l2Bridge;

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

1
2 /**
3 * @param _l2Bridge Адрес стандартного Моста L2.
4 * @param _l1Token Адрес соответствующего токена L1.
5 * @param _name Название ERC20.
6 * @param _symbol Символ ERC20.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Показать все

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

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Показать все

Именно так работает 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, поэтому это механизм проверки на вменяемость, а не механизм безопасности.

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
Показать все

Только Мосту L2 разрешено выпускать и сжигать активы.

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

Код Моста L2

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

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Interface Imports */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { 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).
1/* Library Imports */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Contract Imports */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev Стандартный Мост L2 — это контракт, который работает совместно со стандартным Мостом L1
12 * для обеспечения переходов ETH и ERC20 между L1 и L2.
13 * Этот контракт действует как эмитент для новых токенов, когда он получает информацию о депозитах в стандартный
14 * Мост L1.
15 * Этот контракт также действует как сжигатель токенов, предназначенных для вывода, информируя
16 * Мост L1 о необходимости высвобождения средств L1.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Ссылки на внешние контракты *
21 ********************************/
22
23 address public l1TokenBridge;
Показать все

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

1
2 /***************
3 * Конструктор *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Междоменный мессенджер, используемый этим контрактом.
8 * @param _l1TokenBridge Адрес Моста L1, развернутого в основной сети.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Вывод средств *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Показать все

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

1
2 /**
3 * @dev Выполняет логику вывода средств, сжигая токен и информируя
4 * шлюз токенов L1 о выводе.
5 * @param _l2Token Адрес токена L2, на котором инициирован вывод.
6 * @param _from Аккаунт для списания выводимых средств на L2.
7 * @param _to Аккаунт для зачисления выводимых средств на L1.
8 * @param _amount Сумма токена для вывода.
9 * @param _l1Gas Не используется, но включен для потенциальной прямой совместимости.
10 * @param _data Необязательные данные для пересылки на L1. Эти данные предоставляются
11 * исключительно для удобства внешних контрактов. Кроме ограничения максимальной
12 * длины, эти контракты не дают никаких гарантий относительно их содержимого.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // Когда инициируется вывод, мы сжигаем средства выводившего для предотвращения последующего использования на L2
23 // slither-disable-next-line reentrancy-events
24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Показать все

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

1
2 // Конструируем calldata для l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

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

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Отправить сообщение на Мост L1
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Межсетевая функция: Депозит *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Показать все

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

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

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

1 // Проверяем, что целевой токен соответствует требованиям, и
2 // убеждаемся, что депонированный токен на L1 соответствует представлению токена на L2
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Проверки на вменяемость:

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

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

  1. Выпустить токены
  2. Выпустить соответствующее событие
1 } else {
2 // Либо токен L2, на который делается депозит, не согласен с правильным адресом
3 // своего токена L1, либо не поддерживает правильный интерфейс.
4 // Это должно происходить только в случае вредоносного токена L2 или если пользователь как-то
5 // указал неправильный адрес токена L2 для депозита.
6 // В любом случае мы останавливаем процесс здесь и конструируем сообщение о выводе,
7 // чтобы пользователи в некоторых случаях могли вернуть свои средства.
8 // Невозможно полностью предотвратить вредоносные контракты токенов, но это ограничивает
9 // ошибки пользователя и смягчает некоторые формы вредоносного поведения контрактов.
Показать все

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

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // поменяли _to и _from местами, чтобы вернуть депозит отправителю
6 _from,
7 _amount,
8 _data
9 );
10
11 // Отправить сообщение на Мост L1
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Показать все

Заключение

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

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

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

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

Последнее обновление страницы: 22 октября 2025 г.

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