Zum Hauptinhalt springen

Walkthrough des Optimism-Standardvertrags für kettenübergreifende Brücken

Solidity
kettenübergreifende Brücke
Ebene 2
Fortgeschritten
Ori Pomerantz
30. März 2022
33 Minuten Lesezeit

Optimism (opens 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 Ebene 1 oder L1) verarbeiten, da Transaktionen nur von wenigen Blockchain-Knoten anstatt von jedem Blockchain-Knoten im Netzwerk verarbeitet werden. Gleichzeitig werden alle Daten auf L1 geschrieben, sodass alles mit allen Integritäts- und Verfügbarkeitsgarantien des Mainnets bewiesen und rekonstruiert werden kann.

Um L1-Vermögenswerte auf Optimism (oder einer anderen Ebene 2) zu verwenden, müssen die Vermögenswerte über eine kettenübergreifende Brücke übertragen werden. Eine Möglichkeit, dies zu erreichen, besteht darin, dass Benutzer Vermögenswerte (ETH und ERC-20-Token sind die häufigsten) auf L1 sperren und gleichwertige Vermögenswerte zur Verwendung auf L2 erhalten. Letztendlich möchte derjenige, der sie am Ende besitzt, sie vielleicht wieder über eine kettenübergreifende Brücke zurück zu L1 übertragen. Dabei werden die Vermögenswerte auf L2 verbrannt und dann auf L1 wieder an den Benutzer freigegeben.

Auf diese Weise funktioniert die kettenübergreifende Optimism-Standardbrücke (opens in a new tab). In diesem Artikel gehen wir den Quellcode für diese kettenübergreifende Brücke durch, um zu sehen, wie sie funktioniert, und studieren ihn als Beispiel für gut geschriebenen Solidity-Code.

Kontrollflüsse

Die kettenübergreifende Brücke hat zwei Hauptflüsse:

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

Einzahlungsfluss

Ebene 1

  1. Bei der Einzahlung eines ERC-20-Tokens erteilt der Einzahler der kettenübergreifenden Brücke die Erlaubnis (Allowance), den einzuzahlenden Betrag auszugeben.
  2. Der Einzahler ruft die kettenübergreifende L1-Brücke auf (depositERC20, depositERC20To, depositETH oder depositETHTo).
  3. Die kettenübergreifende L1-Brücke übernimmt den Besitz des überbrückten Vermögenswerts.
    • ETH: Der Vermögenswert wird vom Einzahler als Teil des Aufrufs übertragen.
    • ERC-20: Der Vermögenswert wird von der kettenübergreifenden Brücke unter Verwendung der vom Einzahler bereitgestellten Erlaubnis an sich selbst übertragen.
  4. Die kettenübergreifende L1-Brücke verwendet den Cross-Domain-Nachrichtenmechanismus, um finalizeDeposit auf der kettenübergreifenden L2-Brücke aufzurufen.

Ebene 2

  1. Die kettenübergreifende L2-Brücke überprüft, ob der Aufruf von finalizeDeposit legitim ist:
    • Kam vom Cross-Domain-Nachrichtenvertrag.
    • Stammte ursprünglich von der kettenübergreifenden Brücke auf L1.
  2. Die kettenübergreifende L2-Brücke prüft, ob der ERC-20-Token-Vertrag auf L2 der richtige ist:
  3. Wenn der L2-Vertrag der richtige ist, wird er aufgerufen, um die entsprechende Anzahl von Token an die entsprechende Adresse zu prägen. Wenn nicht, wird ein Auszahlungsprozess gestartet, damit der Benutzer die Token auf L1 beanspruchen kann.

Auszahlungsfluss

Ebene 2

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

Ebene 1

  1. Die kettenübergreifende L1-Brücke überprüft, ob der Aufruf von finalizeETHWithdrawal oder finalizeERC20Withdrawal legitim ist:
    • Kam vom Cross-Domain-Nachrichtenmechanismus.
    • Stammte ursprünglich von der kettenübergreifenden Brücke auf L2.
  2. Die kettenübergreifende L1-Brücke überträgt den entsprechenden Vermögenswert (ETH oder ERC-20) an die entsprechende Adresse.

Ebene-1-Code

Dies ist der Code, der auf L1, dem Ethereum-Mainnet, ausgeführt wird.

IL1ERC20Bridge

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

1// SPDX-License-Identifier: MIT

