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

Покрокове керівництво для стандартного мостового контракту Optimism

мова програмування
міст
рівень 2
Середнячок
Ori Pomerantz
30 березня 2022 р.
29 читається за хвилину

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

Щоб використовувати активи 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, вкладник надає мосту дозвіл (allowance) на витрату суми, що вноситься
  2. Вкладник викликає міст L1 (depositERC20, depositERC20To, depositETH, або depositETHTo)
  3. Міст L1 отримує у володіння актив, що переказується через міст
    • ETH: актив передається вкладником у межах виклику
    • ERC-20: актив переказується мостом самому собі з використанням дозволу (allowance), наданого вкладником
  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 Mainnet.

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 deposit означає переказ з L1 на L2, а withdrawal — переказ з L2 на L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

У більшості випадків адреса ERC-20 на L1 не збігається з адресою еквівалентного ERC-20 на L2. Список адрес токенів можна переглянути тут (opens in a new tab). Адреса з chainId 1 знаходиться на L1 (Mainnet), а адреса з chainId 10 — на L2 (Optimism). Інші два значення chainId призначені для тестової мережі Kovan (42) та оптимістичної тестової мережі 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 Адреса L1 ERC20, який ми вносимо
4 * @param _l2Token Адреса відповідного L2 ERC20 на L1
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 Адреса L1 ERC20, який ми вносимо
4 * @param _l2Token Адреса відповідного L2 ERC20 на L1
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 * токена L1 ERC20.
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 * токена L1 ETH. Оскільки тільки 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/* Імпорт інтерфейсів */
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 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Показати все

Міждоменний обмін повідомленнями доступний будь-якому контракту в блокчейні, де він виконується (або в Ethereum Mainnet, або в Optimism). Але нам потрібно, щоб міст на кожній стороні довіряв лише тим повідомленням, які надходять від мосту з іншого боку.

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: контракт месенджера не автентифікований"
4 );

Можна довіряти лише повідомленням із відповідного міждоменного месенджера (messenger, як ви побачите нижче).

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: неправильний відправник міждоменного повідомлення"
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/* Імпорт інтерфейсів */
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/* Імпорт бібліотек */
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), який гарантує, що всі помилки призводять до revert (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)) {}

Щоб мати можливість оновити цей контракт без необхідності копіювати всі змінні в сховищі. Для цього ми використовуємо Proxy (opens in a new tab), контракт, який використовує delegatecall (opens in a new tab) для переадресації викликів до окремого контракту, чия адреса зберігається в контракті проксі (під час оновлення ви повідомляєте проксі змінити цю адресу). Коли ви використовуєте delegatecall, сховище залишається сховищем викликаючого контракту, тому значення всіх змінних стану контракту не змінюються.

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

1 /******************
2 * Ініціалізація *
3 ******************/
4
5 /**
6 * @param _l1messenger Адреса L1 Messenger, що використовується для міжмережевих комунікацій.
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), "Контракт уже ініціалізовано.");

Функція initialize має викликатися лише один раз. Якщо адреса міждоменного месенджера L1 або мосту токенів L2 змінюється, ми створюємо новий проксі та новий міст, який його викликає. Це навряд чи станеться, за винятком випадків оновлення всієї системи, що буває дуже рідко.

Зауважте, що ця функція не має механізму, який обмежує, хто може її викликати. Це означає, що теоретично зловмисник може почекати, доки ми розгорнемо проксі та першу версію мосту, а потім випередити (front-run) (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), "Обліковий запис не є 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 Deposited Token
3 * про депозит і викликаючи обробник для блокування коштів L1. (наприклад, transferFrom)
4 *
5 * @param _l1Token Адреса L1 ERC20, який ми вносимо
6 * @param _l2Token Адреса відповідного L2 ERC20 на L1
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 може бути кілька адрес, які відповідають одному токену L1 ERC-20, тому для відстеження депозитів недостатньо використовувати баланс токена L1 ERC-20 на мосту.

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");
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 до облікового запису. Це призначено для того, щоб дозволити міграцію ETH
17 * зі старого шлюзу на новий.
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, "Тільки міст L2 може карбувати та спалювати");
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/* Імпорт інтерфейсів */
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/* Імпорт бібліотек */
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/* Імпорт контрактів */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev Стандартний міст L2 — це контракт, який працює разом зі стандартним мостом L1, щоб
12 * забезпечити перекази ETH та ERC20 між L1 та L2.
13 * Цей контракт діє як мінтер для нових токенів, коли він отримує інформацію про депозити в стандартний міст
14 * L1.
15 * Цей контракт також діє як спалювач токенів, призначених для виведення, інформуючи міст L1
16 * про необхідність вивільнення коштів 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), які не чекають періоду оскарження і не вимагають доказу Merkle для завершення виведення.

Ці мости зазвичай працюють, маючи активи на L1, які вони надають негайно за невелику плату (часто меншу, ніж вартість газу для стандартного виведення через міст). Коли міст (або люди, які ним керують) очікує брак активів L1, він переказує достатню кількість активів з L2. Оскільки це дуже великі виведення, вартість виведення амортизується на велику суму та становить набагато менший відсоток.

Сподіваюся, ця стаття допомогла вам більше зрозуміти, як працює рівень 2 і як написати чіткий і безпечний код Solidity.

Більше моїх робіт дивіться тут (opens in a new tab).

Останні оновлення сторінки: 22 жовтня 2025 р.

Чи була ця інструкція корисною?