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

Огляд контракту стандартного мосту Optimism

Solidity
міст
рівень 2 (l2)
Середній рівень
Орі Померанц
30 березня 2022 р.
29 хвилин на читання

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

Щоб використовувати активи l1 в Optimism (або на будь-якому іншому l2), активи потрібно перевести через міст. Один зі способів досягти цього — користувачам заблокувати активи (ETH та токени ERC-20 є найпоширенішими) на l1 і отримати еквівалентні активи для використання на l2. Зрештою, той, хто їх отримає, може захотіти перевести їх назад на l1 через міст. Під час цього активи спалюються на l2, а потім повертаються користувачеві на l1.

Саме так працює стандартний міст Optimism (opens in a new tab). У цій статті ми розглянемо вихідний код цього мосту, щоб побачити, як він працює, і вивчимо його як приклад добре написаного коду Solidity.

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

Міст має два основні потоки:

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

Потік депозиту

Рівень 1

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

Рівень 2

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

Потік виведення

Рівень 2

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

Рівень 1

  1. Міст l1 перевіряє, чи є виклик finalizeETHWithdrawal або finalizeERC20Withdrawal легітимним:
    • Надійшов від механізму міждоменних повідомлень
    • Спочатку був від мосту на l2
  2. Міст l1 переказує відповідний актив (ETH або ERC-20) на відповідну адресу

Код рівня 1

Це код, який виконується на l1, у головній мережі Ethereum.

IL1ERC20Bridge

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

// SPDX-License-Identifier: MIT

Більшість коду Optimism випущено під ліцензією MIT (opens in a new tab).

pragma solidity >0.5.0 <0.9.0;

На момент написання статті останньою версією Solidity є 0.8.12. Поки не вийде версія 0.9.0, ми не знаємо, чи буде цей код сумісний з нею.

У термінології мосту Optimism депозит означає переказ з l1 на l2, а виведення означає переказ з l2 на l1.

        address indexed _l1Token,
        address indexed _l2Token,

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

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

До переказів можна додавати примітки, у такому разі вони додаються до подій, які про них повідомляють.

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

Той самий контракт мосту обробляє перекази в обох напрямках. У випадку мосту l1 це означає ініціалізацію депозитів та завершення виведення.

Ця функція насправді не потрібна, оскільки на l2 це попередньо розгорнутий контракт, тому він завжди знаходиться за адресою 0x4200000000000000000000000000000000000010. Вона тут для симетрії з мостом l2, оскільки адресу мосту l1 дізнатися не так просто.

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

Ця функція майже ідентична depositERC20, але вона дозволяє вам надіслати ERC-20 на іншу адресу.

Виведення (та інші повідомлення з l2 на l1) в Optimism — це двохетапний процес:

  1. Ініціююча транзакція на l2.
  2. Завершальна транзакція або транзакція затребування на l1. Ця транзакція має відбутися після завершення періоду оскарження помилок (opens in a new tab) для транзакції l2.

IL1StandardBridge

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

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

Ця подія майже ідентична версії ERC-20 (ERC20DepositInitiated), за винятком відсутності адрес токенів l1 та l2. Те саме стосується інших подій та функцій.

CrossDomainEnabled

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

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

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

Цей інтерфейс (opens in a new tab) вказує контракту, як надсилати повідомлення на інший рівень, використовуючи міждоменний месенджер. Цей міждоменний месенджер — це зовсім інша система, яка заслуговує на окрему статтю, яку я сподіваюся написати в майбутньому.

Єдиний параметр, який потрібно знати контракту, — це адреса міждоменного месенджера на цьому рівні. Цей параметр встановлюється один раз у конструкторі і ніколи не змінюється.

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

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

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


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

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

Нам потрібно переконатися, що отримане нами повідомлення надійшло від іншого мосту.

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

Нарешті, функція, яка надсилає повідомлення на інший рівень.

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

