Weiter zum Hauptinhalt

Walkthrough zum Vertrag der Optimism-Standard-Brücke

solidity
bridge
layer 2
Fortgeschritten
Ori Pomerantz
30. März 2022
32 Minuten Lesedauer

Optimismopens in a new tab ist ein Optimistic Rollup. Optimistic Rollups können Transaktionen zu einem viel niedrigeren Preis als das Ethereum Mainnet (auch bekannt als Layer 1 oder L1) verarbeiten, da Transaktionen nur von einigen wenigen Knoten anstatt von jedem Knoten im Netzwerk verarbeitet werden. Gleichzeitig werden alle Daten auf L1 geschrieben, sodass alles mit der Integrität und Verfügbarkeit des Mainnets nachgewiesen und rekonstruiert werden kann.

Um L1-Assets auf Optimism (oder einem anderen L2) zu verwenden, müssen die Assets überbrückt werden. Eine Möglichkeit, dies zu erreichen, besteht darin, dass Benutzer Assets (ETH und ERC-20-Tokens sind die häufigsten) auf L1 sperren und gleichwertige Assets zur Verwendung auf L2 erhalten. Schließlich möchte derjenige, der sie am Ende besitzt, sie vielleicht wieder auf L1 zurückbrücken. Dabei werden die Assets auf L2 verbrannt und dann auf L1 wieder an den Benutzer freigegeben.

So funktioniert die Optimism Standard-Brückeopens in a new tab. In diesem Artikel gehen wir den Quellcode für diese Brücke durch, um zu sehen, wie sie funktioniert, und um sie als Beispiel für gut geschriebenen Solidity-Code zu studieren.

Kontrollflüsse

Die Brücke hat zwei Hauptabläufe:

  • Einzahlung (von L1 nach L2)
  • Auszahlung (von L2 nach L1)

Einzahlungsablauf

Layer 1

  1. Bei der Einzahlung eines ERC-20 erteilt der Einzahler der Brücke eine Freigabe, den eingezahlten Betrag auszugeben.
  2. Der Einzahler ruft die L1-Brücke auf (depositERC20, depositERC20To, depositETH oder depositETHTo)
  3. Die L1-Brücke nimmt das überbrückte Asset in Besitz.
    • ETH: Das Asset wird vom Einzahler als Teil des Aufrufs übertragen.
    • ERC-20: Das Asset wird von der Brücke unter Verwendung der vom Einzahler erteilten Freigabe an sich selbst übertragen.
  4. Die L1-Brücke verwendet den domänenübergreifenden Nachrichtenmechanismus, um finalizeDeposit auf der L2-Brücke aufzurufen.

Layer 2

  1. Die L2-Brücke überprüft, ob der Aufruf von finalizeDeposit rechtmäßig ist:
    • Kam vom domänenübergreifenden Nachrichtenvertrag
    • War ursprünglich von der Brücke auf L1
  2. Die L2-Brücke prüft, ob der ERC-20-Token-Vertrag auf L2 der richtige ist:
  3. Wenn der L2-Vertrag der richtige ist, rufen Sie ihn auf, um die entsprechende Anzahl von Tokens an die entsprechende Adresse zu prägen. Wenn nicht, starten Sie einen Auszahlungsprozess, damit der Benutzer die Tokens auf L1 beanspruchen kann.

Auszahlungsablauf

Layer 2

  1. Der Auszahlende ruft die L2-Brücke auf (withdraw oder withdrawTo)
  2. Die L2-Brücke verbrennt die entsprechende Anzahl von Tokens, die msg.sender gehören.
  3. Die L2-Brücke verwendet den domänenübergreifenden Nachrichtenmechanismus, um finalizeETHWithdrawal oder finalizeERC20Withdrawal auf der L1-Brücke aufzurufen.

Layer 1

  1. Die L1-Brücke überprüft, ob der Aufruf von finalizeETHWithdrawal oder finalizeERC20Withdrawal rechtmäßig ist:
    • Kam vom domänenübergreifenden Nachrichtenmechanismus
    • War ursprünglich von der Brücke auf L2
  2. Die L1-Brücke überträgt das entsprechende Asset (ETH oder ERC-20) an die entsprechende Adresse.

Layer-1-Code

Dies ist der Code, der auf L1, dem Ethereum Mainnet, läuft.

IL1ERC20Bridge

Diese Schnittstelle ist hier definiertopens in a new tab. Sie enthält Funktionen und Definitionen, die für die Überbrückung von ERC-20-Tokens erforderlich sind.

1// SPDX-License-Identifier: MIT

Der größte Teil des Codes von Optimism wird unter der MIT-Lizenz veröffentlichtopens in a new tab.

1pragma solidity >0.5.0 <0.9.0;

