Přejít na hlavní obsah

Procházení kontraktu standardního přemostění Optimism

solidity
přemostění
vrstva 2
Středně pokročilý
Ori Pomerantz
30. března 2022
30 minuta čtení

Optimism (opens in a new tab) je optimistický rollup. Optimistické rollupy mohou zpracovávat transakce za mnohem nižší cenu než hlavní síť Ethereum (také známá jako první vrstva nebo L1), protože transakce zpracovává jen několik uzlů, nikoli každý uzel v síti. Všechna data jsou přitom zapsána na L1, takže vše lze prokázat a rekonstruovat se všemi zárukami integrity a dostupnosti hlavní sítě.

Aby bylo možné používat aktiva z L1 v síti Optimism (nebo v jakékoli jiné L2), je třeba je přemostit. Jedním ze způsobů, jak toho dosáhnout, je, že uživatelé uzamknou aktiva (nejčastěji se jedná o ETH a tokeny ERC-20) na L1 a obdrží ekvivalentní aktiva k použití na L2. Nakonec je může chtít ten, kdo je získá, přemostit zpět na L1. Přitom se aktiva na L2 spálí a poté se na L1 uvolní zpět uživateli.

Takto funguje standardní přemostění Optimism (opens in a new tab). V tomto článku si projdeme zdrojový kód tohoto přemostění, abychom viděli, jak funguje, a prostudujeme si jej jako příklad dobře napsaného kódu v Solidity.

Kontrolní toky

Přemostění má dva hlavní toky:

  • Vklad (z L1 na L2)
  • Výběr (z L2 na L1)

Tok vkladů

Vrstva 1

  1. Při vkladu tokenu ERC-20 udělí vkladatel přemostění povolení k útratě vkládané částky.
  2. Vkladatel zavolá přemostění na L1 (depositERC20, depositERC20To, depositETH nebo depositETHTo).
  3. Přemostění na L1 převezme přemostěné aktivum.
    • ETH: Aktivum je převedeno vkladatelem jako součást volání.
    • ERC-20: Aktivum je přemostěním převedeno samo sobě pomocí povolení poskytnutého vkladatelem.
  4. Přemostění na L1 použije mezidoménový mechanismus zpráv k zavolání finalizeDeposit na přemostění na L2.

Vrstva 2

  1. Přemostění na L2 ověří, že volání finalizeDeposit je legitimní:
    • Pochází z mezidoménového kontraktu zpráv.
    • Původně pocházelo z přemostění na L1.
  2. Přemostění na L2 zkontroluje, zda je kontrakt tokenu ERC-20 na L2 správný:
    • Kontrakt na L2 hlásí, že jeho protějšek na L1 je stejný jako ten, ze kterého tokeny na L1 pocházejí.
    • Kontrakt na L2 hlásí, že podporuje správné rozhraní (pomocí ERC-165 (opens in a new tab)).
  3. Pokud je kontrakt L2 správný, zavolejte jej, aby vyrazil příslušný počet tokenů na příslušnou adresu. Pokud ne, zahajte proces výběru, který uživateli umožní nárokovat tokeny na L1.

Tok výběrů

Vrstva 2

  1. Vybírající zavolá přemostění L2 (withdraw nebo withdrawTo).
  2. Přemostění L2 spálí příslušný počet tokenů patřících msg.sender.
  3. Přemostění L2 použije mezidoménový mechanismus zpráv k zavolání finalizeETHWithdrawal nebo finalizeERC20Withdrawal na přemostění na L1.

Vrstva 1

  1. Přemostění na L1 ověří, že volání finalizeETHWithdrawal nebo finalizeERC20Withdrawal je legitimní:
    • Pochází z mezidoménového mechanismu zpráv.
    • Původně pocházelo z přemostění na L2.
  2. Přemostění na L1 převede příslušné aktivum (ETH nebo ERC-20) na příslušnou adresu.

Kód na vrstvě 1

Toto je kód, který běží na L1, tedy hlavní síti Etherea.

