Přejít na hlavní obsah

Průvodce kontraktem standardního mostu Optimism

Solidity
most
vrstva 2
Středně pokročilý
Ori Pomerantz
30. března 2022
30 minut čtení

Optimism (opens in a new tab) je optimistický rollup. Optimistické rollupy mohou zpracovávat transakce za mnohem nižší cenu než Ethereum Mainnet (známý také jako vrstva 1 nebo l1), protože transakce zpracovává pouze několik uzlů, a ne každý uzel v síti. Zároveň se všechna data zapisují na l1, takže vše lze dokázat a zrekonstruovat se všemi zárukami integrity a dostupnosti Mainnetu.

Aby bylo možné používat aktiva z l1 na Optimism (nebo jakékoli jiné l2), musí být aktiva přemostěna. Jedním ze způsobů, jak toho dosáhnout, je, že uživatelé uzamknou aktiva (nejčastěji ETH a ERC-20 tokeny) na l1 a obdrží ekvivalentní aktiva pro použití na l2. Nakonec ten, komu zůstanou, je možná bude chtít přemostit zpět na l1. Při tom jsou aktiva na l2 spálena a poté uvolněna zpět uživateli na l1.

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

Toky řízení

Most má dva hlavní toky:

  • Vklad (z l1 na l2)
  • Výběr (z l2 na l1)

Tok vkladu

Vrstva 1

  1. Pokud vkládáte ERC-20, vkladatel poskytne mostu povolený limit k útratě vkládané částky.
  2. Vkladatel zavolá most na l1 (depositERC20, depositERC20To, depositETH nebo depositETHTo).
  3. Most na l1 převezme vlastnictví přemostěného aktiva.
    • ETH: Aktivum je převedeno vkladatelem jako součást volání.
    • ERC-20: Aktivum je převedeno mostem na sebe sama pomocí povoleného limitu poskytnutého vkladatelem.
  4. Most na l1 použije mechanismus zpráv napříč doménami (cross-domain message) k zavolání finalizeDeposit na mostu l2.

Vrstva 2

  1. Most na l2 ověří, že volání finalizeDeposit je legitimní:
    • Přišlo z kontraktu pro zprávy napříč doménami.
    • Původně pocházelo z mostu na l1.
  2. Most na l2 zkontroluje, zda je kontrakt ERC-20 tokenu na l2 ten správný:
    • Kontrakt na l2 hlásí, že jeho protějšek na l1 je stejný jako ten, ze kterého tokeny přišly na l1.
    • Kontrakt na l2 hlásí, že podporuje správné rozhraní (pomocí ERC-165 (opens in a new tab)).
  3. Pokud je kontrakt na l2 správný, zavolá jej, aby vyrazil příslušný počet tokenů na příslušnou adresu. Pokud ne, zahájí proces výběru, aby uživateli umožnil uplatnit nárok na tokeny na l1.

Tok výběru

Vrstva 2

  1. Vybírající zavolá most na l2 (withdraw nebo withdrawTo).
  2. Most na l2 spálí příslušný počet tokenů patřících msg.sender.
  3. Most na l2 použije mechanismus zpráv napříč doménami k zavolání finalizeETHWithdrawal nebo finalizeERC20Withdrawal na mostu l1.

Vrstva 1

  1. Most na l1 ověří, že volání finalizeETHWithdrawal nebo finalizeERC20Withdrawal je legitimní:
    • Přišlo z mechanismu zpráv napříč doménami.
    • Původně pocházelo z mostu na l2.
  2. Most na l1 převede příslušné aktivum (ETH nebo ERC-20) na příslušnou adresu.

Kód vrstvy 1

Toto je kód, který běží na l1, Ethereum Mainnetu.

IL1ERC20Bridge

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

// 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í tohoto článku je nejnovější verze Solidity 0.8.12. Dokud nebude vydána verze 0.9.0, nevíme, zda s ní bude tento kód kompatibilní, nebo ne.

V terminologii mostu Optimism znamená vklad (deposit) převod z l1 na l2 a výběr (withdrawal) znamená převod z l2 na l1.

        address indexed _l1Token,
        address indexed _l2Token,