Zum Zeitpunkt des Schreibens ist die neueste Version von Solidity 0.8.12. Bis zur Veröffentlichung von Version 0.9.0 wissen wir nicht, ob dieser Code damit kompatibel ist oder nicht.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Ereignisse *
7 **********/
8
9 event ERC20DepositInitiated(
Alles anzeigen

In der Terminologie der Optimism-Brücke bedeutet Einzahlung eine Übertragung von L1 nach L2 und Auszahlung eine Übertragung von L2 nach L1.

1 address indexed _l1Token,
2 address indexed _l2Token,

In den meisten Fällen ist die Adresse eines ERC-20 auf L1 nicht dieselbe wie die Adresse des entsprechenden ERC-20 auf L2. Sie können die Liste der Token-Adressen hier einsehenopens in a new tab. Die Adresse mit chainId 1 befindet sich auf L1 (Mainnet) und die Adresse mit chainId 10 auf L2 (Optimism). Die anderen beiden chainId-Werte sind für das Kovan-Testnetz (42) und das Optimistic Kovan-Testnetz (69).

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

Es ist möglich, Notizen zu Übertragungen hinzuzufügen, in diesem Fall werden sie zu den Ereignissen hinzugefügt, die sie melden.

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

Derselbe Brückenvertrag behandelt Übertragungen in beide Richtungen. Im Fall der L1-Brücke bedeutet dies die Initiierung von Einzahlungen und die Finalisierung von Auszahlungen.

1
2 /********************
3 * Öffentliche Funktionen *
4 ********************/
5
6 /**
7 * @dev Ruft die Adresse des entsprechenden L2-Brückenvertrags ab.
8 * @return Adresse des entsprechenden L2-Brückenvertrags.
9 */
10 function l2TokenBridge() external returns (address);
Alles anzeigen

Diese Funktion wird nicht wirklich benötigt, da es sich auf L2 um einen vorab bereitgestellten Vertrag (predeployed contract) handelt, der sich also immer an der Adresse 0x4200000000000000000000000000000000000010 befindet. Sie ist hier aus Symmetriegründen zur L2-Brücke, da die Adresse der L1-Brücke nicht trivial zu kennen ist.

1 /**
2 * @dev Zahlt einen Betrag des ERC20 auf das Guthaben des Aufrufers auf L2 ein.
3 * @param _l1Token Adresse des L1 ERC20, das wir einzahlen.
4 * @param _l2Token Adresse des entsprechenden L2 ERC20.
5 * @param _amount Einzuzahlender Betrag des ERC20.
6 * @param _l2Gas Erforderliches Gaslimit, um die Einzahlung auf L2 abzuschließen.
7 * @param _data Optionale Daten zur Weiterleitung an L2. Diese Daten werden
8 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
9 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Alles anzeigen

Der Parameter _l2Gas ist die Menge an L2-Gas, die die Transaktion verbrauchen darf. Bis zu einem bestimmten (hohen) Limit ist dies kostenlosopens in a new tab, daher sollte es kein Problem sein, es sei denn, der ERC-20-Vertrag macht beim Prägen etwas wirklich Seltsames. Diese Funktion kümmert sich um das übliche Szenario, bei dem ein Benutzer Assets an dieselbe Adresse auf einer anderen Blockchain überbrückt.

1 /**
2 * @dev zahlt einen Betrag von ERC20 auf das Guthaben eines Empfängers auf L2 ein.
3 * @param _l1Token Adresse des L1 ERC20, den wir einzahlen
4 * @param _l2Token Adresse des entsprechenden L2 ERC20 auf L1
5 * @param _to L2-Adresse, auf die die Abhebung gutgeschrieben werden soll.
6 * @param _amount Einzuzahlender Betrag des ERC20.
7 * @param _l2Gas Gaslimit, das erforderlich ist, um die Einzahlung auf L2 abzuschließen.
8 * @param _data Optionale Daten zur Weiterleitung an L2. Diese Daten werden
9 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
10 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
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;
Alles anzeigen

Diese Funktion ist fast identisch mit depositERC20, aber sie ermöglicht es Ihnen, den ERC-20 an eine andere Adresse zu senden.

1 /*************************
2 * Cross-Chain-Funktionen *
3 *************************/
4
5 /**
6 * @dev Schließt eine Auszahlung von L2 nach L1 ab und schreibt den Betrag dem Guthaben des
7 * L1-ERC20-Tokens des Empfängers gut.
8 * Dieser Aufruf schlägt fehl, wenn die initiierte Auszahlung von L2 noch nicht finalisiert wurde.
9 *
10 * @param _l1Token Adresse des L1-Tokens, für das finalizeWithdrawal ausgeführt wird.
11 * @param _l2Token Adresse des L2-Tokens, auf dem die Auszahlung eingeleitet wurde.
12 * @param _from L2-Adresse, die die Übertragung initiiert.
13 * @param _to L1-Adresse, der die Auszahlung gutgeschrieben werden soll.
14 * @param _amount Einzuzahlender Betrag des ERC20.
15 * @param _data Vom Absender auf L2 bereitgestellte Daten. Diese Daten werden
16 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
17 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
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}
Alles anzeigen

Auszahlungen (und andere Nachrichten von L2 nach L1) in Optimism sind ein zweistufiger Prozess:

  1. Eine initiierende Transaktion auf L2.
  2. Eine finalisierende oder beanspruchende Transaktion auf L1. Diese Transaktion muss nach dem Ende des Fehler-Anfechtungszeitraumsopens in a new tab für die L2-Transaktion erfolgen.

IL1StandardBridge

Diese Schnittstelle ist hier definiertopens in a new tab. Diese Datei enthält Ereignis- und Funktionsdefinitionen für ETH. Diese Definitionen sind sehr ähnlich zu denen, die oben in IL1ERC20Bridge für ERC-20 definiert wurden.

Die Brückenschnittstelle ist auf zwei Dateien aufgeteilt, da einige ERC-20-Tokens eine benutzerdefinierte Verarbeitung erfordern und nicht von der Standardbrücke verarbeitet werden können. Auf diese Weise kann die benutzerdefinierte Brücke, die einen solchen Token verarbeitet, IL1ERC20Bridge implementieren und muss nicht auch ETH überbrücken.

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 * Ereignisse *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Alles anzeigen

Dieses Ereignis ist fast identisch mit der ERC-20-Version (ERC20DepositInitiated), jedoch ohne die L1- und L2-Token-Adressen. Dasselbe gilt für die anderen Ereignisse und die Funktionen.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Öffentliche Funktionen *
9 ********************/
10
11 /**
12 * @dev Einzahlung eines ETH-Betrags in das Guthaben des Aufrufers auf L2.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Einzahlung eines ETH-Betrags in das Guthaben eines Empfängers auf L2.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Cross-Chain-Funktionen *
33 *************************/
34
35 /**
36 * @dev Schließen Sie eine Auszahlung von L2 nach L1 ab und schreiben Sie den Betrag dem Guthaben des L1-ETH-Tokens des Empfängers gut.
37 * Da nur der xDomainMessenger diese Funktion aufrufen kann, wird sie niemals aufgerufen
38 * bevor die Auszahlung finalisiert ist.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Alles anzeigen

CrossDomainEnabled

Dieser Vertragopens in a new tab wird von beiden Brücken (L1 und L2) geerbt, um Nachrichten an den anderen Layer zu senden.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Schnittstellen-Importe */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Diese Schnittstelleopens in a new tab teilt dem Vertrag mit, wie Nachrichten an den anderen Layer gesendet werden sollen, indem der Cross-Domain-Messenger verwendet wird. Dieser Cross-Domain-Messenger ist ein ganz anderes System und verdient einen eigenen Artikel, den ich hoffentlich in Zukunft schreiben werde.

1/**
2 * @title CrossDomainEnabled
3 * @dev Hilfsvertrag für Verträge, die domänenübergreifende Kommunikation durchführen
4 *
5 * Verwendeter Compiler: definiert durch vererbenden Vertrag
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Variablen *
10 *************/
11
12 // Messenger-Vertrag, der zum Senden und Empfangen von Nachrichten aus der anderen Domäne verwendet wird.
13 address public messenger;
14
15 /***************
16 * Konstruktor *
17 ***************/
18
19 /**
20 * @param _messenger Adresse des CrossDomainMessenger auf der aktuellen Ebene.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Alles anzeigen

Der eine Parameter, den der Vertrag kennen muss, ist die Adresse des Cross-Domain-Messengers auf diesem Layer. Dieser Parameter wird einmal im Konstruktor gesetzt und ändert sich nie.

1
2 /**********************
3 * Funktionsmodifikatoren *
4 **********************/
5
6 /**
7 * Erzwingt, dass die modifizierte Funktion nur von einem bestimmten domänenübergreifenden Konto aufgerufen werden kann.
8 * @param _sourceDomainAccount Das einzige Konto in der Ursprungsdomäne, das
9 * zur Ausführung dieser Funktion berechtigt ist.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Alles anzeigen

Das domänenübergreifende Messaging ist für jeden Vertrag auf der Blockchain zugänglich, auf der es läuft (entweder Ethereum Mainnet oder Optimism). Aber wir brauchen die Brücke auf jeder Seite, um nur bestimmten Nachrichten zu vertrauen, wenn sie von der Brücke auf der anderen Seite kommen.

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

Nur Nachrichten vom entsprechenden Cross-Domain-Messenger (messenger, wie Sie unten sehen) können als vertrauenswürdig eingestuft werden.

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

Die Art und Weise, wie der Cross-Domain-Messenger die Adresse bereitstellt, die eine Nachricht mit dem anderen Layer gesendet hat, ist die Funktion .xDomainMessageSender()opens in a new tab. Solange sie in der Transaktion aufgerufen wird, die durch die Nachricht initiiert wurde, kann sie diese Information bereitstellen.

Wir müssen sicherstellen, dass die Nachricht, die wir erhalten haben, von der anderen Brücke kam.

1
2 _;
3 }
4
5 /**********************
6 * Interne Funktionen *
7 **********************/
8
9 /**
10 * Ruft den Messenger ab, normalerweise aus dem Speicher. Diese Funktion wird für den Fall verfügbar gemacht, dass ein untergeordneter Vertrag
11 * sie überschreiben muss.
12 * @return Die Adresse des Cross-Domain-Messenger-Vertrags, der verwendet werden sollte.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Alles anzeigen

Diese Funktion gibt den Cross-Domain-Messenger zurück. Wir verwenden eine Funktion anstelle der Variable messenger, um Verträgen, die von diesem erben, zu ermöglichen, einen Algorithmus zu verwenden, um anzugeben, welcher Cross-Domain-Messenger verwendet werden soll.

1
2 /**
3 * Sendet eine Nachricht an ein Konto in einer anderen Domäne
4 * @param _crossDomainTarget Der beabsichtigte Empfänger in der Zieldomäne
5 * @param _message Die an das Ziel zu sendenden Daten (normalerweise Calldata an eine Funktion mit
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit Das GasLimit für den Empfang der Nachricht in der Zieldomäne.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Alles anzeigen

Schließlich die Funktion, die eine Nachricht an den anderen Layer sendet.

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

Slitheropens in a new tab ist ein statischer Analysator, den Optimism auf jedem Vertrag ausführt, um nach Schwachstellen und anderen potenziellen Problemen zu suchen. In diesem Fall löst die folgende Zeile zwei Schwachstellen aus:

  1. Reentrancy-Ereignisseopens in a new tab
  2. Gutartige Wiedereintrittsfähigkeitopens in a new tab
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

In diesem Fall machen wir uns keine Sorgen über Wiedereintrittsfähigkeit, da wir wissen, dass getCrossDomainMessenger() eine vertrauenswürdige Adresse zurückgibt, auch wenn Slither keine Möglichkeit hat, das zu wissen.

Der L1-Brückenvertrag

Der Quellcode für diesen Vertrag befindet sich hieropens in a new tab.

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

Die Schnittstellen können Teil anderer Verträge sein, daher müssen sie eine breite Palette von Solidity-Versionen unterstützen. Aber die Brücke selbst ist unser Vertrag, und wir können streng sein, welche Solidity-Version sie verwendet.

1/* Schnittstellen-Importe */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge und IL1StandardBridge werden oben erklärt.

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

Diese Schnittstelleopens in a new tab ermöglicht es uns, Nachrichten zu erstellen, um die Standardbrücke auf L2 zu steuern.

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

Diese Schnittstelleopens in a new tab ermöglicht es uns, ERC-20-Verträge zu steuern. Hier können Sie mehr darüber lesen.

1/* Bibliotheks-Importe */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Wie oben erklärt, wird dieser Vertrag für die Interlayer-Nachrichtenübermittlung verwendet.

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.solopens in a new tab) hat die Adressen für die L2-Verträge, die immer dieselbe Adresse haben. Dies schließt die Standard-Brücke auf L2 mit ein.

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