IL1ERC20Bridge

Toto rozhraní je definováno zde (opens in a new tab). Obsahuje funkce a definice potřebné pro přemostění tokenů ERC-20.

// SPDX-License-Identifier: MIT

Většina kódu Optimism je vydána pod licencí MIT (opens in a new tab).

pragma solidity >0.5.0 <0.9.0;

V době psaní je poslední verze Solidity 0.8.12. Dokud nebude vydána verze 0.9.0, nevíme, zda s ní tento kód bude kompatibilní.

V terminologii přemostění Optimism znamená vklad převod z L1 na L2 a výběr převod z L2 na L1.

        address indexed _l1Token,
        address indexed _l2Token,

Ve většině případů se adresa ERC-20 na L1 nerovná adrese ekvivalentního ERC-20 na L2. Seznam adres tokenů naleznete zde (opens in a new tab). Adresa s chainId 1 je na L1 (hlavní síť) a adresa s chainId 10 je na L2 (Optimism). Další dvě hodnoty chainId jsou pro testovací síť Kovan (42) a testovací síť Optimistic Kovan (69).

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

K převodům je možné přidávat poznámky, které se v takovém případě přidají k událostem, jež je hlásí.

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

Stejný kontrakt přemostění zpracovává převody v obou směrech. V případě přemostění L1 to znamená inicializaci vkladů a finalizaci výběrů.

Tato funkce není ve skutečnosti potřeba, protože na L2 se jedná o předem nasazený kontrakt, takže se vždy nachází na adrese 0x4200000000000000000000000000000000000010. Je zde z důvodu symetrie s přemostěním L2, protože adresa přemostění L1 není triviálně zjistitelná.

Parametr _l2Gas je množství transakčního poplatku na L2, které může transakce utratit. Až do určitého (vysokého) limitu je to zdarma (opens in a new tab), takže pokud kontrakt ERC-20 nedělá při ražbě něco opravdu zvláštního, neměl by to být problém. Tato funkce se stará o běžný scénář, kdy uživatel přemosťuje aktiva na stejnou adresu na jiném blockchainu.

Tato funkce je téměř identická s depositERC20, ale umožňuje poslat ERC-20 na jinou adresu.

Výběry (a další zprávy z L2 do L1) v Optimism jsou dvoukrokový proces:

  1. Iniciační transakce na L2.
  2. Finalizační nebo nárokovací transakce na L1. Tato transakce se musí uskutečnit po skončení období pro napadení chyb (opens in a new tab) pro transakci na L2.

IL1StandardBridge

Toto rozhraní je definováno zde (opens in a new tab). Tento soubor obsahuje definice událostí a funkcí pro ETH. Tyto definice jsou velmi podobné těm, které jsou definovány v IL1ERC20Bridge výše pro ERC-20.

Rozhraní přemostění je rozděleno do dvou souborů, protože některé tokeny ERC-20 vyžadují vlastní zpracování a standardní přemostění je nemůže zpracovat. Tímto způsobem může vlastní přemostění, které takový token zpracovává, implementovat IL1ERC20Bridge a nemusí přemosťovat také ETH.

Tato událost je téměř totožná s verzí ERC-20 (ERC20DepositInitiated), pouze bez adres tokenů L1 a L2. Totéž platí pro ostatní události a funkce.

CrossDomainEnabled

Tento kontrakt (opens in a new tab) je zděděn oběma přemostěními (L1 a L2) pro posílání zpráv na druhou vrstvu.

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

/* Importy rozhraní */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Toto rozhraní (opens in a new tab) říká kontraktu, jak posílat zprávy na druhou vrstvu pomocí mezidoménového posílače zpráv. Tento mezidoménový posílač zpráv je zcela jiný systém a zaslouží si vlastní článek, který, doufám, v budoucnu napíšu.

Jediný parametr, který kontrakt potřebuje znát, je adresa mezidoménového posílače zpráv na této vrstvě. Tento parametr je nastaven jednou, v konstruktoru, a nikdy se nemění.