Ve většině případů není adresa ERC-20 na l1 stejná jako adresa ekvivalentního ERC-20 na l2. Seznam adres tokenů si můžete prohlédnout zde (opens in a new tab). Adresa s chainId 1 je na l1 (Mainnet) 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, v takovém případě jsou přidány k událostem, které je hlásí.

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

Stejný kontrakt mostu zpracovává převody v obou směrech. V případě mostu na 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 je vždy na adrese 0x4200000000000000000000000000000000000010. Je zde kvůli symetrii s mostem na l2, protože zjistit adresu mostu na l1 není triviální.

Parametr _l2Gas je množství gasu 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žení 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řemostí aktiva na stejnou adresu na jiném blockchainu.

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

Výběry (a další zprávy z l2 na l1) v Optimism jsou dvoukrokový proces:

  1. Inicializační transakce na l2.
  2. Finalizační transakce nebo transakce uplatňující nárok na l1. Tato transakce musí proběhnout po skončení období pro zpochybnění chyby (fault challenge period) (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í mostu je rozděleno do dvou souborů, protože některé ERC-20 tokeny vyžadují vlastní zpracování a nelze je obsloužit standardním mostem. Tímto způsobem může vlastní most, který takový token zpracovává, implementovat IL1ERC20Bridge a nemusí zároveň přemosťovat ETH.

Tato událost je téměř identická s verzí pro ERC-20 (ERC20DepositInitiated), s výjimkou adres tokenů na l1 a l2. Totéž platí pro ostatní události a funkce.

CrossDomainEnabled

Tento kontrakt (opens in a new tab) dědí oba mosty (l1 a l2) pro odesílání zpráv do druhé vrstvy.

// 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 odesílat zprávy do druhé vrstvy pomocí cross domain messengeru (nástroje pro zprávy napříč doménami). Tento cross domain messenger je zcela jiný systém a zaslouží si vlastní článek, který snad v budoucnu napíšu.

Jediný parametr, který kontrakt potřebuje znát, je adresa cross domain messengeru na této vrstvě. Tento parametr se nastavuje jednou, v konstruktoru, a nikdy se nemění.

Zasílání zpráv napříč doménami je přístupné jakémukoli kontraktu na blockchainu, kde běží (buď Ethereum Mainnet, nebo Optimism). Potřebujeme však, aby most na každé straně důvěřoval pouze určitým zprávám, pokud pocházejí z mostu na druhé straně.

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

Důvěřovat lze pouze zprávám z příslušného cross domain messengeru (messenger, jak vidíte níže).


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

Způsob, jakým cross domain messenger poskytuje adresu, která odeslala zprávu do druhé vrstvy, je funkce .xDomainMessageSender() (opens in a new tab). Dokud je volána v transakci, která byla iniciována zprávou, může tuto informaci poskytnout.

Musíme se ujistit, že zpráva, kterou jsme obdrželi, přišla z druhého mostu.

Tato funkce vrací cross domain messenger. Používáme funkci spíše než proměnnou messenger, abychom umožnili kontraktům, které z tohoto dědí, použít algoritmus k určení, který cross domain messenger se má použít.

A nakonec funkce, která odesílá zprávu do druhé vrstvy.

    ) 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 reentrance (opens in a new tab)
  2. Neškodná reentrance (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

V tomto případě se reentrance neobáváme, víme, že getCrossDomainMessenger() vrací důvěryhodnou adresu, i když Slither nemá jak to zjistit.

Kontrakt mostu na 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ý most 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 mostu 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 bylo vysvětleno výše, tento kontrakt se používá pro zasílání zpráv mezi vrstvami.

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

Lib_PredeployAddresses (opens in a new tab) obsahuje adresy pro kontrakty na l2, které mají vždy stejnou adresu. To zahrnuje standardní most na l2.

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

Nástroje pro adresy od 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).

Vezměte na vědomí, že to není dokonalé řešení, protože neexistuje způsob, jak rozlišit mezi přímými voláními a voláními z konstruktoru kontraktu, ale alespoň nám to umožňuje identifikovat a předcházet 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. Zvrátit (revert)
  2. Vrátit false