OpenZeppelins Address-Dienstprogrammeopens in a new tab. Es wird verwendet, um zwischen Vertragsadressen und solchen zu unterscheiden, die zu extern besessenen Konten (EOA) gehören.

Beachten Sie, dass dies keine perfekte Lösung ist, da es keine Möglichkeit gibt, zwischen direkten Aufrufen und Aufrufen aus dem Konstruktor eines Vertrags zu unterscheiden, aber zumindest können wir so einige häufige Benutzerfehler identifizieren und verhindern.

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

Der ERC-20-Standardopens in a new tab unterstützt zwei Möglichkeiten für einen Vertrag, einen Fehler zu melden:

  1. Zurücksetzen (revert)
  2. false zurückgeben

Die Handhabung beider Fälle würde unseren Code komplizierter machen, daher verwenden wir stattdessen OpenZeppelins SafeERC20opens in a new tab, das sicherstellt, dass alle Fehler zu einem Revert führenopens in a new tab.

1/**
2 * @title L1StandardBridge
3 * @dev Die L1-ETH- und ERC20-Brücke ist ein Vertrag, der hinterlegte L1-Mittel und Standard-Token
4 * speichert, die auf L2 verwendet werden. Sie synchronisiert eine entsprechende L2-Brücke, informiert sie über Einzahlungen
5 * und hört auf sie für neu finalisierte Auszahlungen.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Alles anzeigen

Diese Zeile gibt an, dass der SafeERC20-Wrapper jedes Mal verwendet werden soll, wenn wir die IERC20-Schnittstelle verwenden.

1
2 /********************************
3 * Externe Vertragsreferenzen *
4 ********************************/
5
6 address public l2TokenBridge;