Der Großteil des Codes von Optimism ist unter der MIT-Lizenz veröffentlicht (opens 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 der Version 0.9.0 wissen wir nicht, ob dieser Code damit kompatibel ist oder nicht.

1/* *
2 * @title IL1ERC20Bridge */
3
4
5
6interface IL1ERC20Bridge {
7 /* *********
8 * Ereignisse *
9 ********* */
10
11
12
13
14 event ERC20DepositInitiated(
Alle anzeigen

In der Terminologie der kettenübergreifenden Brücke von Optimism bedeutet Einzahlung (deposit) eine Übertragung von L1 zu L2 und Auszahlung (withdrawal) eine Übertragung von L2 zu 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 einsehen (opens in a new tab). Die Adresse mit der chainId 1 befindet sich auf L1 (Mainnet) und die Adresse mit der chainId 10 befindet sich auf L2 (Optimism). Die anderen beiden chainId-Werte sind für das Kovan-Testnet (42) und das Optimistic Kovan-Testnet (69).

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

Es ist möglich, Übertragungen Notizen hinzuzufügen. In diesem Fall werden sie 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 Vertrag für die kettenübergreifende Brücke verarbeitet Übertragungen in beide Richtungen. Im Falle der kettenübergreifenden L1-Brücke bedeutet dies die Initialisierung von Einzahlungen und die Finalisierung von Auszahlungen.

1
2 /* *******************
3 * Öffentliche Funktionen *
4 ******************* */
5
6
7
8
9 /* *
10 * @dev Ruft die Adresse des entsprechenden L2-Vertrags für die kettenübergreifende Brücke ab.
11 * @return Adresse des entsprechenden L2-Vertrags für die kettenübergreifende Brücke. */
12
13
14
15
16 function l2TokenBridge() external returns (address);
Alle anzeigen

Diese Funktion wird nicht wirklich benötigt, da es sich auf L2 um einen vorab bereitgestellten Vertrag handelt, sodass er sich immer an der Adresse 0x4200000000000000000000000000000000000010 befindet. Sie ist hier aus Symmetriegründen mit der kettenübergreifenden L2-Brücke vorhanden, da die Adresse der kettenübergreifenden L1-Brücke nicht trivial zu ermitteln ist.

1 /* *
2 * @dev Zahlt einen Betrag des ERC20 auf das Guthaben des Aufrufers auf L2 ein.
3 * @param _l1Token Adresse des L1-ERC20, den wir einzahlen
4 * @param _l2Token Adresse des entsprechenden L2-ERC20 zum L1
5 * @param _amount Betrag des einzuzahlenden ERC20
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 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
9 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
10
11
12
13
14
15
16
17
18
19
20 function depositERC20(
21 address _l1Token,
22 address _l2Token,
23 uint256 _amount,
24 uint32 _l2Gas,
25 bytes calldata _data
26 ) external;
Alle anzeigen

Der Parameter _l2Gas ist die Menge an L2-Gas, die die Transaktion ausgeben darf. Bis zu einem bestimmten (hohen) Limit ist dies kostenlos (opens in a new tab). Solange der ERC-20-Vertrag beim Prägen also nichts wirklich Seltsames tut, sollte dies kein Problem sein. Diese Funktion kümmert sich um das häufige Szenario, bei dem ein Benutzer Vermögenswerte über eine kettenübergreifende Brücke an dieselbe Adresse auf einer anderen Blockchain überträgt.

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 zum L1
5 * @param _to L2-Adresse, der die Abhebung gutgeschrieben werden soll.
6 * @param _amount Betrag des einzuzahlenden 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 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
10 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
11
12
13
14
15
16
17
18
19
20
21
22 function depositERC20To(
23 address _l1Token,
24 address _l2Token,
25 address _to,
26 uint256 _amount,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external;
Alle anzeigen

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

1 /* ************************
2 * Kettenübergreifende Funktionen *
3 ************************ */
4
5
6
7
8 /* *
9 * @dev Schließt eine Abhebung von L2 nach L1 ab und schreibt das Guthaben dem L1-ERC20-Token-Guthaben des Empfängers gut.
10 * Dieser Aufruf schlägt fehl, wenn die initiierte Abhebung von L2 nicht abgeschlossen wurde.
11 *
12 * @param _l1Token Adresse des L1-Tokens, für den finalizeWithdrawal ausgeführt wird.
13 * @param _l2Token Adresse des L2-Tokens, bei dem die Abhebung initiiert wurde.
14 * @param _from L2-Adresse, die den Transfer initiiert.
15 * @param _to L1-Adresse, der die Abhebung gutgeschrieben werden soll.
16 * @param _amount Betrag des einzuzahlenden ERC20.
17 * @param _data Daten, die vom Sender auf L2 bereitgestellt werden. Diese Daten werden
18 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
19 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 function finalizeERC20Withdrawal(
35 address _l1Token,
36 address _l2Token,
37 address _from,
38 address _to,
39 uint256 _amount,
40 bytes calldata _data
41 ) external;
42}
Alle anzeigen

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

  1. Eine initiierende Transaktion auf L2.
  2. Eine abschließende oder beanspruchende Transaktion auf L1. Diese Transaktion muss stattfinden, nachdem die Fehleranfechtungsfrist (Fault Challenge Period) (opens in a new tab) für die L2-Transaktion abgelaufen ist.

IL1StandardBridge

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

Die Schnittstelle der kettenübergreifenden Brücke ist auf zwei Dateien aufgeteilt, da einige ERC-20-Token eine benutzerdefinierte Verarbeitung erfordern und nicht von der kettenübergreifenden Standardbrücke verarbeitet werden können. Auf diese Weise kann die benutzerdefinierte kettenübergreifende 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
9
10
11interface IL1StandardBridge is IL1ERC20Bridge {
12 /* *********
13 * Ereignisse *
14 ********* */
15
16
17
18 event ETHDepositInitiated(
19 address indexed _from,
20 address indexed _to,
21 uint256 _amount,
22 bytes _data
23 );
Alle anzeigen

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

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /* *******************
8 * Öffentliche Funktionen *
9 ******************* */
10
11
12
13
14 /* *
15 * @dev Zahlt einen Betrag an ETH auf das Guthaben des Aufrufers auf L2 ein.
16 .
17 .
18 . */
19
20
21
22
23
24
25 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
26
27 /* *
28 * @dev Zahlt einen Betrag an ETH auf das Guthaben eines Empfängers auf L2 ein.
29 .
30 .
31 . */
32
33
34
35
36
37
38 function depositETHTo(
39 address _to,
40 uint32 _l2Gas,
41 bytes calldata _data
42 ) external payable;
43
44 /* ************************
45 * Kettenübergreifende Funktionen *
46 ************************ */
47
48
49
50
51 /* *
52 * @dev Schließt eine Abhebung von L2 nach L1 ab und schreibt das Guthaben dem L1-ETH-Token-Guthaben des Empfängers gut. Da nur der xDomainMessenger diese Funktion aufrufen kann, wird sie niemals aufgerufen,
53 * bevor die Abhebung abgeschlossen ist.
54 .
55 .
56 . */
57
58
59
60
61
62
63
64
65 function finalizeETHWithdrawal(
66 address _from,
67 address _to,
68 uint256 _amount,
69 bytes calldata _data
70 ) external;
71}
Alle anzeigen

CrossDomainEnabled

Dieser Vertrag (opens in a new tab) wird von beiden kettenübergreifenden Brücken (L1 und L2) geerbt, um Nachrichten an die andere Ebene zu senden.

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

Diese Schnittstelle (opens in a new tab) teilt dem Vertrag mit, wie Nachrichten unter Verwendung des Cross-Domain-Messengers an die andere Ebene gesendet werden. Dieser Cross-Domain-Messenger ist ein völlig 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 den erbenden Vertrag */
6
7
8
9
10
11
12contract CrossDomainEnabled {
13 /* ************
14 * Variablen *
15 ************ */
16
17
18
19
20 // Messenger-Vertrag, der zum Senden und Empfangen von Nachrichten aus der anderen Domäne verwendet wird.
21 address public messenger;
22
23 /* **************
24 * Konstruktor *
25 ************** */
26
27
28
29
30 /* *
31 * @param _messenger Adresse des CrossDomainMessenger auf der aktuellen Ebene. */
32
33
34
35 constructor(address _messenger) {
36 messenger = _messenger;
37 }
Alle anzeigen

Der einzige Parameter, den der Vertrag kennen muss, ist die Adresse des Cross-Domain-Messengers auf dieser Ebene. Dieser Parameter wird einmal im Konstruktor festgelegt und ändert sich nie.

1
2 /* *********************
3 * Funktionsmodifikatoren *
4 ********************* */
5
6
7
8
9 /* *
10 * Erzwingt, dass die modifizierte Funktion nur von einem bestimmten domänenübergreifenden Konto aufgerufen werden kann.
11 * @param _sourceDomainAccount Das einzige Konto in der Ursprungsdomäne, das
12 * authentifiziert ist, diese Funktion aufzurufen. */
13
14
15
16
17
18 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Alle anzeigen

Das Cross-Domain-Messaging ist für jeden Vertrag auf der Blockchain zugänglich, auf der es ausgeführt wird (entweder Ethereum-Mainnet oder Optimism). Wir benötigen jedoch, dass die kettenübergreifende Brücke auf jeder Seite nur bestimmten Nachrichten vertraut, wenn sie von der kettenübergreifenden Brücke auf der anderen Seite stammen.

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 an die andere Ebene 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 Informationen bereitstellen.

Wir müssen sicherstellen, dass die empfangene Nachricht von der anderen kettenübergreifenden Brücke stammt.

1
2 _;
3 }
4
5 /* *********************
6 * Interne Funktionen *
7 ********************* */
8
9
10
11
12 /* *
13 * Ruft den Messenger ab, normalerweise aus dem Speicher. Diese Funktion wird offengelegt, falls ein untergeordneter Vertrag
14 * sie überschreiben muss.
15 * @return Die Adresse des domänenübergreifenden Messenger-Vertrags, der verwendet werden soll. */
16
17
18
19
20
21 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
22 return ICrossDomainMessenger(messenger);
23 }
Alle anzeigen

Diese Funktion gibt den Cross-Domain-Messenger zurück. Wir verwenden eine Funktion anstelle der Variablen messenger, um Verträgen, die von diesem erben, die Verwendung eines Algorithmus zu ermöglichen, mit dem angegeben wird, 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 für eine Funktion mit
6 * `onlyFromCrossDomainAccount()`)
7 * @param _gasLimit Das Gaslimit für den Empfang der Nachricht in der Zieldomäne. */
8
9
10
11
12
13
14
15 function sendCrossDomainMessage(
16 address _crossDomainTarget,
17 uint32 _gasLimit,
18 bytes memory _message
Alle anzeigen

Schließlich die Funktion, die eine Nachricht an die andere Ebene sendet.

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

Slither (opens 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-Ereignisse (opens in a new tab)
  2. Gutartige Reentrancy (opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

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

Der L1-Brückenvertrag

Der Quellcode für diesen Vertrag ist hier zu finden (opens 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 kettenübergreifende Brücke selbst ist unser Vertrag, und wir können streng sein, welche Solidity-Version sie verwendet.

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

IL1ERC20Bridge und IL1StandardBridge werden oben erklärt.

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

Diese Schnittstelle (opens in a new tab) ermöglicht es uns, Nachrichten zur Steuerung der kettenübergreifenden Standardbrücke auf L2 zu erstellen.

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

Diese Schnittstelle (opens 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 */
2/* Library Imports */
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Wie oben erklärt, wird dieser Vertrag für das Messaging zwischen den Ebenen verwendet.

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

Lib_PredeployAddresses (opens in a new tab) enthält die Adressen für die L2-Verträge, die immer dieselbe Adresse haben. Dies schließt die kettenübergreifende Standardbrücke auf L2 ein.

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

OpenZeppelins Address-Dienstprogramme (opens in a new tab). Sie werden verwendet, um zwischen Vertragsadressen und solchen zu unterscheiden, die zu extern verwalteten Konten (Externally Owned Accounts, 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. Zumindest können wir so jedoch einige häufige Benutzerfehler identifizieren und verhindern.

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

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

  1. Revert (Rückgängig machen)
  2. Rückgabe von false

Die Behandlung beider Fälle würde unseren Code komplizierter machen. Stattdessen verwenden wir OpenZeppelins SafeERC20 (opens in a new tab), was sicherstellt, dass alle Fehler zu einem Revert führen (opens in a new tab).

1/* *
2 * @title L1StandardBridge
3 * @dev Die L1-ETH- und ERC20-kettenübergreifende Brücke ist ein Vertrag, der eingezahlte L1-Guthaben und Standard-Token speichert,
4 * die auf L2 verwendet werden. Er synchronisiert eine entsprechende L2-kettenübergreifende Brücke, informiert sie über Einzahlungen
5 * und lauscht auf neu abgeschlossene Abhebungen.
6 * */
7
8
9
10
11
12
13
14contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
15 using SafeERC20 for IERC20;
Alle anzeigen

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

1
2 /* *******************************
3 * Externe Vertragsreferenzen *
4 ******************************* */
5
6
7
8
9 address public l2TokenBridge;
Alle anzeigen

Die Adresse der L2StandardBridge.

1
2 // Ordnet L1-Token dem L2-Token und dem Guthaben des eingezahlten L1-Tokens zu
3 mapping(address => mapping(address => uint256)) public deposits;

Ein doppeltes Mapping (opens in a new tab) wie dieses ist die Art und Weise, wie Sie ein zweidimensionales spärliches Array (Sparse Array) (opens in a new tab) definieren. Werte in dieser Datenstruktur werden als deposit[L1 token addr][L2 token addr] 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
7
8
9 // Dieser Vertrag befindet sich hinter einem Proxy, daher bleiben die Konstruktorparameter ungenutzt.
10 constructor() CrossDomainEnabled(address(0)) {}
Alle anzeigen

Wir möchten in der Lage sein, diesen Vertrag zu aktualisieren, ohne alle Variablen im Speicher kopieren zu müssen. Dazu verwenden wir einen Proxy (opens in a new tab), einen Vertrag, der delegatecall (opens in a new tab) verwendet, um Aufrufe an einen separaten Vertrag zu übertragen, dessen Adresse vom Proxy-Vertrag gespeichert wird (beim Upgrade 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 Konstruktor CrossDomainEnabled einen unsinnigen Wert übergeben können. Es ist auch der Grund, warum die unten stehende Initialisierung vom Konstruktor getrennt ist.

1 /* *****************
2 * Initialisierung *
3 ***************** */
4
5
6
7
8 /* *
9 * @param _l1messenger L1-Messenger-Adresse, die für kettenübergreifende Kommunikation verwendet wird.
10 * @param _l2TokenBridge Adresse der L2-Standard-kettenübergreifenden Brücke. */
11
12
13
14
15 // slither-disable-next-line external-function
Alle anzeigen

Dieser Slither-Test (opens in a new tab) identifiziert Funktionen, die nicht aus dem Vertragscode aufgerufen werden und daher als external anstatt als public deklariert werden könnten. Die Gaskosten von external-Funktionen können niedriger sein, da sie mit Parametern in den Calldata versehen werden können. Funktionen, die als public deklariert sind, müssen aus dem Vertrag heraus zugänglich sein. Verträge können ihre eigenen Calldata nicht ändern, daher müssen sich die Parameter im Arbeitsspeicher (Memory) befinden. Wenn eine solche Funktion extern aufgerufen wird, ist es notwendig, die Calldata in den Arbeitsspeicher 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), "Contract has already been initialized.");

Die Funktion initialize sollte nur einmal aufgerufen werden. Wenn sich die Adresse des L1-Cross-Domain-Messengers oder der kettenübergreifenden L2-Token-Brücke ändert, erstellen wir einen neuen Proxy und eine neue kettenübergreifende Brücke, die ihn aufruft. Dies ist unwahrscheinlich, außer wenn das gesamte System aktualisiert wird, was ein sehr seltenes Ereignis ist.

Beachten Sie, dass diese Funktion keinen Mechanismus hat, der einschränkt, wer sie aufrufen kann. Dies bedeutet, dass ein Angreifer theoretisch warten könnte, bis wir den Proxy und die erste Version der kettenübergreifenden Brücke bereitstellen, und dann Front-Running (opens in a new tab) betreiben könnte, um vor dem legitimen Benutzer zur Funktion initialize zu gelangen. Es gibt jedoch zwei Methoden, um dies zu verhindern:

  1. Wenn die Verträge nicht direkt von einem EOA (Extern verwaltetes Konto), sondern in einer Transaktion bereitgestellt werden, bei der ein anderer Vertrag sie erstellt (opens 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 kettenübergreifende Brücke zu ignorieren und neue zu erstellen.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

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

1
2 /* *************
3 * Einzahlen *
4 ************* */
5
6
7
8
9 /* * @dev Modifikator, der erfordert, dass der Sender ein EOA ist. Diese Prüfung könnte von einem bösartigen
10 * Vertrag über Initcode umgangen werden, aber sie kümmert sich um den Benutzerfehler, den wir vermeiden wollen. */
11
12
13
14 modifier onlyEOA() {
15 // Wird verwendet, um Einzahlungen von Verträgen zu stoppen (vermeidet versehentlich verlorene Token)
16 require(!Address.isContract(msg.sender), "Account not EOA");
17 _;
18 }
Alle 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 an ETH auf das Guthaben des Aufrufers auf L2 einzuzahlen.
4 * Da die Receive-Funktion keine Daten annimmt, wird ein konservativer
5 * Standardbetrag an L2 weitergeleitet. */
6
7
8
9
10
11
12 receive() external payable onlyEOA {
13 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
14 }
Alle 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
5
6 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
7 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
8 }
9
10 /* *
11 * @inheritdoc IL1StandardBridge */
12
13
14
15 function depositETHTo(
16 address _to,
17 uint32 _l2Gas,
18 bytes calldata _data
19 ) external payable {
20 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
21 }
Alle anzeigen

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

1 /* *
2 * @dev Führt die Logik für Einzahlungen aus, indem die ETH gespeichert und das L2-ETH-Gateway über
3 * die Einzahlung informiert wird.
4 * @param _from Konto, von dem die Einzahlung auf L1 eingezogen 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 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
9 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
10
11
12
13
14
15
16
17
18
19
20 function _initiateETHDeposit(
21 address _from,
22 address _to,
23 uint32 _l2Gas,
24 bytes memory _data
25 ) internal {
26 // Erstellt Calldata für den finalizeDeposit-Aufruf
27 bytes memory message = abi.encodeWithSelector(
Alle anzeigen

Die Art und Weise, wie Cross-Domain-Nachrichten funktionieren, besteht darin, dass der Zielvertrag mit der Nachricht als seinen Calldata aufgerufen wird. Solidity-Verträge interpretieren ihre Calldata immer in Übereinstimmung mit den ABI-Spezifikationen (opens in a new tab). Die Solidity-Funktion abi.encodeWithSelector (opens 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 besteht darin, die Funktion finalizeDeposit (opens in a new tab) mit diesen Parametern aufzurufen:

ParameterValueMeaning
_l1Tokenaddress(0)Spezieller Wert, der für ETH (das kein ERC-20-Token ist) auf L1 steht
_l2TokenLib_PredeployAddresses.OVM_ETHDer L2-Vertrag, der ETH auf Optimism verwaltet, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (dieser Vertrag ist nur für die interne Nutzung durch Optimism bestimmt)
_from_fromDie Adresse auf L1, die die ETH sendet
_to_toDie Adresse auf L2, die die ETH empfängt
amountmsg.valueMenge der gesendeten Wei (die bereits an die kettenübergreifende Brücke gesendet wurden)
_data_dataZusätzliche Daten, die der Einzahlung beigefügt werden sollen
1 // Sendet Calldata an 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 }

Geben Sie ein Ereignis aus, um jede dezentralisierte Anwendung, die zuhört, über diese Übertragung zu informieren.

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

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

1 /* *
2 * @dev Führt die Logik für Einzahlungen aus, indem der L2-Deposited-Token-Vertrag
3 * über die Einzahlung informiert wird und ein Handler aufgerufen wird, um die L1-Guthaben zu sperren. (z. B. transferFrom)
4 *
5 * @param _l1Token Adresse des L1-ERC20, den wir einzahlen
6 * @param _l2Token Adresse des entsprechenden L2-ERC20 zum L1
7 * @param _from Konto, von dem die Einzahlung auf L1 eingezogen wird
8 * @param _to Konto, dem die Einzahlung auf L2 gutgeschrieben wird
9 * @param _amount Betrag des einzuzahlenden 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 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
13 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 function _initiateERC20Deposit(
29 address _l1Token,
30 address _l2Token,
31 address _from,
32 address _to,
33 uint256 _amount,
34 uint32 _l2Gas,
35 bytes calldata _data
36 ) internal {
Alle anzeigen

Diese Funktion ähnelt _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 Falle von ETH beinhaltet der Aufruf der kettenübergreifenden Brücke bereits die Übertragung des Vermögenswerts auf das Brückenkonto (msg.value).

1 // Wenn eine Einzahlung auf L1 initiiert wird, überträgt die L1-kettenübergreifende Brücke die Guthaben an sich selbst für zukünftige
2 // Abhebungen. safeTransferFrom prüft auch, ob der Vertrag Code hat, daher schlägt dies fehl, 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);

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

  1. Der Benutzer (_from) erteilt der kettenübergreifenden Brücke die Erlaubnis (Allowance), die entsprechenden Token zu übertragen.
  2. Der Benutzer ruft die kettenübergreifende Brücke mit der Adresse des Token-Vertrags, dem Betrag usw. auf.
  3. Die kettenübergreifende Brücke überträgt die Token (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 Parameter _from aufrufen.

1 // Erstellt 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 // Sendet Calldata an 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;
Alle anzeigen

Fügen Sie den eingezahlten Token-Betrag zur Datenstruktur deposits hinzu. Es könnte mehrere Adressen auf L2 geben, die demselben L1-ERC-20-Token entsprechen, daher reicht es nicht aus, den Saldo der kettenübergreifenden Brücke des L1-ERC-20-Tokens zu verwenden, um Einzahlungen zu verfolgen.

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

Die kettenübergreifende L2-Brücke sendet eine Nachricht an den L2-Cross-Domain-Messenger, was dazu führt, dass der L1-Cross-Domain-Messenger diese Funktion aufruft (natürlich erst, sobald die Transaktion, die die Nachricht abschließt (opens 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 stammt und von der kettenübergreifenden L2-Token-Brücke ausgeht. Diese Funktion wird verwendet, um ETH von der kettenübergreifenden 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));

Der Weg zur Übertragung von ETH besteht darin, den Empfänger mit der Menge an Wei in 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);

Geben Sie ein Ereignis über die Auszahlung aus.

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

Diese Funktion ähnelt finalizeETHWithdrawal oben, mit den notwendigen Änderungen für ERC-20-Token.

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

Aktualisieren Sie die Datenstruktur deposits.

1
2 // Wenn eine Abhebung auf L1 abgeschlossen ist, überträgt die L1-kettenübergreifende Brücke die Guthaben an den Abhebenden
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 * Temporär - ETH migrieren *
13 **************************** */
14
15
16
17
18 /* *
19 * @dev Fügt dem Konto ETH-Guthaben hinzu. Dies soll ermöglichen, dass ETH
20 * von einem alten Gateway zu einem neuen Gateway migriert werden.
21 * HINWEIS: Dies wird nur für ein Upgrade belassen, damit wir die migrierten ETH aus dem
22 * alten Vertrag empfangen können */
23
24
25
26
27
28
29 function donateETH() external payable {}
30}
Alle anzeigen

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

ERC-20-Token auf L2

Damit ein ERC-20-Token in die kettenübergreifende Standardbrücke passt, muss er es der kettenübergreifenden Standardbrücke und nur der kettenübergreifenden Standardbrücke erlauben, Token zu prägen. Dies ist notwendig, da die kettenübergreifenden Brücken sicherstellen müssen, dass die Anzahl der auf Optimism zirkulierenden Token der Anzahl der im Vertrag der kettenübergreifenden L1-Brücke gesperrten Token entspricht. Wenn es zu viele Token auf L2 gibt, könnten einige Benutzer ihre Vermögenswerte nicht über die kettenübergreifende Brücke zurück zu L1 übertragen. Anstelle einer vertrauenswürdigen kettenübergreifenden Brücke würden wir im Wesentlichen das Mindestreservesystem (Fractional Reserve Banking) (opens in a new tab) nachbilden. Wenn es zu viele Token auf L1 gibt, würden einige dieser Token für immer im Brückenvertrag gesperrt bleiben, da es keine Möglichkeit gibt, sie freizugeben, ohne L2-Token zu verbrennen.

IL2StandardERC20

Jeder ERC-20-Token auf L2, der die kettenübergreifende Standardbrücke verwendet, muss diese Schnittstelle (opens in a new tab) bereitstellen, die über die Funktionen und Ereignisse verfügt, die die kettenübergreifende 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-Schnittstelle (opens in a new tab) enthält nicht die Funktionen mint und burn. Diese Methoden werden vom ERC-20-Standard (opens in a new tab) nicht verlangt, der die Mechanismen zum Erstellen und Zerstören von Token unspezifiziert lässt.

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

Die ERC-165-Schnittstelle (opens in a new tab) wird verwendet, um anzugeben, welche Funktionen ein Vertrag bereitstellt. Sie können den Standard hier lesen (opens in a new tab).

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

Diese Funktion liefert die Adresse des L1-Tokens, der über eine kettenübergreifende Brücke mit diesem Vertrag verbunden ist. Beachten Sie, dass wir keine ähnliche Funktion in der entgegengesetzten Richtung haben. Wir müssen in der Lage sein, jeden L1-Token über eine kettenübergreifende Brücke zu übertragen, unabhängig davon, ob bei seiner Implementierung 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 Token. Die kettenübergreifende Brücke sollte die einzige Entität sein, die diese Funktionen ausführen kann, um sicherzustellen, dass die Anzahl der Token korrekt ist (gleich der Anzahl der auf L1 gesperrten Token).

L2StandardERC20

Dies ist unsere Implementierung der Schnittstelle IL2StandardERC20 (opens 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-Vertrag (opens in a new tab). Optimism glaubt nicht daran, das Rad neu zu erfinden, insbesondere 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 benötigt.

1
2 /* *
3 * @param _l2Bridge Adresse der L2-Standard-kettenübergreifenden Brücke.
4 * @param _l1Token Adresse des entsprechenden L1-Tokens.
5 * @param _name ERC20-Name.
6 * @param _symbol ERC20-Symbol. */
7
8
9
10
11
12
13 constructor(
14 address _l2Bridge,
15 address _l1Token,
16 string memory _name,
17 string memory _symbol
18 ) ERC20(_name, _symbol) {
19 l1Token = _l1Token;
20 l2Bridge = _l2Bridge;
21 }
Alle anzeigen

Rufen Sie zuerst den Konstruktor für den Vertrag auf, von dem wir erben (ERC20(_name, _symbol)), und legen Sie dann unsere eigenen Variablen fest.

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

So funktioniert ERC-165 (opens in a new tab). Jede Schnittstelle ist eine Anzahl unterstützter Funktionen und wird als das exklusive Oder (XOR) (opens in a new tab) der ABI-Funktionsselektoren (opens in a new tab) dieser Funktionen identifiziert.

Die kettenübergreifende L2-Brücke verwendet ERC-165 als Plausibilitätsprüfung (Sanity Check), um sicherzustellen, dass der ERC-20-Vertrag, an den sie Vermögenswerte sendet, ein IL2StandardERC20 ist.

Hinweis: Es gibt nichts, was einen bösartigen Vertrag daran hindert, falsche Antworten auf supportsInterface zu geben. Dies ist also 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}
Alle anzeigen

Nur die kettenübergreifende L2-Brücke darf Vermögenswerte prägen und verbrennen.

_mint und _burn sind tatsächlich im OpenZeppelin-ERC-20-Vertrag definiert. Dieser Vertrag macht sie nur nicht nach außen hin zugänglich, da die Bedingungen zum Prägen und Verbrennen von Token so vielfältig sind wie die Anzahl der Möglichkeiten, ERC-20 zu verwenden.

Code der kettenübergreifenden L2-Brücke

Dies ist der Code, der die kettenübergreifende Brücke auf Optimism ausführt. Die Quelle für diesen Vertrag finden Sie hier (opens in a new tab).

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

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

  1. Auf L1 initiieren Sie Einzahlungen und schließen Auszahlungen ab. Hier initiieren Sie Auszahlungen und schließen Einzahlungen ab.
  2. Auf L1 ist es notwendig, zwischen ETH und ERC-20-Token zu unterscheiden. Auf L2 können wir für beides dieselben Funktionen verwenden, da ETH-Guthaben auf Optimism intern als ERC-20-Token mit der Adresse 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab) behandelt werden.
1/* Bibliotheks-Importe */
2/* Library Imports */
3import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
4import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
5import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
6
7/* Vertrags-Importe */
8/* Contract Imports */
9import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
10
11/* *
12 * @title L2StandardBridge
13 * @dev Die L2-Standard-kettenübergreifende Brücke ist ein Vertrag, der mit der L1-Standard-kettenübergreifenden Brücke zusammenarbeitet, um
14 * ETH- und ERC20-Übergänge zwischen L1 und L2 zu ermöglichen.
15 * Dieser Vertrag fungiert als Präger für neue Token, wenn er von Einzahlungen in die L1-Standard-kettenübergreifende
16 * Brücke erfährt.
17 * Dieser Vertrag fungiert auch als Verbrenner der für die Abhebung vorgesehenen Token und informiert die L1-kettenübergreifende
18 * Brücke, L1-Guthaben freizugeben. */
19
20
21
22
23
24
25
26
27
28contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
29 /* *******************************
30 * Externe Vertragsreferenzen *
31 ******************************* */
32
33
34
35
36 address public l1TokenBridge;
Alle anzeigen

Behalten Sie die Adresse der kettenübergreifenden L1-Brücke im Auge. Beachten Sie, dass wir im Gegensatz zum L1-Äquivalent diese Variable hier benötigen. Die Adresse der kettenübergreifenden L1-Brücke ist im Voraus nicht bekannt.

1
2 /* **************
3 * Konstruktor *
4 ************** */
5
6
7
8
9 /* *
10 * @param _l2CrossDomainMessenger Domänenübergreifender Messenger, der von diesem Vertrag verwendet wird.
11 * @param _l1TokenBridge Adresse der L1-kettenübergreifenden Brücke, die auf der Hauptchain bereitgestellt ist. */
12
13
14
15
16 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
17 CrossDomainEnabled(_l2CrossDomainMessenger)
18 {
19 l1TokenBridge = _l1TokenBridge;
20 }
21
22 /* **************
23 * Abheben *
24 ************** */
25
26
27
28
29 /* *
30 * @inheritdoc IL2ERC20Bridge */
31
32
33
34 function withdraw(
35 address _l2Token,
36 uint256 _amount,
37 uint32 _l1Gas,
38 bytes calldata _data
39 ) external virtual {
40 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
41 }
42
43 /* *
44 * @inheritdoc IL2ERC20Bridge */
45
46
47
48 function withdrawTo(
49 address _l2Token,
50 address _to,
51 uint256 _amount,
52 uint32 _l1Gas,
53 bytes calldata _data
54 ) external virtual {
55 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
56 }
Alle anzeigen

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

1
2 /* *
3 * @dev Führt die Logik für Abhebungen aus, indem der Token verbrannt und
4 * das L1-Token-Gateway über die Abhebung informiert wird.
5 * @param _l2Token Adresse des L2-Tokens, bei dem die Abhebung initiiert wird.
6 * @param _from Konto, von dem die Abhebung auf L2 eingezogen wird.
7 * @param _to Konto, dem die Abhebung auf L1 gutgeschrieben wird.
8 * @param _amount Betrag des abzuhebenden Tokens.
9 * @param _l1Gas Unbenutzt, aber für mögliche zukünftige Kompatibilitätsüberlegungen enthalten.
10 * @param _data Optionale Daten zur Weiterleitung an L1. Diese Daten werden
11 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen
12 * Länge geben diese Verträge keine Garantien über deren Inhalt. */
13
14
15
16
17
18
19
20
21
22
23
24
25 function _initiateWithdrawal(
26 address _l2Token,
27 address _from,
28 address _to,
29 uint256 _amount,
30 uint32 _l1Gas,
31 bytes calldata _data
32 ) internal {
33 // Wenn eine Abhebung initiiert wird, verbrennen wir die Guthaben des Abhebenden, um eine spätere L2-
34 // Nutzung zu verhindern
35 // slither-disable-next-line reentrancy-events
36 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Alle anzeigen

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

1
2 // Erstellt 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 // Sendet Nachricht hoch zur L1-kettenübergreifenden Brücke
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 * Kettenübergreifende Funktion: Einzahlen *
30 *********************************** */
31
32
33
34
35 /* *
36 * @inheritdoc IL2ERC20Bridge */
37
38
39
40 function finalizeDeposit(
41 address _l1Token,
42 address _l2Token,
43 address _from,
44 address _to,
45 uint256 _amount,
46 bytes calldata _data
Alle 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 Token auszugeben, die nicht durch Token gedeckt sind, die die kettenübergreifende Brücke auf L1 besitzt.

1 // Prüft, ob der Ziel-Token konform ist und
2 // verifiziert, dass der eingezahlte Token auf L1 mit der hier eingezahlten L2-Token-Repräsentation übereinstimmt
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Plausibilitätsprüfungen (Sanity Checks):

  1. Die richtige Schnittstelle wird unterstützt.
  2. Die L1-Adresse des L2-ERC-20-Vertrags stimmt mit der L1-Quelle der Token überein.
1 ) {
2 // Wenn eine Einzahlung abgeschlossen ist, schreiben wir dem Konto auf L2 den gleichen Betrag an
3 // Token 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 Token.
  2. Geben Sie das entsprechende Ereignis aus.
1 } else {
2 // Entweder ist der L2-Token, in den eingezahlt wird, nicht mit der korrekten Adresse
3 // seines L1-Tokens 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 beiden Fällen stoppen wir den Prozess hier und erstellen eine Abhebungs-
7 // Nachricht, damit Benutzer in einigen Fällen ihre Guthaben herausbekommen 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 ab.
Alle 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 Token auf L1 zurückgeben. Die einzige Möglichkeit, dies von L2 aus zu tun, besteht darin, eine Nachricht zu senden, die die Fehleranfechtungsfrist abwarten muss. Dies ist jedoch für den Benutzer viel besser, als die Token dauerhaft zu verlieren.

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

Fazit

Die kettenübergreifende Standardbrücke ist der flexibelste Mechanismus für Vermögensübertragungen. Da sie jedoch so generisch ist, ist sie nicht immer der am einfachsten zu verwendende Mechanismus. Insbesondere für Auszahlungen ziehen es die meisten Benutzer vor, kettenübergreifende Brücken von Drittanbietern (opens in a new tab) zu verwenden, die nicht auf die Anfechtungsfrist warten und keinen Merkle-Proof benötigen, um die Auszahlung abzuschließen.

Diese kettenübergreifenden Brücken funktionieren in der Regel so, dass sie über Vermögenswerte auf L1 verfügen, die sie gegen eine geringe Gebühr (oft weniger als die Gaskosten für eine Auszahlung über eine kettenübergreifende Standardbrücke) sofort bereitstellen. Wenn die kettenübergreifende Brücke (oder die Personen, die sie betreiben) voraussieht, dass L1-Vermögenswerte knapp werden, überträgt sie ausreichende Vermögenswerte von L2. Da es sich um sehr große Auszahlungen handelt, werden die Auszahlungskosten über einen großen Betrag amortisiert und machen einen viel geringeren Prozentsatz aus.

Hoffentlich hat Ihnen dieser Artikel geholfen, mehr darüber zu verstehen, wie Ebene 2 funktioniert und wie man Solidity-Code schreibt, der klar und sicher ist.

Weitere meiner Arbeiten finden Sie hier (opens in a new tab).

Letzte Aktualisierung der Seite: 22. Oktober 2025

War dieses Tutorial hilfreich?