Ošetření obou případů by náš kód zkomplikovalo, takže místo toho používáme SafeERC20 od OpenZeppelin (opens in a new tab), což zajišťuje, že všechna selhání vedou ke zvrácení (opens in a new tab).

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


    /********************************
     * Odkazy na externí kontrakty *
     ********************************/

    address public l2TokenBridge;

Adresa L2StandardBridge.


    // Mapuje token na vrstvě 1 na token na vrstvě 2 na zůstatek vloženého tokenu na vrstvě 1
    mapping(address => mapping(address => uint256)) public deposits;

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


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

    // Tento kontrakt běží za proxy, takže parametry konstruktoru zůstanou nevyužity.
    constructor() CrossDomainEnabled(address(0)) {}

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

Jedním z důsledků tohoto vzoru je, že úložiště kontraktu, který je volán pomocí delegatecall, se nepoužívá, a proto na hodnotách předaných jeho konstruktoru nezáleží. To je důvod, proč můžeme konstruktoru CrossDomainEnabled poskytnout nesmyslnou hodnotu. Je to také důvod, proč je inicializace níže oddělena od konstruktoru.

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

    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 změní adresa cross domain messengeru na l1 nebo mostu tokenů na l2, vytvoříme nový proxy kontrakt a nový most, který jej 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 mostu, a pak použít předbíhání (front-running) (opens in a new tab), aby se dostal k funkci initialize dříve než legitimní uživatel. Existují však dvě metody, jak tomu zabránit:

  1. Pokud kontrakty nejsou nasazeny přímo pomocí EOA, ale v transakci, ve které je vytvoří jiný kontrakt (opens in a new tab), může být celý proces atomický a dokončit se 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 most a vytvořit nové.
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

Toto jsou dva parametry, které most potřebuje znát.

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

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

Tyto dvě funkce jsou obaly (wrappers) kolem _initiateETHDeposit, funkce, která zpracovává samotný vklad ETH.

Zprávy napříč doménami fungují tak, že cílový kontrakt je volán se zprávou jako svými daty volání (calldata). Kontrakty v Solidity vždy interpretují svá data volání v souladu se specifikacemi ABI (opens in a new tab). Funkce Solidity abi.encodeWithSelector (opens in a new tab) tato data volání vytváří.

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

Zpráva zde znamená zavolat funkci finalizeDeposit (opens in a new tab) s těmito parametry:

ParametrHodnotaVýznam
_l1Tokenaddress(0)Speciální hodnota zastupující ETH (což není ERC-20 token) na l1
_l2TokenLib_PredeployAddresses.OVM_ETHKontrakt na l2, který spravuje ETH na Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (tento kontrakt je pouze pro interní použití Optimism)
_from_fromAdresa na l1, která odesílá ETH
_to_toAdresa na l2, která přijímá ETH
amountmsg.valueMnožství odeslaných Wei (které již bylo odesláno do mostu)
_data_dataDalší data k připojení ke vkladu
        // Odešle data volání na vrstvu 2
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Odeslat zprávu přes cross domain messenger.

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

Vyvolat událost, která informuje jakoukoli decentralizovanou aplikaci (dapp), která naslouchá tomuto převodu.

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

Tato funkce je podobná _initiateETHDeposit výše, 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í mostu již zahrnuje převod aktiva na účet mostu (msg.value).

        // Když je na vrstvě 1 zahájen vklad, most na vrstvě 1 převede prostředky sám sobě 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 ERC-20 tokenů probíhají jiným procesem než ETH:

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

První krok může proběhnout v samostatné transakci odděleně od posledních dvou. Předbíhání však není problém, protože dvě funkce, které volají _initiateERC20Deposit (depositERC20 a depositERC20To), volají tuto funkci pouze s msg.sender jako parametrem _from.

Přidat vložené množství tokenů do datové struktury deposits. Na l2 by mohlo být více adres, které odpovídají stejnému ERC-20 tokenu na l1, takže ke sledování vkladů nestačí použít zůstatek ERC-20 tokenu na l1 v mostu.

Most na l2 odešle zprávu do cross domain messengeru na l2, což způsobí, že cross domain messenger na l1 zavolá tuto funkci (samozřejmě jakmile je na l1 odeslána transakce, která zprávu finalizuje (opens in a new tab)).

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Ujistěte se, že se jedná o legitimní zprávu, která pochází z cross domain messengeru a má původ v mostu tokenů na l2. Tato funkce se používá k výběru ETH z mostu, takže se musíme ujistit, že ji volá pouze autorizovaný volající.

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