Die Adresse von L2StandardBridge.

1
2 // Ordnet L1-Token zu L2-Token zu Saldo des hinterlegten L1-Tokens zu
3 mapping(address => mapping(address => uint256)) public deposits;

Ein doppeltes Mappingopens in a new tab wie dieses ist die Art und Weise, wie Sie ein zweidimensionales dünn besetztes Arrayopens in a new tab definieren. Werte in dieser Datenstruktur werden als deposit[L1-Token-Adresse][L2-Token-Adresse] identifiziert. Der Standardwert ist Null. Nur Zellen, die auf einen anderen Wert gesetzt sind, werden in den Speicher geschrieben.

1
2 /***************
3 * Konstruktor *
4 ***************/
5
6 // Dieser Vertrag lebt hinter einem Proxy, daher werden die Konstruktorparameter ungenutzt bleiben.
7 constructor() CrossDomainEnabled(address(0)) {}

Wir wollen diesen Vertrag aktualisieren können, ohne alle Variablen im Speicher kopieren zu müssen. Dazu verwenden wir einen Proxyopens in a new tab, einen Vertrag, der delegatecall verwendet, um Aufrufe an einen separaten Vertrag zu übertragen, dessen Adresse vom Proxy-Vertrag gespeichert wird (wenn Sie ein Upgrade durchführen, weisen Sie den Proxy an, diese Adresse zu ändern). Wenn Sie delegatecall verwenden, bleibt der Speicher der Speicher des aufrufenden Vertrags, sodass die Werte aller Vertragszustandsvariablen unberührt bleiben.

Ein Effekt dieses Musters ist, dass der Speicher des Vertrags, der der Aufgerufene von delegatecall ist, nicht verwendet wird und daher die an ihn übergebenen Konstruktorwerte keine Rolle spielen. Dies ist der Grund, warum wir dem CrossDomainEnabled-Konstruktor einen unsinnigen Wert übergeben können. Dies ist auch der Grund, warum die folgende Initialisierung vom Konstruktor getrennt ist.

1 /******************
2 * Initialisierung *
3 ******************/
4
5 /**
6 * @param _l1messenger L1 Messenger-Adresse, die für die Cross-Chain-Kommunikation verwendet wird.
7 * @param _l2TokenBridge L2-Standard-Brückenadresse.
8 */
9 // slither-disable-next-line external-function
Alles anzeigen

Dieser Slither-Testopens in a new tab identifiziert Funktionen, die nicht aus dem Vertragscode aufgerufen werden und daher als external anstatt public deklariert werden könnten. Die Gaskosten von external-Funktionen können niedriger sein, da sie mit Parametern in den Calldata versorgt werden können. Als public deklarierte Funktionen müssen innerhalb des Vertrags zugänglich sein. Verträge können ihre eigenen Calldata nicht ändern, daher müssen sich die Parameter im Speicher befinden. Wenn eine solche Funktion extern aufgerufen wird, ist es notwendig, die Calldata in den Speicher zu kopieren, was Gas kostet. In diesem Fall wird die Funktion nur einmal aufgerufen, sodass die Ineffizienz für uns keine Rolle spielt.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "Vertrag wurde bereits initialisiert.");

Die initialize-Funktion sollte nur einmal aufgerufen werden. Wenn sich die Adresse des L1-Cross-Domain-Messengers oder der L2-Token-Brücke ändert, erstellen wir einen neuen Proxy und eine neue Brücke, die ihn aufruft. Dies wird wahrscheinlich nur bei einem Upgrade des gesamten Systems geschehen, ein sehr seltenes Ereignis.