Слізер (opens in a new tab) — це статичний аналізатор, який Optimism запускає на кожному контракті для пошуку вразливостей та інших потенційних проблем. У цьому випадку наступний рядок викликає дві вразливості:

  1. Події повторного входу (opens in a new tab)
  2. Безпечний повторний вхід (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

У цьому випадку ми не турбуємося про повторний вхід, оскільки знаємо, що getCrossDomainMessenger() повертає надійну адресу, навіть якщо Слізер не має можливості про це дізнатися.

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

Вихідний код цього контракту знаходиться тут (opens in a new tab).

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

Інтерфейси можуть бути частиною інших контрактів, тому вони повинні підтримувати широкий спектр версій Solidity. Але сам міст — це наш контракт, і ми можемо бути суворими щодо того, яку версію Solidity він використовує.

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

IL1ERC20Bridge та IL1StandardBridge пояснюються вище.

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

Цей інтерфейс (opens in a new tab) дозволяє нам створювати повідомлення для керування стандартним мостом на l2.

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

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

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

Як пояснювалося вище, цей контракт використовується для міжрівневого обміну повідомленнями.

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

Lib_PredeployAddresses (opens in a new tab) містить адреси для контрактів l2, які завжди мають однакову адресу. Це включає стандартний міст на l2.

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

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

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

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

Стандарт ERC-20 (opens in a new tab) підтримує два способи для контракту повідомити про помилку:

  1. Скасувати
  2. Повернути false

Обробка обох випадків ускладнила б наш код, тому замість цього ми використовуємо SafeERC20 від ОупенЗеппелін (opens in a new tab), що гарантує, що всі помилки призводять до скасування (opens in a new tab).

Цей рядок вказує на використання обгортки SafeERC20 щоразу, коли ми використовуємо інтерфейс IERC20.


    /********************************
     * Посилання на зовнішні контракти *
     ********************************/

    address public l2TokenBridge;

Адреса L2StandardBridge.


    // Відображає токен l1 на токен l2 до балансу внесеного токена l1
    mapping(address => mapping(address => uint256)) public deposits;

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


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

    // Цей контракт знаходиться за проксі, тому параметри конструктора не використовуватимуться.
    constructor() CrossDomainEnabled(address(0)) {}

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

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

Цей тест Слізер (opens in a new tab) ідентифікує функції, які не викликаються з коду контракту і тому можуть бути оголошені як external замість public. Вартість газу для функцій external може бути нижчою, оскільки їм можна передавати параметри в даних виклику. Функції, оголошені як public, мають бути доступними зсередини контракту. Контракти не можуть змінювати власні дані виклику, тому параметри мають бути в пам'яті. Коли така функція викликається ззовні, необхідно скопіювати дані виклику в пам'ять, що коштує газу. У цьому випадку функція викликається лише один раз, тому неефективність не має для нас значення.

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

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

Зауважте, що ця функція не має жодного механізму, який обмежує те, хто може її викликати. Це означає, що теоретично зловмисник міг би зачекати, поки ми розгорнемо проксі-контракт і першу версію мосту, а потім здійснити випередження (opens in a new tab), щоб дістатися до функції initialize раніше за легітимного користувача. Але є два методи запобігти цьому:

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

Це два параметри, які потрібно знати мосту.

Саме тому нам знадобилися утиліти Address від ОупенЗеппелін.

Ця функція існує для цілей тестування. Зверніть увагу, що вона не з'являється у визначеннях інтерфейсу — вона не призначена для звичайного використання.

Ці дві функції є обгортками навколо _initiateETHDeposit, функції, яка обробляє фактичний депозит ETH.

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

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

Повідомлення тут полягає у виклику функції finalizeDeposit (opens in a new tab) з такими параметрами:

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

Надіслати повідомлення через міждоменний месенджер.

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

Викликати подію, щоб повідомити будь-який децентралізований застосунок (dapp), який прослуховує цей переказ.

Ці дві функції є обгортками навколо _initiateERC20Deposit, функції, яка обробляє фактичний депозит ERC-20.

Ця функція схожа на _initiateETHDeposit вище, з кількома важливими відмінностями. Перша відмінність полягає в тому, що ця функція отримує адреси токенів і суму для переказу як параметри. У випадку з ETH виклик мосту вже включає переказ активу на акаунт мосту (msg.value).

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

Перекази токенів ERC-20 відбуваються за іншим процесом, ніж ETH:

  1. Користувач (_from) надає мосту дозвіл на переказ відповідних токенів.
  2. Користувач викликає міст з адресою контракту токена, сумою тощо.
  3. Міст переказує токени (самому собі) як частину процесу депозиту.

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

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

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

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Переконайтеся, що це легітимне повідомлення, яке надходить від міждоменного месенджера і походить від мосту токенів l2. Ця функція використовується для виведення ETH з мосту, тому ми маємо переконатися, що її викликає лише авторизований абонент.

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

Спосіб переказу ETH полягає у виклику одержувача з кількістю Wei у msg.value.

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

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

Викликати подію про виведення.

Ця функція схожа на finalizeETHWithdrawal вище, з необхідними змінами для токенів ERC-20.

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

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

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

Токени ERC-20 на рівні 2

Щоб токен ERC-20 підходив для стандартного мосту, він має дозволяти стандартному мосту, і лише стандартному мосту, карбувати токен. Це необхідно, оскільки мости мають гарантувати, що кількість токенів, які циркулюють в Optimism, дорівнює кількості токенів, заблокованих у контракті мосту l1. Якщо на l2 буде забагато токенів, деякі користувачі не зможуть перевести свої активи назад на l1 через міст. Замість надійного мосту ми б по суті відтворили банкінг із частковим резервуванням (opens in a new tab). Якщо на l1 буде забагато токенів, деякі з цих токенів назавжди залишаться заблокованими в контракті мосту, оскільки немає способу вивільнити їх без спалювання токенів l2.

IL2StandardERC20

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

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

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

Стандартний інтерфейс ERC-20 (opens in a new tab) не включає функції mint та burn. Ці методи не вимагаються стандартом ERC-20 (opens in a new tab), який залишає невизначеними механізми створення та знищення токенів.

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

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

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

Ця функція надає адресу токена l1, який переведено через міст до цього контракту. Зауважте, що ми не маємо подібної функції у зворотному напрямку. Нам потрібно мати можливість перевести через міст будь-який токен l1, незалежно від того, чи планувалася підтримка l2 під час його реалізації, чи ні.


    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;

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

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

L2StandardERC20

Це наша реалізація інтерфейсу IL2StandardERC20 (opens in a new tab). Якщо вам не потрібна якась спеціальна логіка, вам слід використовувати саме її.

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

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

Контракт ERC-20 від ОупенЗеппелін (opens in a new tab). Optimism не вірить у винайдення велосипеда, особливо коли цей велосипед добре перевірений і має бути достатньо надійним для зберігання активів.

import "./IL2StandardERC20.sol";

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

Це два додаткові параметри конфігурації, які ми вимагаємо, а ERC-20 зазвичай ні.

Спочатку викликаємо конструктор для контракту, який ми успадковуємо (ERC20(_name, _symbol)), а потім встановлюємо власні змінні.

Саме так працює ERC-165 (opens in a new tab). Кожен інтерфейс — це низка підтримуваних функцій, і він ідентифікується як виключне АБО (opens in a new tab) селекторів функцій ABI (opens in a new tab) цих функцій.

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

Примітка: Ніщо не заважає шахрайському контракту надавати неправдиві відповіді на supportsInterface, тому це механізм базової перевірки, а не механізм безпеки.

Лише мосту l2 дозволено карбувати та спалювати активи.

_mint та _burn насправді визначені в контракті ERC-20 від ОупенЗеппелін. Цей контракт просто не відкриває їх назовні, оскільки умови для карбування та спалювання токенів такі ж різноманітні, як і кількість способів використання ERC-20.

Код мосту рівня 2

Це код, який запускає міст в Optimism. Вихідний код цього контракту знаходиться тут (opens in a new tab).

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

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

Інтерфейс IL2ERC20Bridge (opens in a new tab) дуже схожий на еквівалент l1, який ми бачили вище. Є дві суттєві відмінності:

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

Відстежувати адресу мосту l1. Зауважте, що на відміну від еквівалента l1, тут нам потрібна ця змінна. Адреса мосту l1 невідома заздалегідь.

Ці дві функції ініціюють виведення. Зауважте, що немає потреби вказувати адресу токена l1. Очікується, що токени l2 повідомлять нам адресу еквівалента l1.

Зверніть увагу, що ми не покладаємося на параметр _from, а на msg.sender, який набагато важче підробити (неможливо, наскільки мені відомо).


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

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

На l1 необхідно розрізняти ETH та ERC-20.

Ця функція викликається L1StandardBridge.

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Переконайтеся, що джерело повідомлення є легітимним. Це важливо, оскільки ця функція викликає _mint і може бути використана для видачі токенів, які не покриваються токенами, якими міст володіє на l1.

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

Базові перевірки:

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

Якщо базові перевірки пройдено, завершити депозит:

  1. Карбувати токени
  2. Викликати відповідну подію

Якщо користувач зробив помилку, яку можна виявити, використавши неправильну адресу токена l2, ми хочемо скасувати депозит і повернути токени на l1. Єдиний спосіб зробити це з l2 — надіслати повідомлення, якому доведеться чекати період оскарження помилок, але це набагато краще для користувача, ніж назавжди втратити токени.

Висновок

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

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

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

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