Mezidoménové zasílání zpráv je dostupné jakémukoli kontraktu na blockchainu, kde běží (buď na hlavní síti Etherea, nebo Optimism). Potřebujeme ale, aby přemostění na každé straně důvěřovalo pouze určitým zprávám, pokud pocházejí z přemostění na druhé straně.

        require(
            msg.sender == address(getCrossDomainMessenger()),
            "OVM_XCHAIN: kontrakt posílače zpráv není ověřený"
        );

Důvěřovat lze pouze zprávám z příslušného mezidoménového posílače zpráv (messenger, jak uvidíte níže).


        require(
            getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
            "OVM_XCHAIN: nesprávný odesílatel mezidoménové zprávy"
        );

Způsob, jakým mezidoménový posílač zpráv poskytuje adresu, která odeslala zprávu z druhé vrstvy, je funkce .xDomainMessageSender() (opens in a new tab). Pokud je volána v transakci, která byla zprávou iniciována, může tyto informace poskytnout.

Musíme se ujistit, že zpráva, kterou jsme obdrželi, pochází z druhého přemostění.

Tato funkce vrací mezidoménového posílače zpráv. Používáme funkci spíše než proměnnou messenger, aby kontrakty, které z ní dědí, mohly použít algoritmus k určení, který mezidoménový posílač zpráv použít.

A nakonec funkce, která posílá zprávu na druhou vrstvu.

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

