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

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

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

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

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

Саме так працює стандартний міст Optimism (відкривається в новій вкладці). У цій статті ми розглянемо вихідний код цього мосту, щоб побачити, як він працює, і вивчимо його як приклад добре написаного коду 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 правильним:
  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

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

// SPDX-License-Identifier: MIT

Більшість коду Optimism випущено під ліцензією MIT (відкривається в новій вкладці).

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. Ви можете переглянути список адрес токенів тут (відкривається в новій вкладці). Адреса з 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, яку дозволено витратити транзакції. До певної (високої) межі це безкоштовно (відкривається в новій вкладці), тому, якщо контракт ERC-20 не робить нічого дійсно дивного під час карбування, це не повинно бути проблемою. Ця функція обробляє поширений сценарій, коли користувач переводить активи через міст на ту саму адресу в іншому блокчейні.

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

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

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

IL1StandardBridge

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

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

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

CrossDomainEnabled

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

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

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

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

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

Міждоменний обмін повідомленнями доступний будь-якому контракту в блокчейні, де він працює (чи то Головна мережа 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() (відкривається в новій вкладці). Поки вона викликається в транзакції, ініційованій повідомленням, вона може надати цю інформацію.

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

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

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

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

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

  1. Події повторного входу (відкривається в новій вкладці)
  2. Безпечний повторний вхід (відкривається в новій вкладці)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

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

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

Вихідний код цього контракту знаходиться тут (відкривається в новій вкладці).

// 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";

Цей інтерфейс (відкривається в новій вкладці) дозволяє нам створювати повідомлення для керування стандартним мостом на l2.

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

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

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

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

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

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

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

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

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

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

Стандарт ERC-20 (відкривається в новій вкладці) підтримує два способи для контракту повідомити про помилку:

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

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

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


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

    address public l2TokenBridge;

Адреса L2StandardBridge.


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Повідомлення тут полягає у виклику функції finalizeDeposit (відкривається в новій вкладці) з такими параметрами:

ПараметрЗначенняЗначення
_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 викликати цю функцію (звісно, після того, як транзакція, що завершує повідомлення (відкривається в новій вкладці), буде надіслана на 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 через міст. Замість надійного мосту ми б по суті відтворили банкінг із частковим резервуванням (відкривається в новій вкладці). Якщо на l1 буде забагато токенів, деякі з цих токенів назавжди залишаться заблокованими в контракті мосту, оскільки немає способу вивільнити їх без спалювання токенів l2.

IL2StandardERC20

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

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

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

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

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

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

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 (відкривається в новій вкладці). Якщо вам не потрібна якась спеціальна логіка, вам слід використовувати саме її.

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

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

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

import "./IL2StandardERC20.sol";

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

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

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

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

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

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

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

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

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

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

// 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 (відкривається в новій вкладці) дуже схожий на еквівалент l1, який ми бачили вище. Є дві суттєві відмінності:

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

Відстежувати адресу мосту 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 — надіслати повідомлення, якому доведеться чекати період оскарження помилок, але це набагато краще для користувача, ніж назавжди втратити токени.

Висновок

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

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

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

Дивіться тут більше моїх робіт (відкривається в новій вкладці).