Beachten Sie, dass diese Funktion keinen Mechanismus hat, der einschränkt, wer sie aufrufen kann. Das bedeutet, dass ein Angreifer theoretisch warten könnte, bis wir den Proxy und die erste Version der Brücke deployen, und dann Front-Running betreibenopens in a new tab könnte, um zur initialize-Funktion zu gelangen, bevor der legitime Benutzer dies tut. Es gibt jedoch zwei Methoden, um dies zu verhindern:

  1. Wenn die Verträge nicht direkt von einem EOA, sondern in einer Transaktion bereitgestellt werden, in der ein anderer Vertrag sie erstelltopens in a new tab, kann der gesamte Prozess atomar sein und abgeschlossen werden, bevor eine andere Transaktion ausgeführt wird.
  2. Wenn der legitime Aufruf von initialize fehlschlägt, ist es immer möglich, den neu erstellten Proxy und die Brücke zu ignorieren und neue zu erstellen.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

Dies sind die beiden Parameter, die die Brücke kennen muss.

1
2 /**************
3 * Einzahlung *
4 **************/
5
6 /** @dev Modifikator, der erfordert, dass der Absender ein EOA ist. Diese Prüfung könnte von einem bösartigen
7 * Vertrag über initcode umgangen werden, aber sie kümmert sich um den Benutzerfehler, den wir vermeiden wollen.
8 */
9 modifier onlyEOA() {
10 // Wird verwendet, um Einzahlungen von Verträgen zu stoppen (verhindert versehentlich verlorene Tokens)
11 require(!Address.isContract(msg.sender), "Konto nicht EOA");
12 _;
13 }
Alles anzeigen

Dies ist der Grund, warum wir die Address-Dienstprogramme von OpenZeppelin benötigten.