Způsob, jak převést ETH, je zavolat příjemce s množstvím Wei v msg.value.

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

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

Vyvolat událost o výběru.

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

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

Aktualizovat datovou strukturu deposits.

Existovala dřívější implementace mostu. Když jsme přešli z této implementace na současnou, museli jsme přesunout všechna aktiva. ERC-20 tokeny lze jednoduše přesunout. K převodu ETH do kontraktu však potřebujete schválení tohoto kontraktu, což nám poskytuje donateETH.

ERC-20 tokeny na l2

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

IL2StandardERC20

Každý ERC-20 token na l2, který používá standardní most, musí poskytovat toto rozhraní (opens in a new tab), které obsahuje funkce a události, jež standardní most 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á k určení, jaké funkce 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 na l1, který je přemostěn do tohoto kontraktu. Všimněte si, že nemáme podobnou funkci v opačném směru. Musíme být schopni přemostit jakýkoli token z l1, bez ohledu na to, zda byla podpora l2 plánována při jeho implementaci, nebo ne.


    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žení (vytváření) a pálení (ničení) tokenů. Most by měl být jedinou entitou, která může tyto funkce spouštět, aby se zajistilo, že počet tokenů je správný (rovná se počtu tokenů uzamčených na l1).

L2StandardERC20

Toto je naše implementace rozhraní IL2StandardERC20 (opens in a new tab). Pokud nepotřebujete nějakou 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ěří ve znovuobjevování kola, zvláště když je kolo dobře auditováno a musí být dostatečně důvěryhodné, aby drželo aktiva.

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ě nevyžaduje.

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

Tímto způsobem funguje ERC-165 (opens in a new tab). Každé rozhraní je množinou podporovaných funkcí a je identifikováno jako exkluzivní disjunkce (XOR) (opens in a new tab) selektorů funkcí ABI (opens in a new tab) těchto funkcí.

Most na l2 používá ERC-165 jako kontrolu správnosti (sanity check), aby se ujistil, že kontrakt ERC-20, do kterého odesílá aktiva, je IL2StandardERC20.

Poznámka: Nic nebrání škodlivému kontraktu poskytovat falešné odpovědi na supportsInterface, takže se jedná o mechanismus kontroly správnosti, nikoli o bezpečnostní mechanismus.

Pouze most na l2 má povoleno razit a pálit aktiva.

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

Kód mostu na l2

Toto je kód, který provozuje most na 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 na l1, který jsme viděli výše. Jsou zde dva významné rozdíly:

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

Udržovat přehled o adrese mostu na l1. Všimněte si, že na rozdíl od ekvivalentu na l1 zde tuto proměnnou potřebujeme. Adresa mostu na l1 není předem známa.

Tyto dvě funkce inicializují výběry. Všimněte si, že není nutné specifikovat adresu tokenu na l1. Očekává se, že tokeny na l2 nám sdělí adresu 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, je to nemožné).


        // Sestaví data volání 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.

Tato funkce je volána pomocí 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 vydání tokenů, které nejsou kryty tokeny, jež most vlastní na l1.

        // Zkontroluje, zda je cílový token kompatibilní a
        // ověří, zda vložený token na vrstvě 1 odpovídá zdejší reprezentaci vloženého tokenu na vrstvě 2
        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 tokenů na l1
        ) {
            // Když je vklad dokončen, připíšeme na účet na vrstvě 2 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, finalizujte vklad:

  1. Vyrazit tokeny
  2. Vyvolat příslušnou událost

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

Závěr

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

Tyto mosty obvykle fungují tak, že mají aktiva na l1, která poskytují okamžitě za malý poplatek (často nižší než náklady na gas pro výběr ze standardního mostu). Když most (nebo lidé, kteří jej provozují) předpokládá nedostatek aktiv na l1, převede dostatečná aktiva z l2. Vzhledem k tomu, že se jedná o velmi velké výběry, náklady na výběr se amortizují na velkou částku a tvoří mnohem menší procento.

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

Zde najdete další mou práci (opens in a new tab).