Slither (opens in a new tab) je statický analyzátor, který Optimism spouští na každém kontraktu, aby hledal zranitelnosti a další potenciální problémy. V tomto případě následující řádek spouští dvě zranitelnosti:

  1. Události opětovného vstupu (reentrancy) (opens in a new tab)
  2. Nezávažné opětovné vstupy (reentrancy) (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

V tomto případě se o opětovné vstupy (reentrancy) nestaráme, protože víme, že getCrossDomainMessenger() vrací důvěryhodnou adresu, i když Slither to nemá jak vědět.

Kontrakt přemostění L1

Zdrojový kód tohoto kontraktu je zde (opens in a new tab).

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

Rozhraní mohou být součástí jiných kontraktů, takže musí podporovat širokou škálu verzí Solidity. Ale samotné přemostění je náš kontrakt a můžeme být přísní ohledně toho, jakou verzi Solidity používá.

/* Importy rozhraní */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge a IL1StandardBridge jsou vysvětleny výše.

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

Toto rozhraní (opens in a new tab) nám umožňuje vytvářet zprávy pro ovládání standardního přemostění na L2.

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

Toto rozhraní (opens in a new tab) nám umožňuje ovládat kontrakty ERC-20. Více si o tom můžete přečíst zde.

/* Importy knihoven */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Jak je vysvětleno výše, tento kontrakt se používá pro mezivrstvové zasílání zpráv.

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

Lib_PredeployAddresses (https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol (opens in a new tab)) má adresy kontraktů L2, které mají vždy stejnou adresu. To zahrnuje standardní přemostění na L2.

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

Nástroje pro adresy OpenZeppelin (opens in a new tab). Používá se k rozlišení mezi adresami kontraktů a adresami patřícími externě vlastněným účtům (EOA).

Všimněte si, že se nejedná o dokonalé řešení, protože neexistuje způsob, jak rozlišit mezi přímými voláními a voláními provedenými z konstruktoru kontraktu, ale alespoň nám to umožňuje identifikovat a zabránit některým běžným chybám uživatelů.

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

Standard ERC-20 (opens in a new tab) podporuje dva způsoby, jak může kontrakt nahlásit selhání:

  1. Zpětné vrácení (revert)
  2. Vrátit false

Zpracování obou případů by náš kód zkomplikovalo, takže místo toho používáme SafeERC20 od OpenZeppelin (opens in a new tab), který zajišťuje, že všechna selhání vedou k zpětnému vrácení (revert) (opens in a new tab).

Tento řádek určuje, že se má použít obal SafeERC20 pokaždé, když použijeme rozhraní IERC20.


    /********************************
     * Reference na externí kontrakty *
     ********************************/

    address public l2TokenBridge;

Adresa L2StandardBridge.


    // Mapuje token L1 na token L2 k zůstatku vloženého tokenu L1
    mapping(address => mapping(address => uint256)) public deposits;

Dvojité mapování (opens in a new tab) jako je toto, je způsob, jak definovat dvojrozměrné řídké pole (opens in a new tab). Hodnoty v této datové struktuře jsou identifikovány jako deposit[adresa tokenu L1][adresa tokenu L2]. Výchozí hodnota je nula. Do úložiště jsou zapsány pouze buňky, které jsou nastaveny na jinou hodnotu.


    /***************
     * Konstruktor *
     ***************/

    // Tento kontrakt se nachází za proxy, takže parametry konstruktoru nebudou použity.
    constructor() CrossDomainEnabled(address(0)) {}

Chceme mít možnost upgradovat tento kontrakt bez nutnosti kopírovat všechny proměnné v úložišti. K tomu používáme Proxy (opens in a new tab), kontrakt, který používá delegatecall (opens in a new tab) k přenosu volání na samostatný kontrakt, jehož adresa je uložena v proxy kontraktu (při upgradu řeknete proxy, aby změnila tuto adresu). Při použití delegatecall zůstává úložiště úložištěm volajícího kontraktu, takže hodnoty všech proměnných stavu kontraktu zůstávají nedotčeny.

Jedním z důsledků tohoto vzoru je, že úložiště kontraktu, který je volaný pomocí delegatecall, se nepoužívá, a proto hodnoty konstruktoru, které mu jsou předány, nejsou důležité. To je důvod, proč můžeme konstruktoru CrossDomainEnabled poskytnout nesmyslnou hodnotu. Je to také důvod, proč je níže uvedená inicializace oddělena od konstruktoru.

Tento test Slither (opens in a new tab) identifikuje funkce, které nejsou volány z kódu kontraktu a mohly by být proto deklarovány jako external místo public. Náklady na transakční poplatky u funkcí external mohou být nižší, protože jim mohou být parametry poskytnuty v calldata. Funkce deklarované jako public musí být přístupné zevnitř kontraktu. Kontrakty nemohou měnit svá vlastní calldata, takže parametry musí být v paměti. Když je taková funkce volána externě, je nutné zkopírovat calldata do paměti, což stojí transakční poplatky. V tomto případě je funkce volána pouze jednou, takže neefektivita pro nás není důležitá.

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

Funkce initialize by měla být volána pouze jednou. Pokud se adresa mezidoménového posílače zpráv na L1 nebo přemostění tokenu na L2 změní, vytvoříme nové proxy a nové přemostění, které ho volá. Je nepravděpodobné, že by k tomu došlo, s výjimkou upgradu celého systému, což je velmi vzácná událost.

Všimněte si, že tato funkce nemá žádný mechanismus, který by omezoval, kdo ji může volat. To znamená, že teoreticky by útočník mohl počkat, až nasadíme proxy a první verzi přemostění, a poté provést front-running (opens in a new tab) a dostat se k funkci initialize dříve, než to udělá legitimní uživatel. Existují však dvě metody, jak tomu zabránit:

  1. Pokud kontrakty nejsou nasazeny přímo pomocí EOA, ale v transakci, která nechá vytvořit jiný kontrakt (opens in a new tab), celý proces může být atomický a dokončen dříve, než je provedena jakákoli jiná transakce.
  2. Pokud legitimní volání initialize selže, je vždy možné ignorovat nově vytvořené proxy a přemostění a vytvořit nové.
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

Toto jsou dva parametry, které přemostění potřebuje znát.

To je důvod, proč jsme potřebovali nástroje Address od OpenZeppelin.

Tato funkce existuje pro testovací účely. Všimněte si, že se neobjevuje v definicích rozhraní – není určena pro běžné použití.

Tyto dvě funkce jsou obaly kolem _initiateETHDeposit, funkce, která zpracovává skutečný vklad ETH.

Způsob, jakým fungují mezidoménové zprávy, je ten, že cílový kontrakt je volán se zprávou jako jeho calldata. Kontrakty v Solidity vždy interpretují svá calldata v souladu s specifikacemi ABI (opens in a new tab). Funkce Solidity abi.encodeWithSelector (opens in a new tab) vytváří tato calldata.

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

Zpráva zde znamená volání funkce finalizeDeposit (opens in a new tab) s těmito parametry:

ParametrHodnotaVýznam
_l1Tokenaddress(0)Speciální hodnota, která na L1 představuje ETH (který není tokenem ERC-20).
_l2TokenLib_PredeployAddresses.OVM_ETHKontrakt na L2, který spravuje ETH v síti Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (tento kontrakt je určen pouze pro interní použití v síti Optimism).
_from_fromAdresa na L1, která odesílá ETH.
_to_toAdresa na L2, která přijímá ETH.
částkamsg.valueOdeslaná částka ve wei (která již byla odeslána do přemostění).
_data_dataDalší data, která se připojí k vkladu.
        // Odeslání calldata na L2
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Odeslání zprávy prostřednictvím mezidoménového posílače zpráv.

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

Vyslat událost, aby se o tomto převodu informovala jakákoli decentralizovaná aplikace, která naslouchá.

Tyto dvě funkce jsou obaly kolem _initiateERC20Deposit, funkce, která zpracovává skutečný vklad ERC-20.

Tato funkce je podobná výše uvedené funkci _initiateETHDeposit, s několika důležitými rozdíly. Prvním rozdílem je, že tato funkce přijímá adresy tokenů a částku k převodu jako parametry. V případě ETH volání přemostění již zahrnuje převod aktiv na účet přemostění (msg.value).

        // Když je vklad iniciován na L1, přemostění L1 převede prostředky na sebe pro budoucí
        // výběry. safeTransferFrom také kontroluje, zda má kontrakt kód, takže toto selže, pokud
        // _from je EOA nebo address(0).
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

Převody tokenů ERC-20 se řídí jiným procesem než ETH:

  1. Uživatel (_from) udělí přemostění povolení k převodu příslušných tokenů.
  2. Uživatel zavolá přemostění s adresou kontraktu tokenu, částkou atd.
  3. Přemostění převede tokeny (na sebe) jako součást procesu vkladu.

První krok se může uskutečnit v samostatné transakci od posledních dvou. Front-running však není problém, protože obě funkce, které volají _initiateERC20Deposit (depositERC20 a depositERC20To), volají tuto funkci pouze s msg.sender jako parametrem _from.

Přidat vloženou částku tokenů do datové struktury deposits. Může existovat více adres na L2, které odpovídají stejnému tokenu ERC-20 na L1, takže pro sledování vkladů nestačí použít zůstatek přemostění tokenu ERC-20 na L1.

Přemostění L2 odešle zprávu mezidoménovému posílači zpráv L2, což způsobí, že mezidoménový posílač zpráv L1 zavolá tuto funkci (samozřejmě až po odeslání transakce, která zprávu finalizuje (opens in a new tab) na L1).

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Ujistěte se, že se jedná o legitimní zprávu, která pochází od mezidoménového posílače zpráv a pochází z přemostění tokenu L2. Tato funkce se používá k výběru ETH z přemostění, takže se musíme ujistit, že ji volá pouze oprávněný volající.

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

Způsobem převodu ETH je zavolat příjemce s částkou wei v msg.value.

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

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

Vyslat událost o výběru.

Tato funkce je podobná výše uvedené funkci finalizeETHWithdrawal s nezbytnými změnami pro tokeny ERC-20.

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

Aktualizovat datovou strukturu deposits.

Existovala starší implementace přemostění. Když jsme přešli z této implementace na tuto, museli jsme přesunout všechna aktiva. Tokeny ERC-20 se dají jednoduše přesunout. Abyste však mohli převést ETH do kontraktu, potřebujete souhlas tohoto kontraktu, což nám poskytuje donateETH.

Tokeny ERC-20 na L2

Aby token ERC-20 vyhovoval standardnímu přemostění, musí umožnit standardnímu přemostění, a pouze standardnímu přemostění, razit tokeny. To je nutné, protože přemostění musí zajistit, aby se počet tokenů v oběhu v síti Optimism rovnal počtu tokenů uzamčených v kontraktu přemostění na L1. Pokud by na L2 bylo příliš mnoho tokenů, někteří uživatelé by nemohli svá aktiva přemostit zpět na L1. Místo důvěryhodného přemostění bychom v podstatě znovu vytvořili bankovnictví s částečnými rezervami (opens in a new tab). Pokud je na L1 příliš mnoho tokenů, některé z nich by zůstaly navždy uzamčeny v kontraktu přemostění, protože neexistuje způsob, jak je uvolnit bez spálení tokenů na L2.

IL2StandardERC20

Každý token ERC-20 na L2, který používá standardní přemostění, musí poskytovat toto rozhraní (opens in a new tab), které má funkce a události, jež standardní přemostění potřebuje.

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

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

Standardní rozhraní ERC-20 (opens in a new tab) nezahrnuje funkce mint a burn. Tyto metody nejsou vyžadovány standardem ERC-20 (opens in a new tab), který ponechává mechanismy pro vytváření a ničení tokenů nespecifikované.

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

Rozhraní ERC-165 (opens in a new tab) se používá ke specifikaci funkcí, které kontrakt poskytuje. Standard si můžete přečíst zde (opens in a new tab).

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

Tato funkce poskytuje adresu tokenu L1, který je přemostěn do tohoto kontraktu. Všimněte si, že podobnou funkci v opačném směru nemáme. Musíme být schopni přemostit jakýkoli token na L1 bez ohledu na to, zda podpora na L2 byla plánována při jeho implementaci či nikoli.


    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);
}

