Přeskočit 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.

1// SPDX-License-Identifier: MIT

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

1pragma 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í.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Události *
7 **********/
8
9 event ERC20DepositInitiated(
Zobrazit vše

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

1 address indexed _l1Token,
2 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).

1 address indexed _from,
2 address _to,
3 uint256 _amount,
4 bytes _data
5 );

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í.

1 event ERC20WithdrawalFinalized(
2 address indexed _l1Token,
3 address indexed _l2Token,
4 address indexed _from,
5 address _to,
6 uint256 _amount,
7 bytes _data
8 );

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ů.

1
2 /********************
3 * Veřejné funkce *
4 ********************/
5
6 /**
7 * @dev získá adresu odpovídajícího kontraktu přemostění na L2.
8 * @return Adresa odpovídajícího kontraktu přemostění na L2.
9 */
10 function l2TokenBridge() external returns (address);
Zobrazit vše

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á.

1 /**
2 * @dev vloží částku ERC20 na zůstatek volajícího na L2.
3 * @param _l1Token Adresa ERC20 na L1, který vkládáme.
4 * @param _l2Token Adresa příslušného ERC20 na L2.
5 * @param _amount Částka ERC20 k vložení.
6 * @param _l2Gas Limit transakčních poplatků potřebný k dokončení vkladu na L2.
7 * @param _data Volitelná data k předání na L2. Tato data jsou poskytována
8 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
9 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Zobrazit vše

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.

1 /**
2 * @dev vloží částku ERC20 na zůstatek příjemce na L2.
3 * @param _l1Token Adresa ERC20 na L1, který vkládáme.
4 * @param _l2Token Adresa příslušného ERC20 na L2.
5 * @param _to Adresa na L2, na kterou se připíše výběr.
6 * @param _amount Částka ERC20 k vložení.
7 * @param _l2Gas Limit transakčních poplatků potřebný k dokončení vkladu na L2.
8 * @param _data Volitelná data k předání na L2. Tato data jsou poskytována
9 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
10 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
11 */
12 function depositERC20To(
13 address _l1Token,
14 address _l2Token,
15 address _to,
16 uint256 _amount,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external;
Zobrazit vše

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

1} /*************************
2 * Meziřetězcové funkce *
3 *************************/
4
5 /**
6 * @dev Dokončí výběr z L2 na L1 a připíše prostředky na zůstatek příjemce
7 * tokenu ERC20 na L1.
8 * Toto volání selže, pokud inicializovaný výběr z L2 nebyl finalizován.
9 *
10 * @param _l1Token Adresa tokenu na L1, pro který se má dokončit výběr.
11 * @param _l2Token Adresa tokenu na L2, kde byl výběr iniciován.
12 * @param _from Adresa na L2, která iniciuje převod.
13 * @param _to Adresa na L1, na kterou se má výběr připsat.
14 * @param _amount Částka ERC20 k vložení.
15 * @param _data Data poskytnutá odesílatelem na L2. Tato data jsou poskytována
16 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
17 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Zobrazit vše

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.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /**********
11 * Události *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Zobrazit vše

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.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Veřejné funkce *
9 ********************/
10
11 /**
12 * @dev Vloží částku ETH na zůstatek volajícího na L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Vloží částku ETH na zůstatek příjemce na L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Meziřetězcové funkce *
33 *************************/
34
35 /**
36 * @dev Dokončí výběr z L2 na L1 a připíše prostředky na zůstatek příjemce
37 * tokenu ETH na L1. Protože tuto funkci může volat pouze xDomainMessenger, nebude nikdy volána
38 * před dokončením výběru.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Zobrazit vše

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.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Importy rozhraní */
5import { 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.

1/**
2 * @title CrossDomainEnabled
3 * @dev Pomocný kontrakt pro kontrakty provádějící mezidoménovou komunikaci
4 *
5 * Použitý kompilátor: definován dědícím kontraktem
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Proměnné *
10 *************/
11
12 // Kontrakt Messengeru použitý k odesílání a přijímání zpráv z druhé domény.
13 address public messenger;
14
15 /***************
16 * Konstruktor *
17 ***************/
18
19 /**
20 * @param _messenger Adresa CrossDomainMessenger na aktuální vrstvě.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Zobrazit vše

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í.

1
2 /**********************
3 * Modifikátory funkcí *
4 **********************/
5
6 /**
7 * Vynucuje, aby upravená funkce byla volána pouze z konkrétního mezidoménového účtu.
8 * @param _sourceDomainAccount Jediný účet na původní doméně, který je
9 * oprávněn tuto funkci volat.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Zobrazit vše

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ě.

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

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).

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

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í.

1
2 _;
3 }
4
5 /**********************
6 * Interní funkce *
7 **********************/
8
9 /**
10 * Získá posílače zpráv, obvykle z úložiště. Tato funkce je vystavena pro případ, že by ji
11 * dětský kontrakt potřeboval přepsat.
12 * @return Adresa kontraktu mezidoménového posílače zpráv, který by se měl použít.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Zobrazit vše

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.

1
2 /**
3 * Odešle zprávu účtu v jiné doméně.
4 * @param _crossDomainTarget Zamýšlený příjemce v cílové doméně.
5 * @param _message Data k odeslání cíli (obvykle calldata pro funkci s
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit Limit transakčních poplatků pro příjem zprávy v cílové doméně.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Zobrazit vše

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

1 ) internal {
2 // 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)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

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).

1// SPDX-License-Identifier: MIT
2pragma 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á.

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

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

1import { 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.

1import { 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.

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

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

1import { 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.

1import { 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ů.

1import { 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).

1/**
2 * @title L1StandardBridge
3 * @dev Přemostění L1 pro ETH a ERC20 je kontrakt, který ukládá vložené prostředky z L1 a standardní
4 * tokeny, které se používají na L2. Synchronizuje odpovídající přemostění L2, informuje ho o vkladech
5 * a naslouchá mu pro nově finalizované výběry.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Zobrazit vše

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

1
2 /********************************
3 * Reference na externí kontrakty *
4 ********************************/
5
6 address public l2TokenBridge;

Adresa L2StandardBridge.

1
2 // Mapuje token L1 na token L2 k zůstatku vloženého tokenu L1
3 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.

1
2 /***************
3 * Konstruktor *
4 ***************/
5
6 // Tento kontrakt se nachází za proxy, takže parametry konstruktoru nebudou použity.
7 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.

1 /******************
2 * Inicializace *
3 ******************/
4
5 /**
6 * @param _l1messenger Adresa L1 Messengeru používaná pro meziřetězcovou komunikaci.
7 * @param _l2TokenBridge Adresa standardního přemostění na L2.
8 */
9 // slither-disable-next-line external-function
Zobrazit vše

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á.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 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é.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

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

1
2 /**************
3 * Vkládání *
4 **************/
5
6 /** @dev Modifikátor vyžadující, aby odesílatel byl EOA. Toto ověření by mohl obejít zákeřný
7 * kontrakt pomocí initcode, ale řeší to chybu uživatele, které se chceme vyhnout.
8 */
9 modifier onlyEOA() {
10 // Používá se k zastavení vkladů z kontraktů (zabraňuje náhodné ztrátě tokenů)
11 require(!Address.isContract(msg.sender), "Account not EOA");
12 _;
13 }
Zobrazit vše

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

1 /**
2 * @dev Tuto funkci lze volat bez dat
3 * pro vložení částky ETH na zůstatek volajícího na L2.
4 * Protože funkce receive nepřijímá data, konzervativní
5 * výchozí částka se předává na L2.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Zobrazit vše

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í.

1 /**
2 * @inheritdoc IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }
Zobrazit vše

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

1 /**
2 * @dev Provádí logiku pro vklady uložením ETH a informováním L2 ETH Gateway o
3 * vkladu.
4 * @param _from Účet, ze kterého se má vklad na L1 stáhnout.
5 * @param _to Účet, na který se má vklad na L2 připsat.
6 * @param _l2Gas Limit transakčních poplatků potřebný k dokončení vkladu na L2.
7 * @param _data Volitelná data k předání na L2. Tato data jsou poskytována
8 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
9 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Sestavení calldata pro volání finalizeDeposit
18 bytes memory message = abi.encodeWithSelector(
Zobrazit vše

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.

1 IL2ERC20Bridge.finalizeDeposit.selector,
2 address(0),
3 Lib_PredeployAddresses.OVM_ETH,
4 _from,
5 _to,
6 msg.value,
7 _data
8 );

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.
1 // Odeslání calldata na L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

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

1 /**
2 * @inheritdoc IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }
Zobrazit vše

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

1 /**
2 * @dev Provádí logiku pro vklady informováním kontraktu L2 Deposited Token
3 * o vkladu a voláním handleru pro uzamčení prostředků L1. (např. transferFrom)
4 *
5 * @param _l1Token Adresa ERC20 na L1, který vkládáme.
6 * @param _l2Token Adresa příslušného ERC20 na L2.
7 * @param _from Účet, ze kterého se má vklad na L1 stáhnout.
8 * @param _to Účet, na který se má vklad na L2 připsat.
9 * @param _amount Částka ERC20 k vložení.
10 * @param _l2Gas Limit transakčních poplatků potřebný k dokončení vkladu na L2.
11 * @param _data Volitelná data k předání na L2. Tato data jsou poskytována
12 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
13 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
14 */
15 function _initiateERC20Deposit(
16 address _l1Token,
17 address _l2Token,
18 address _from,
19 address _to,
20 uint256 _amount,
21 uint32 _l2Gas,
22 bytes calldata _data
23 ) internal {
Zobrazit vše

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).

1 // Když je vklad iniciován na L1, přemostění L1 převede prostředky na sebe pro budoucí
2 // výběry. safeTransferFrom také kontroluje, zda má kontrakt kód, takže toto selže, pokud
3 // _from je EOA nebo address(0).
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 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.

1 // Sestavení calldata pro _l2Token.finalizeDeposit(_to, _amount)
2 bytes memory message = abi.encodeWithSelector(
3 IL2ERC20Bridge.finalizeDeposit.selector,
4 _l1Token,
5 _l2Token,
6 _from,
7 _to,
8 _amount,
9 _data
10 );
11
12 // Odeslání calldata na L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Zobrazit vše

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.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Meziřetězcové funkce *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Zobrazit vše

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).