1 /**
2 * @dev Diese Funktion kann ohne Daten aufgerufen werden
3 * um einen Betrag von ETH auf das Guthaben des Aufrufers auf L2 einzuzahlen.
4 * Da die Empfangsfunktion keine Daten entgegennimmt, wird ein konservativer
5 * Standardbetrag an L2 weitergeleitet.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Alles anzeigen

Diese Funktion existiert zu Testzwecken. Beachten Sie, dass sie nicht in den Schnittstellendefinitionen erscheint – sie ist nicht für den normalen Gebrauch bestimmt.

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 }
Alles anzeigen

Diese beiden Funktionen sind Wrapper um _initiateETHDeposit, die Funktion, die die eigentliche ETH-Einzahlung abwickelt.

1 /**
2 * @dev Führt die Logik für Einzahlungen durch, indem der ETH gespeichert und das L2-ETH-Gateway
3 * über die Einzahlung informiert wird.
4 * @param _from Konto, von dem die Einzahlung auf L1 abgezogen wird.
5 * @param _to Konto, dem die Einzahlung auf L2 gutgeschrieben wird.
6 * @param _l2Gas Gaslimit, das erforderlich ist, um die Einzahlung auf L2 abzuschließen.
7 * @param _data Optionale Daten zur Weiterleitung an L2. Diese Daten werden
8 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
9 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // Konstruiere Calldata für den finalizeDeposit-Aufruf
18 bytes memory message = abi.encodeWithSelector(
Alles anzeigen

Die Funktionsweise von Cross-Domain-Nachrichten besteht darin, dass der Zielvertrag mit der Nachricht als Calldata aufgerufen wird. Solidity-Verträge interpretieren ihre Calldata immer gemäß den ABI-Spezifikationenopens in a new tab. Die Solidity-Funktion abi.encodeWithSelectoropens in a new tab erstellt diese Calldata.

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

Die Nachricht hier ist, die Funktion finalizeDepositopens in a new tab mit diesen Parametern aufzurufen:

ParameterWertBedeutung
_l1TokenAdresse(0)Sonderwert für ETH (das kein ERC-20-Token ist) auf L1
_l2TokenLib_PredeployAddresses.OVM_ETHDer L2-Vertrag, der ETH auf Optimism verwaltet, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (dieser Vertrag ist nur für den internen Gebrauch von Optimism bestimmt)
_from_fromDie Adresse auf L1, die die ETH sendet
_to_toDie Adresse auf L2, die die ETH empfängt
Betragmsg.valueGesendeter Betrag in Wei (der bereits an die Brücke gesendet wurde)
_data_dataZusätzliche Daten, die an die Einzahlung angehängt werden
1 // Senden Sie Calldata nach L2
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Senden Sie die Nachricht über den Cross-Domain-Messenger.

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

Ein Ereignis auslösen, um jede dezentralisierte Anwendung, die zuhört, über diese Übertragung zu informieren.

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 }
Alles anzeigen

Diese beiden Funktionen sind Wrapper um _initiateERC20Deposit, die Funktion, die die eigentliche ERC-20-Einzahlung abwickelt.

1 /**
2 * @dev Führt die Logik für Einzahlungen durch, indem der L2 Deposited Token
3 * Vertrag über die Einzahlung informiert wird und ein Handler aufgerufen wird, um die L1-Mittel zu sperren. (z. B. transferFrom)
4 *
5 * @param _l1Token Adresse des L1 ERC20, den wir einzahlen
6 * @param _l2Token Adresse des entsprechenden L2 ERC20 auf L1
7 * @param _from Konto, von dem die Einzahlung auf L1 abgezogen wird
8 * @param _to Konto, dem die Einzahlung auf L2 gutgeschrieben wird
9 * @param _amount Einzuzahlender Betrag des ERC20.
10 * @param _l2Gas Gaslimit, das erforderlich ist, um die Einzahlung auf L2 abzuschließen.
11 * @param _data Optionale Daten zur Weiterleitung an L2. Diese Daten werden
12 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
13 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
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 {
Alles anzeigen

Diese Funktion ist ähnlich wie _initiateETHDeposit oben, mit einigen wichtigen Unterschieden. Der erste Unterschied besteht darin, dass diese Funktion die Token-Adressen und den zu übertragenden Betrag als Parameter erhält. Im Fall von ETH enthält der Aufruf der Brücke bereits die Übertragung des Assets auf das Brückenkonto (msg.value).

1 // Wenn eine Einzahlung auf L1 initiiert wird, überträgt die L1-Brücke die Mittel an sich selbst für zukünftige
2 // Auszahlungen. safeTransferFrom prüft auch, ob der Vertrag Code hat, sodass dies fehlschlägt, wenn
3 // _from ein EOA oder address(0) ist.
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

Übertragungen von ERC-20-Token folgen einem anderen Prozess als ETH:

  1. Der Benutzer (_from) erteilt der Brücke eine Freigabe, um die entsprechenden Tokens zu übertragen.
  2. Der Benutzer ruft die Brücke mit der Adresse des Token-Vertrags, dem Betrag usw. auf.
  3. Die Brücke überträgt die Tokens (an sich selbst) als Teil des Einzahlungsprozesses.

Der erste Schritt kann in einer separaten Transaktion von den letzten beiden erfolgen. Front-Running ist jedoch kein Problem, da die beiden Funktionen, die _initiateERC20Deposit aufrufen (depositERC20 und depositERC20To), diese Funktion nur mit msg.sender als _from-Parameter aufrufen.

1 // Konstruiere Calldata für _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 // Sende Calldata nach 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;
Alles anzeigen

Fügen Sie den eingezahlten Betrag an Tokens zur deposits-Datenstruktur hinzu. Es könnten mehrere Adressen auf L2 vorhanden sein, die demselben L1-ERC-20-Token entsprechen, daher ist es nicht ausreichend, das Guthaben der Brücke am L1-ERC-20-Token zu verwenden, um die Einzahlungen zu verfolgen.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Cross-Chain-Funktionen *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Alles anzeigen

Die L2-Brücke sendet eine Nachricht an den L2-Cross-Domain-Messenger, der bewirkt, dass der L1-Cross-Domain-Messenger diese Funktion aufruft (natürlich sobald die Transaktion, die die Nachricht finalisiertopens in a new tab, auf L1 übermittelt wurde).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Stellen Sie sicher, dass dies eine legitime Nachricht ist, die vom Cross-Domain-Messenger kommt und von der L2-Token-Brücke stammt. Diese Funktion wird verwendet, um ETH von der Brücke abzuheben, daher müssen wir sicherstellen, dass sie nur vom autorisierten Aufrufer aufgerufen wird.

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

Die Art und Weise, ETH zu übertragen, besteht darin, den Empfänger mit dem Betrag in Wei im msg.value aufzurufen.

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

Ein Ereignis über die Auszahlung auslösen.

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) {
Alles anzeigen

Diese Funktion ist ähnlich wie finalizeETHWithdrawal oben, mit den notwendigen Änderungen für ERC-20-Tokens.

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

Aktualisieren Sie die deposits-Datenstruktur.

1
2 // Wenn eine Auszahlung auf L1 finalisiert wird, überträgt die L1-Brücke die Mittel an den Auszahlenden
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 * Vorübergehend - Migration von ETH *
13 *****************************/
14
15 /**
16 * @dev Fügt ETH-Guthaben zum Konto hinzu. Dies soll ermöglichen, dass ETH
17 * von einem alten Gateway zu einem neuen Gateway migriert wird.
18 * HINWEIS: Dies wird nur für ein Upgrade beibehalten, damit wir die migrierte ETH aus dem
19 * alten Vertrag empfangen können
20 */
21 function donateETH() external payable {}
22}
Alles anzeigen

Es gab eine frühere Implementierung der Brücke. Als wir von dieser Implementierung zu dieser wechselten, mussten wir alle Assets verschieben. ERC-20-Tokens können einfach verschoben werden. Um jedoch ETH an einen Vertrag zu übertragen, benötigen Sie die Zustimmung dieses Vertrags, was uns donateETH bietet.

ERC-20-Tokens auf L2

Damit ein ERC-20-Token in die Standardbrücke passt, muss es der Standardbrücke und nur der Standardbrücke erlauben, Token zu prägen. Dies ist notwendig, weil die Brücken sicherstellen müssen, dass die Anzahl der auf Optimism zirkulierenden Tokens gleich der Anzahl der im L1-Brückenvertrag gesperrten Tokens ist. Wenn es zu viele Tokens auf L2 gibt, könnten einige Benutzer ihre Assets nicht zurück auf L1 überbrücken. Anstelle einer vertrauenswürdigen Brücke würden wir im Wesentlichen Mindestreservebankingopens in a new tab nachbilden. Wenn es zu viele Tokens auf L1 gibt, würden einige dieser Tokens für immer im Brückenvertrag gesperrt bleiben, weil es keine Möglichkeit gibt, sie ohne das Verbrennen von L2-Tokens freizugeben.

IL2StandardERC20

Jeder ERC-20-Token auf L2, der die Standardbrücke verwendet, muss diese Schnittstelleopens in a new tab bereitstellen, die die Funktionen und Ereignisse enthält, die die Standardbrücke benötigt.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Die Standard-ERC-20-Schnittstelleopens in a new tab enthält nicht die Funktionen mint und burn. Diese Methoden sind nicht durch den ERC-20-Standardopens in a new tab vorgeschrieben, der die Mechanismen zur Erstellung und Zerstörung von Tokens nicht spezifiziert.

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