Funkce a události pro ražbu (vytvoření) a pálení (zničení) tokenů. Přemostění by mělo být jedinou entitou, která může tyto funkce spouštět, aby se zajistil správný počet tokenů (rovný počtu tokenů uzamčených na L1).

L2StandardERC20

Toto je naše implementace rozhraní IL2StandardERC20 (opens in a new tab). Pokud nepotřebujete žádnou vlastní logiku, měli byste použít tuto.

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

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

Kontrakt ERC-20 od OpenZeppelin (opens in a new tab). Optimism nevěří v znovuobjevování kola, zejména když je kolo dobře auditováno a musí být dostatečně důvěryhodné pro držení aktiv.

import "./IL2StandardERC20.sol";

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

Toto jsou dva další konfigurační parametry, které vyžadujeme a které ERC-20 normálně nemá.

Nejprve zavoláme konstruktor kontraktu, ze kterého dědíme (ERC20(_name, _symbol)), a poté nastavíme vlastní proměnné.

Takto funguje ERC-165 (opens in a new tab). Každé rozhraní je souborem podporovaných funkcí a je identifikováno jako exkluzivní nebo (opens in a new tab) selektorů funkcí ABI (opens in a new tab) těchto funkcí.

Přemostění na L2 používá ERC-165 jako kontrolu správnosti, aby se ujistilo, že kontrakt ERC-20, do kterého posílá aktiva, je IL2StandardERC20.