1 ) 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í.

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

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

1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Vyslat událost o výběru.

1 }
2
3 /**
4 * @inheritdoc IL1ERC20Bridge
5 */
6 function finalizeERC20Withdrawal(
7 address _l1Token,
8 address _l2Token,
9 address _from,
10 address _to,
11 uint256 _amount,
12 bytes calldata _data
13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {
Zobrazit vše

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

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

Aktualizovat datovou strukturu deposits.

1
2 // Když je výběr finalizován na L1, přemostění L1 převede prostředky vybírajícímu
3 // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events
7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
8 }
9
10
11 /*****************************
12 * Dočasné - migrace ETH *
13 *****************************/
14
15 /**
16 * @dev Přidá zůstatek ETH na účet. Toto je určeno k tomu, aby se ETH
17 * mohlo migrovat ze staré brány na novou bránu.
18 * POZNÁMKA: Toto je ponecháno pouze pro jeden upgrade, abychom mohli přijmout migrované ETH ze
19 * starého kontraktu
20 */
21 function donateETH() external payable {}
22}
Zobrazit vše

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.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { 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é.

1import { 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).

1interface IL2StandardERC20 is IERC20, IERC165 {
2 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.

1
2 function mint(address _to, uint256 _amount) external;
3
4 function burn(address _from, uint256 _amount) external;
5
6 event Mint(address indexed _account, uint256 _amount);
7 event Burn(address indexed _account, uint256 _amount);
8}

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.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { 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.

1import "./IL2StandardERC20.sol";
2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {
4 address public l1Token;
5 address public l2Bridge;

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

1
2 /**
3 * @param _l2Bridge Adresa standardního přemostění na L2.
4 * @param _l1Token Adresa odpovídajícího tokenu na L1.
5 * @param _name Název ERC20.
6 * @param _symbol Symbol ERC20.
7 */
8 constructor(
9 address _l2Bridge,
10 address _l1Token,
11 string memory _name,
12 string memory _symbol
13 ) ERC20(_name, _symbol) {
14 l1Token = _l1Token;
15 l2Bridge = _l2Bridge;
16 }
Zobrazit vše

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

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Zobrazit vše

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.

1 // slither-disable-next-line external-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
Zobrazit vše

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).

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Importy rozhraní */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { 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).
1/* Importy knihoven */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Importy kontraktů */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev Standardní přemostění L2 je kontrakt, který spolupracuje se standardním přemostěním L1,
12 * aby umožnil přechody ETH a ERC20 mezi L1 a L2.
13 * Tento kontrakt funguje jako razič nových tokenů, když se dozví o vkladech do standardního
14 * přemostění L1.
15 * Tento kontrakt také funguje jako palič tokenů určených k výběru a informuje přemostění L1
16 * o uvolnění prostředků L1.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Reference na externí kontrakty *
21 ********************************/
22
23 address public l1TokenBridge;
Zobrazit vše

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.

1
2 /***************
3 * Konstruktor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Mezidoménový posílač zpráv používaný tímto kontraktem.
8 * @param _l1TokenBridge Adresa přemostění L1 nasazeného na hlavní řetězec.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Vybírání *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Zobrazit vše

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.

1
2 /**
3 * @dev Provádí logiku pro výběry pálením tokenu a informováním
4 * brány tokenu L1 o výběru.
5 * @param _l2Token Adresa tokenu na L2, kde je výběr iniciován.
6 * @param _from Účet, ze kterého se má výběr na L2 stáhnout.
7 * @param _to Účet, na který se má výběr na L1 připsat.
8 * @param _amount Částka tokenu k výběru.
9 * @param _l1Gas Nepoužito, ale zahrnuto pro případné úvahy o budoucí kompatibilitě.
10 * @param _data Volitelná data k předání na L1. Tato data jsou poskytována
11 * pouze pro pohodlí externích kontraktů. Kromě vynucení maximální
12 * délky tyto kontrakty neposkytují žádné záruky ohledně jejich obsahu.
13 */
14 function _initiateWithdrawal(
15 address _l2Token,
16 address _from,
17 address _to,
18 uint256 _amount,
19 uint32 _l1Gas,
20 bytes calldata _data
21 ) internal {
22 // Když je výběr iniciován, spálíme prostředky vybírajícího, abychom zabránili následnému použití na L2
23 // slither-disable-next-line reentrancy-events
24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Zobrazit vše

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é).

1
2 // Sestavení calldata pro l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

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

1 message = abi.encodeWithSelector(
2 IL1StandardBridge.finalizeETHWithdrawal.selector,
3 _from,
4 _to,
5 _amount,
6 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Odeslání zprávy do přemostění na L1
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Meziřetězcová funkce: Vkládání *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Zobrazit vše

Tuto funkci volá L1StandardBridge.

1 ) 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.

1 // Zkontrolujte, zda je cílový token vyhovující a
2 // ověřte, že vložený token na L1 odpovídá reprezentaci vloženého tokenu na L2 zde
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _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ů.
1 ) {
2 // Když je vklad finalizován, připíšeme na účet na L2 stejnou částku
3 // tokenů.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

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

  1. Razit tokeny
  2. Vyslat příslušnou událost
1 } else {
2 // Buď token L2, do kterého se vkládá, nesouhlasí se správnou adresou
3 // svého tokenu L1, nebo nepodporuje správné rozhraní.
4 // Toto by se mělo stát pouze v případě zákeřného tokenu L2, nebo pokud uživatel nějakým způsobem
5 // zadal špatnou adresu tokenu L2 pro vklad.
6 // V obou případech zde proces zastavíme a sestavíme zprávu o výběru,
7 // aby si uživatelé mohli v některých případech své prostředky vybrat.
8 // Neexistuje žádný způsob, jak zcela zabránit zákeřným kontraktům tokenů, ale toto omezuje
9 // chyby uživatelů a zmírňuje některé formy zákeřného chování kontraktů.
Zobrazit vše

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ů.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // zde jsme prohodili _to a _from, abychom vklad vrátili odesílateli
6 _from,
7 _amount,
8 _data
9 );
10
11 // Odeslání zprávy do přemostění na L1
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Zobrazit vše

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).

Stránka naposledy aktualizována: 22. října 2025

Byl tento tutoriál užitečný?