Die ERC-165-Schnittstelleopens in a new tab wird verwendet, um anzugeben, welche Funktionen ein Vertrag bereitstellt. Sie können den Standard hier lesenopens in a new tab.

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

Diese Funktion gibt die Adresse des L1-Tokens an, das auf diesen Vertrag überbrückt wird. Beachten Sie, dass wir keine ähnliche Funktion in die entgegengesetzte Richtung haben. Wir müssen in der Lage sein, jeden L1-Token zu überbrücken, unabhängig davon, ob bei seiner Implementierung eine L2-Unterstützung geplant war oder nicht.

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}

Funktionen und Ereignisse zum Prägen (Erstellen) und Verbrennen (Zerstören) von Tokens. Die Brücke sollte die einzige Entität sein, die diese Funktionen ausführen kann, um sicherzustellen, dass die Anzahl der Tokens korrekt ist (gleich der Anzahl der auf L1 gesperrten Tokens).

L2StandardERC20

Dies ist unsere Implementierung der IL2StandardERC20-Schnittstelleopens in a new tab. Sofern Sie keine benutzerdefinierte Logik benötigen, sollten Sie diese verwenden.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

Der OpenZeppelin ERC-20-Vertragopens in a new tab. Optimism glaubt nicht daran, das Rad neu zu erfinden, besonders wenn das Rad gut geprüft ist und vertrauenswürdig genug sein muss, um Vermögenswerte zu halten.

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

Dies sind die beiden zusätzlichen Konfigurationsparameter, die wir benötigen und die ERC-20 normalerweise nicht hat.

1
2 /**
3 * @param _l2Bridge Adresse der L2-Standardbrücke.
4 * @param _l1Token Adresse des entsprechenden L1-Tokens.
5 * @param _name ERC20-Name.
6 * @param _symbol ERC20-Symbol.
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 }
Alles anzeigen

Zuerst den Konstruktor für den Vertrag aufrufen, von dem wir erben (ERC20(_name, _symbol)) und dann unsere eigenen Variablen setzen.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Nur die L2-Brücke kann prägen und verbrennen");
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 }
Alles anzeigen

So funktioniert ERC-165opens in a new tab. Jede Schnittstelle ist eine Anzahl von unterstützten Funktionen und wird als Exklusiv-Oderopens in a new tab der ABI-Funktionsselektorenopens in a new tab dieser Funktionen identifiziert.

Die L2-Brücke verwendet ERC-165 als Plausibilitätsprüfung, um sicherzustellen, dass der ERC-20-Vertrag, an den sie Assets sendet, ein IL2StandardERC20 ist.

Hinweis: Es gibt nichts, was betrügerische Verträge daran hindert, falsche Antworten auf supportsInterface zu geben, daher ist dies ein Plausibilitätsprüfungsmechanismus, kein Sicherheitsmechanismus.

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}
Alles anzeigen

Nur die L2-Brücke darf Assets prägen und verbrennen.

_mint und _burn sind tatsächlich im OpenZeppelin ERC-20-Vertrag definiert. Dieser Vertrag macht sie nur nicht extern zugänglich, weil die Bedingungen zum Prägen und Verbrennen von Tokens so vielfältig sind wie die Anzahl der Verwendungsmöglichkeiten von ERC-20.

L2-Brücken-Code

Dies ist der Code, der die Brücke auf Optimism ausführt. Die Quelle für diesen Vertrag befindet sich hieropens in a new tab.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Schnittstellen-Importe */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

Die IL2ERC20Bridgeopens in a new tab-Schnittstelle ist dem L1-Äquivalent, das wir oben gesehen haben, sehr ähnlich. Es gibt zwei wesentliche Unterschiede:

  1. Auf L1 initiieren Sie Einzahlungen und finalisieren Auszahlungen. Hier initiieren Sie Auszahlungen und finalisieren Einzahlungen.
  2. Auf L1 ist es notwendig, zwischen ETH- und ERC-20-Tokens zu unterscheiden. Auf L2 können wir dieselben Funktionen für beide verwenden, da intern ETH-Guthaben auf Optimism als ERC-20-Token mit der Adresse 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens in a new tab behandelt werden.
1/* Bibliotheks-Importe */
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/* Vertrags-Importe */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev Die L2-Standardbrücke ist ein Vertrag, der zusammen mit der L1-Standardbrücke
12 * ETH- und ERC20-Übergänge zwischen L1 und L2 ermöglicht.
13 * Dieser Vertrag fungiert als Minter für neue Tokens, wenn er von Einzahlungen in die L1-Standardbrücke
14 * erfährt.
15 * Dieser Vertrag fungiert auch als Burner der für die Auszahlung vorgesehenen Tokens und informiert die L1-
16 * Brücke, L1-Mittel freizugeben.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Externe Vertragsreferenzen *
21 ********************************/
22
23 address public l1TokenBridge;
Alles anzeigen

Die Adresse der L1-Brücke im Auge behalten. Beachten Sie, dass wir im Gegensatz zum L1-Äquivalent hier diese Variable brauchen. Die Adresse der L1-Brücke ist nicht im Voraus bekannt.