Poznámka: Nic nebrání tomu, aby podvodný kontrakt poskytoval falešné odpovědi na supportsInterface, takže se jedná o mechanismus kontroly správnosti, nikoli o bezpečnostní mechanismus.

Pouze přemostění L2 smí razit a pálit aktiva.

_mint a _burn jsou ve skutečnosti definovány v kontraktu ERC-20 OpenZeppelin. Tento kontrakt je pouze nevystavuje externě, protože podmínky pro ražbu a pálení tokenů jsou tak rozmanité jako počet způsobů použití ERC-20.

Kód přemostění L2

Toto je kód, který spouští přemostění v síti Optimism. Zdrojový kód tohoto kontraktu je zde (opens in a new tab).

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

/* Importy rozhraní */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

Rozhraní IL2ERC20Bridge (opens in a new tab) je velmi podobné ekvivalentu L1, který jsme viděli výše. Existují dva významné rozdíly:

  1. Na L1 iniciujete vklady a finalizujete výběry. Zde iniciujete výběry a finalizujete vklady.
  2. Na L1 je nutné rozlišovat mezi ETH a tokeny ERC-20. Na L2 můžeme použít stejné funkce pro obojí, protože interně jsou zůstatky ETH v síti Optimism spravovány jako token ERC-20 s adresou 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).