1
2 /***************
3 * Konstruktor *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Von diesem Vertrag verwendeter domänenübergreifender Messenger.
8 * @param _l1TokenBridge Adresse der auf der Hauptkette bereitgestellten L1-Brücke.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Auszahlung *
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 }
Alles anzeigen

Diese beiden Funktionen leiten Auszahlungen ein. Beachten Sie, dass die L1-Token-Adresse nicht angegeben werden muss. Es wird erwartet, dass L2-Tokens uns die Adresse des L1-Äquivalents mitteilen.

1
2 /**
3 * @dev Führt die Logik für Auszahlungen durch, indem der Token verbrannt und
4 * das L1-Token-Gateway über die Auszahlung informiert wird.
5 * @param _l2Token Adresse des L2-Tokens, bei dem die Auszahlung eingeleitet wird.
6 * @param _from Konto, von dem die Auszahlung auf L2 abgezogen wird.
7 * @param _to Konto, dem die Auszahlung auf L1 gutgeschrieben wird.
8 * @param _amount Betrag des abzuhebenden Tokens.
9 * @param _l1Gas Unbenutzt, aber aus Gründen der potenziellen Vorwärtskompatibilität enthalten.
10 * @param _data Optionale Daten zur Weiterleitung an L1. Diese Daten werden
11 * lediglich als Annehmlichkeit für externe Verträge zur Verfügung gestellt. Abgesehen von der Durchsetzung einer maximalen
12 * Länge geben diese Verträge keine Garantien über ihren Inhalt.
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 // Wenn eine Auszahlung eingeleitet wird, verbrennen wir die Mittel des Auszahlenden, um eine nachfolgende L2-
23 // Nutzung zu verhindern
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Alles anzeigen

Beachten Sie, dass wir uns nicht auf den _from-Parameter verlassen, sondern auf msg.sender, was viel schwerer zu fälschen ist (soweit ich weiß, unmöglich).

1
2 // Konstruiere Calldata für 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) {

Auf L1 ist es notwendig, zwischen ETH und ERC-20 zu unterscheiden.

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 // Nachricht an L1-Brücke senden
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 * Cross-Chain-Funktion: Einzahlung *
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
Alles anzeigen

Diese Funktion wird von L1StandardBridge aufgerufen.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Stellen Sie sicher, dass die Quelle der Nachricht legitim ist. Dies ist wichtig, da diese Funktion _mint aufruft und verwendet werden könnte, um Tokens auszugeben, die nicht durch Tokens gedeckt sind, die die Brücke auf L1 besitzt.

1 // Prüfen Sie, ob der Ziel-Token konform ist und
2 // überprüfen Sie, ob der eingezahlte Token auf L1 mit der L2-Darstellung des eingezahlten Tokens hier übereinstimmt
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Plausibilitätsprüfungen:

  1. Die richtige Schnittstelle wird unterstützt
  2. Die L1-Adresse des L2-ERC-20-Vertrags stimmt mit der L1-Quelle der Tokens überein
1 ) {
2 // Wenn eine Einzahlung abgeschlossen ist, schreiben wir dem Konto auf L2 den gleichen Betrag an
3 // Tokens gut.
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);

Wenn die Plausibilitätsprüfungen erfolgreich sind, schließen Sie die Einzahlung ab:

  1. Prägen Sie die Tokens
  2. Das entsprechende Ereignis auslösen
1 } else {
2 // Entweder ist der L2-Token, in den eingezahlt wird, mit der korrekten Adresse
3 // seines L1-Tokens nicht einverstanden oder er unterstützt nicht die korrekte Schnittstelle.
4 // Dies sollte nur passieren, wenn es einen bösartigen L2-Token gibt oder wenn ein Benutzer irgendwie
5 // die falsche L2-Token-Adresse für die Einzahlung angegeben hat.
6 // In jedem Fall stoppen wir den Prozess hier und erstellen eine Auszahlungsnachricht
7 //, damit Benutzer in einigen Fällen ihre Gelder abheben können.
8 // Es gibt keine Möglichkeit, bösartige Token-Verträge vollständig zu verhindern, aber dies begrenzt
9 // Benutzerfehler und mildert einige Formen von bösartigem Vertragsverhalten.
Alles anzeigen

Wenn ein Benutzer einen erkennbaren Fehler gemacht hat, indem er die falsche L2-Token-Adresse verwendet hat, möchten wir die Einzahlung stornieren und die Tokens auf L1 zurückgeben. Der einzige Weg, wie wir dies von L2 aus tun können, ist das Senden einer Nachricht, die den Fehler-Anfechtungszeitraum abwarten muss, aber das ist für den Benutzer viel besser als der dauerhafte Verlust der Tokens.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // hier _to und _from vertauscht, um die Einzahlung an den Absender zurückzuspringen
6 _from,
7 _amount,
8 _data
9 );
10
11 // Nachricht an die L1-Brücke senden
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}
Alles anzeigen

Fazit

Die Standard-Brücke ist der flexibelste Mechanismus für Asset-Übertragungen. Da er jedoch so generisch ist, ist er nicht immer der einfachste zu verwendende Mechanismus. Insbesondere für Auszahlungen bevorzugen die meisten Benutzer Drittanbieter-Brückenopens in a new tab, die nicht auf den Anfechtungszeitraum warten und keinen Merkle-Beweis benötigen, um die Auszahlung abzuschließen.

Diese Brücken funktionieren typischerweise, indem sie Assets auf L1 haben, die sie sofort gegen eine geringe Gebühr (oft weniger als die Gaskosten für eine Standard-Brückenauszahlung) zur Verfügung stellen. Wenn die Brücke (oder die Personen, die sie betreiben) erwartet, dass sie auf L1 knapp bei Kasse ist, überträgt sie ausreichend Vermögenswerte von L2. Da es sich hierbei um sehr große Auszahlungen handelt, werden die Auszahlungskosten auf einen großen Betrag verteilt und machen einen viel kleineren Prozentsatz aus.

Hoffentlich hat Ihnen dieser Artikel geholfen, besser zu verstehen, wie Layer 2 funktioniert und wie man klaren und sicheren Solidity-Code schreibt.

Hier finden Sie mehr von meiner Arbeitopens in a new tab.

Seite zuletzt aktualisiert: 22. Oktober 2025

War dieses Tutorial hilfreich?