Sledovat adresu přemostění L1. Všimněte si, že na rozdíl od ekvivalentu na L1 zde tuto proměnnou potřebujeme. Adresa přemostění L1 není známa předem.

Tyto dvě funkce iniciují výběry. Všimněte si, že není třeba specifikovat adresu tokenu L1. Očekává se, že tokeny na L2 nám sdělí adresu svého ekvivalentu na L1.

Všimněte si, že se nespoléháme na parametr _from, ale na msg.sender, který je mnohem těžší zfalšovat (pokud vím, nemožné).


        // Sestavení calldata pro l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
        // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

Na L1 je nutné rozlišovat mezi ETH a ERC-20.

Tuto funkci volá L1StandardBridge.

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Ujistěte se, že zdroj zprávy je legitimní. To je důležité, protože tato funkce volá _mint a mohla by být použita k poskytnutí tokenů, které nejsou kryty tokeny, jež přemostění vlastní na L1.

        // Zkontrolujte, zda je cílový token vyhovující a
        // ověřte, že vložený token na L1 odpovídá reprezentaci vloženého tokenu na L2 zde
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

Kontroly správnosti:

  1. Je podporováno správné rozhraní
  2. Adresa L1 kontraktu ERC-20 na L2 odpovídá zdroji L1 tokenů.
        ) {
            // Když je vklad finalizován, připíšeme na účet na L2 stejnou částku
            // tokenů.
            // 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);

Pokud kontroly správnosti projdou, dokončete vklad:

  1. Razit tokeny
  2. Vyslat příslušnou událost

Pokud uživatel udělal zjistitelnou chybu použitím špatné adresy tokenu na L2, chceme vklad zrušit a vrátit tokeny na L1. Jediný způsob, jak to můžeme udělat z L2, je poslat zprávu, která bude muset počkat na období pro napadení chyby, ale to je pro uživatele mnohem lepší než trvalá ztráta tokenů.

Závěr

Standardní přemostění je nejflexibilnějším mechanismem pro převody aktiv. Protože je však tak obecný, není vždy nejjednodušším mechanismem k použití. Zejména pro výběry většina uživatelů dává přednost použití přemostění třetích stran (opens in a new tab), která nečekají na období pro napadení a nevyžadují Merkleho důkaz k dokončení výběru.

Tato přemostění obvykle fungují tak, že mají aktiva na L1, která okamžitě poskytnou za malý poplatek (často nižší než náklady na transakční poplatky za výběr standardním přemostěním). Když přemostění (nebo lidé, kteří ho provozují) předpokládá, že bude mít málo aktiv na L1, převede dostatečná aktiva z L2. Protože se jedná o velmi velké výběry, náklady na výběr se amortizují na velkou částku a představují mnohem menší procento.

Doufejme, že vám tento článek pomohl lépe pochopit, jak funguje druhá vrstva a jak psát kód v Solidity, který je jasný a bezpečný.

Více z mé práce najdete zde (opens in a new tab).

Poslední aktualizace stránky: 3. dubna 2026

Byl tento návod užitečný?