Walkthrough des Optimism-Standardvertrags für kettenübergreifende Brücken
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
- Bei der Einzahlung eines ERC-20-Tokens erteilt der Einzahler der kettenübergreifenden Brücke die Erlaubnis (Allowance), den einzuzahlenden Betrag auszugeben.
- Der Einzahler ruft die kettenübergreifende L1-Brücke auf (
depositERC20,depositERC20To,depositETHoderdepositETHTo). - 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.
- Die kettenübergreifende L1-Brücke verwendet den Cross-Domain-Nachrichtenmechanismus, um
finalizeDepositauf der kettenübergreifenden L2-Brücke aufzurufen.
Ebene 2
- Die kettenübergreifende L2-Brücke überprüft, ob der Aufruf von
finalizeDepositlegitim ist:- Kam vom Cross-Domain-Nachrichtenvertrag.
- Stammte ursprünglich von der kettenübergreifenden Brücke auf L1.
- Die kettenübergreifende L2-Brücke prüft, ob der ERC-20-Token-Vertrag auf L2 der richtige ist:
- Der L2-Vertrag meldet, dass sein L1-Gegenstück dasselbe ist wie das, von dem die Token auf L1 stammten.
- Der L2-Vertrag meldet, dass er die richtige Schnittstelle unterstützt (unter Verwendung von ERC-165 (opens in a new tab)).
- 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
- Der Auszahlende ruft die kettenübergreifende L2-Brücke auf (
withdrawoderwithdrawTo). - Die kettenübergreifende L2-Brücke verbrennt die entsprechende Anzahl von Token, die
msg.sendergehören. - Die kettenübergreifende L2-Brücke verwendet den Cross-Domain-Nachrichtenmechanismus, um
finalizeETHWithdrawaloderfinalizeERC20Withdrawalauf der kettenübergreifenden L1-Brücke aufzurufen.
Ebene 1
- Die kettenübergreifende L1-Brücke überprüft, ob der Aufruf von
finalizeETHWithdrawaloderfinalizeERC20Withdrawallegitim ist:- Kam vom Cross-Domain-Nachrichtenmechanismus.
- Stammte ursprünglich von der kettenübergreifenden Brücke auf L2.
- 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: MITDer 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 */3456interface IL1ERC20Bridge {7 /* *********8 * Ereignisse *9 ********* */10 11121314 event ERC20DepositInitiated(Alle anzeigenIn 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 _data5 );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 _data8 );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.
12 /* *******************3 * Öffentliche Funktionen *4 ******************* */5 6789 /* *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 13141516 function l2TokenBridge() external returns (address);Alle anzeigenDiese 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 einzahlen4 * @param _l2Token Adresse des entsprechenden L2-ERC20 zum L15 * @param _amount Betrag des einzuzahlenden ERC206 * @param _l2Gas Gaslimit, das erforderlich ist, um die Einzahlung auf L2 abzuschließen.7 * @param _data Optionale Daten zur Weiterleitung an L2. Diese Daten werden8 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen9 * Länge geben diese Verträge keine Garantien über deren Inhalt. */10 11121314151617181920 function depositERC20(21 address _l1Token,22 address _l2Token,23 uint256 _amount,24 uint32 _l2Gas,25 bytes calldata _data26 ) external;Alle anzeigenDer 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 einzahlen4 * @param _l2Token Adresse des entsprechenden L2-ERC20 zum L15 * @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 werden9 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen10 * Länge geben diese Verträge keine Garantien über deren Inhalt. */11 1213141516171819202122 function depositERC20To(23 address _l1Token,24 address _l2Token,25 address _to,26 uint256 _amount,27 uint32 _l2Gas,28 bytes calldata _data29 ) external;Alle anzeigenDiese 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 5678 /* *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 werden18 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen19 * Länge geben diese Verträge keine Garantien über deren Inhalt. */20 2122232425262728293031323334 function finalizeERC20Withdrawal(35 address _l1Token,36 address _l2Token,37 address _from,38 address _to,39 uint256 _amount,40 bytes calldata _data41 ) external;42}Alle anzeigenAuszahlungen (und andere Nachrichten von L2 zu L1) in Optimism sind ein zweistufiger Prozess:
- Eine initiierende Transaktion auf L2.
- 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: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/* *7 * @title IL1StandardBridge */891011interface IL1StandardBridge is IL1ERC20Bridge {12 /* *********13 * Ereignisse *14 ********* */15 161718 event ETHDepositInitiated(19 address indexed _from,20 address indexed _to,21 uint256 _amount,22 bytes _data23 );Alle anzeigenDieses 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 );67 /* *******************8 * Öffentliche Funktionen *9 ******************* */10 11121314 /* *15 * @dev Zahlt einen Betrag an ETH auf das Guthaben des Aufrufers auf L2 ein.16 .17 .18 . */19 202122232425 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;2627 /* *28 * @dev Zahlt einen Betrag an ETH auf das Guthaben eines Empfängers auf L2 ein.29 .30 .31 . */32 333435363738 function depositETHTo(39 address _to,40 uint32 _l2Gas,41 bytes calldata _data42 ) external payable;4344 /* ************************45 * Kettenübergreifende Funktionen *46 ************************ */47 48495051 /* *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 5859606162636465 function finalizeETHWithdrawal(66 address _from,67 address _to,68 uint256 _amount,69 bytes calldata _data70 ) external;71}Alle anzeigenCrossDomainEnabled
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: MIT2pragma solidity >0.5.0 <0.9.0;34/* 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 CrossDomainEnabled3 * @dev Hilfsvertrag für Verträge, die domänenübergreifende Kommunikation durchführen4 *5 * Verwendeter Compiler: definiert durch den erbenden Vertrag */6789101112contract CrossDomainEnabled {13 /* ************14 * Variablen *15 ************ */16 17181920 // Messenger-Vertrag, der zum Senden und Empfangen von Nachrichten aus der anderen Domäne verwendet wird.21 address public messenger;2223 /* **************24 * Konstruktor *25 ************** */26 27282930 /* *31 * @param _messenger Adresse des CrossDomainMessenger auf der aktuellen Ebene. */32 333435 constructor(address _messenger) {36 messenger = _messenger;37 }Alle anzeigenDer 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.
12 /* *********************3 * Funktionsmodifikatoren *4 ********************* */5 6789 /* *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, das12 * authentifiziert ist, diese Funktion aufzurufen. */13 1415161718 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Alle anzeigenDas 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.
12 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.
12 _;3 }45 /* *********************6 * Interne Funktionen *7 ********************* */8 9101112 /* *13 * Ruft den Messenger ab, normalerweise aus dem Speicher. Diese Funktion wird offengelegt, falls ein untergeordneter Vertrag14 * sie überschreiben muss.15 * @return Die Adresse des domänenübergreifenden Messenger-Vertrags, der verwendet werden soll. */16 1718192021 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {22 return ICrossDomainMessenger(messenger);23 }Alle anzeigenDiese 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.
12 /* *3 * Sendet eine Nachricht an ein Konto in einer anderen Domäne4 * @param _crossDomainTarget Der beabsichtigte Empfänger in der Zieldomäne5 * @param _message Die an das Ziel zu sendenden Daten (normalerweise Calldata für eine Funktion mit6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit Das Gaslimit für den Empfang der Nachricht in der Zieldomäne. */8 9101112131415 function sendCrossDomainMessage(16 address _crossDomainTarget,17 uint32 _gasLimit,18 bytes memory _messageAlle anzeigenSchließlich die Funktion, die eine Nachricht an die andere Ebene sendet.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlither (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 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: MIT2pragma 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:
- Revert (Rückgängig machen)
- 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 L1StandardBridge3 * @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 Einzahlungen5 * und lauscht auf neu abgeschlossene Abhebungen.6 * */7891011121314contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {15 using SafeERC20 for IERC20;Alle anzeigenMit dieser Zeile geben wir an, dass der SafeERC20-Wrapper jedes Mal verwendet werden soll, wenn wir die IERC20-Schnittstelle verwenden.
12 /* *******************************3 * Externe Vertragsreferenzen *4 ******************************* */5 6789 address public l2TokenBridge;Alle anzeigenDie Adresse der L2StandardBridge.
12 // Ordnet L1-Token dem L2-Token und dem Guthaben des eingezahlten L1-Tokens zu3 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.
12 /* **************3 * Konstruktor *4 ************** */5 6789 // Dieser Vertrag befindet sich hinter einem Proxy, daher bleiben die Konstruktorparameter ungenutzt.10 constructor() CrossDomainEnabled(address(0)) {}Alle anzeigenWir 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 5678 /* *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 12131415 // slither-disable-next-line external-functionAlle anzeigenDieser 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:
- 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.
- Wenn der legitime Aufruf von
initializefehlschlä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.
12 /* *************3 * Einzahlen *4 ************* */5 6789 /* * @dev Modifikator, der erfordert, dass der Sender ein EOA ist. Diese Prüfung könnte von einem bösartigen10 * Vertrag über Initcode umgangen werden, aber sie kümmert sich um den Benutzerfehler, den wir vermeiden wollen. */11 121314 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 anzeigenDies 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 konservativer5 * Standardbetrag an L2 weitergeleitet. */6 789101112 receive() external payable onlyEOA {13 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));14 }Alle anzeigenDiese 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 456 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {7 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);8 }910 /* *11 * @inheritdoc IL1StandardBridge */12 131415 function depositETHTo(16 address _to,17 uint32 _l2Gas,18 bytes calldata _data19 ) external payable {20 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);21 }Alle anzeigenDiese 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 über3 * 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 werden8 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen9 * Länge geben diese Verträge keine Garantien über deren Inhalt. */10 11121314151617181920 function _initiateETHDeposit(21 address _from,22 address _to,23 uint32 _l2Gas,24 bytes memory _data25 ) internal {26 // Erstellt Calldata für den finalizeDeposit-Aufruf27 bytes memory message = abi.encodeWithSelector(Alle anzeigenDie 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 _data8 );Die Nachricht hier besteht darin, die Funktion finalizeDeposit (opens in a new tab) mit diesen Parametern aufzurufen:
| Parameter | Value | Meaning |
|---|---|---|
| _l1Token | address(0) | Spezieller Wert, der für ETH (das kein ERC-20-Token ist) auf L1 steht |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Der L2-Vertrag, der ETH auf Optimism verwaltet, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (dieser Vertrag ist nur für die interne Nutzung durch Optimism bestimmt) |
| _from | _from | Die Adresse auf L1, die die ETH sendet |
| _to | _to | Die Adresse auf L2, die die ETH empfängt |
| amount | msg.value | Menge der gesendeten Wei (die bereits an die kettenübergreifende Brücke gesendet wurden) |
| _data | _data | Zusätzliche Daten, die der Einzahlung beigefügt werden sollen |
1 // Sendet Calldata an L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Senden Sie die Nachricht über den Cross-Domain-Messenger.
1 // slither-disable-next-line reentrancy-events2 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 456 function depositERC20(7 .8 .9 .10 ) external virtual onlyEOA {11 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);12 }1314 /* *15 * @inheritdoc IL1ERC20Bridge */16 171819 function depositERC20To(20 .21 .22 .23 ) external virtual {24 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);25 }Alle anzeigenDiese 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-Vertrag3 * ü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 einzahlen6 * @param _l2Token Adresse des entsprechenden L2-ERC20 zum L17 * @param _from Konto, von dem die Einzahlung auf L1 eingezogen wird8 * @param _to Konto, dem die Einzahlung auf L2 gutgeschrieben wird9 * @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 werden12 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen13 * Länge geben diese Verträge keine Garantien über deren Inhalt. */14 1516171819202122232425262728 function _initiateERC20Deposit(29 address _l1Token,30 address _l2Token,31 address _from,32 address _to,33 uint256 _amount,34 uint32 _l2Gas,35 bytes calldata _data36 ) internal {Alle anzeigenDiese 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ünftige2 // Abhebungen. safeTransferFrom prüft auch, ob der Vertrag Code hat, daher schlägt dies fehl, wenn3 // _from ein EOA oder address(0) ist.4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);ERC-20-Token-Übertragungen folgen einem anderen Prozess als ETH:
- Der Benutzer (
_from) erteilt der kettenübergreifenden Brücke die Erlaubnis (Allowance), die entsprechenden Token zu übertragen. - Der Benutzer ruft die kettenübergreifende Brücke mit der Adresse des Token-Vertrags, dem Betrag usw. auf.
- 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 _data10 );1112 // Sendet Calldata an L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Alle anzeigenFü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.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /* ************************7 * Kettenübergreifende Funktionen *8 ************************ */9 10111213 /* *14 * @inheritdoc IL1StandardBridge */15 161718 function finalizeETHWithdrawal(19 address _from,20 address _to,21 uint256 _amount,22 bytes calldata _dataAlle anzeigenDie 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-events2 (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");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Geben Sie ein Ereignis über die Auszahlung aus.
1 }23 /* *4 * @inheritdoc IL1ERC20Bridge */5 678 function finalizeERC20Withdrawal(9 address _l1Token,10 address _l2Token,11 address _from,12 address _to,13 uint256 _amount,14 bytes calldata _data15 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Alle anzeigenDiese 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.
12 // Wenn eine Abhebung auf L1 abgeschlossen ist, überträgt die L1-kettenübergreifende Brücke die Guthaben an den Abhebenden3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /* ****************************12 * Temporär - ETH migrieren *13 **************************** */14 15161718 /* *19 * @dev Fügt dem Konto ETH-Guthaben hinzu. Dies soll ermöglichen, dass ETH20 * 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 dem22 * alten Vertrag empfangen können */23 242526272829 function donateETH() external payable {}30}Alle anzeigenEs 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: MIT2pragma solidity ^0.8.9;34import { 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.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 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: MIT2pragma solidity ^0.8.9;34import { 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";23contract 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.
12 /* *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 8910111213 constructor(14 address _l2Bridge,15 address _l1Token,16 string memory _name,17 string memory _symbol18 ) ERC20(_name, _symbol) {19 l1Token = _l1Token;20 l2Bridge = _l2Bridge;21 }Alle anzeigenRufen Sie zuerst den Konstruktor für den Vertrag auf, von dem wir erben (ERC20(_name, _symbol)), und legen Sie dann unsere eigenen Variablen fest.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Alle anzeigenSo 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-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Alle anzeigenNur 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: MIT2pragma solidity ^0.8.9;34/* 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:
- Auf L1 initiieren Sie Einzahlungen und schließen Auszahlungen ab. Hier initiieren Sie Auszahlungen und schließen Einzahlungen ab.
- 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";67/* Vertrags-Importe */8/* Contract Imports */9import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";1011/* *12 * @title L2StandardBridge13 * @dev Die L2-Standard-kettenübergreifende Brücke ist ein Vertrag, der mit der L1-Standard-kettenübergreifenden Brücke zusammenarbeitet, um14 * 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übergreifende16 * Brücke erfährt.17 * Dieser Vertrag fungiert auch als Verbrenner der für die Abhebung vorgesehenen Token und informiert die L1-kettenübergreifende18 * Brücke, L1-Guthaben freizugeben. */19202122232425262728contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {29 /* *******************************30 * Externe Vertragsreferenzen *31 ******************************* */32 33343536 address public l1TokenBridge;Alle anzeigenBehalten 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.
12 /* **************3 * Konstruktor *4 ************** */5 6789 /* *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 13141516 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)17 CrossDomainEnabled(_l2CrossDomainMessenger)18 {19 l1TokenBridge = _l1TokenBridge;20 }2122 /* **************23 * Abheben *24 ************** */25 26272829 /* *30 * @inheritdoc IL2ERC20Bridge */31 323334 function withdraw(35 address _l2Token,36 uint256 _amount,37 uint32 _l1Gas,38 bytes calldata _data39 ) external virtual {40 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);41 }4243 /* *44 * @inheritdoc IL2ERC20Bridge */45 464748 function withdrawTo(49 address _l2Token,50 address _to,51 uint256 _amount,52 uint32 _l1Gas,53 bytes calldata _data54 ) external virtual {55 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);56 }Alle anzeigenDiese 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.
12 /* *3 * @dev Führt die Logik für Abhebungen aus, indem der Token verbrannt und4 * 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 werden11 * ausschließlich als Annehmlichkeit für externe Verträge bereitgestellt. Abgesehen von der Durchsetzung einer maximalen12 * Länge geben diese Verträge keine Garantien über deren Inhalt. */13 141516171819202122232425 function _initiateWithdrawal(26 address _l2Token,27 address _from,28 address _to,29 uint256 _amount,30 uint32 _l1Gas,31 bytes calldata _data32 ) internal {33 // Wenn eine Abhebung initiiert wird, verbrennen wir die Guthaben des Abhebenden, um eine spätere L2-34 // Nutzung zu verhindern35 // slither-disable-next-line reentrancy-events36 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Alle anzeigenBeachten 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ß).
12 // Erstellt Calldata für l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 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 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Sendet Nachricht hoch zur L1-kettenübergreifenden Brücke21 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /* ***********************************29 * Kettenübergreifende Funktion: Einzahlen *30 *********************************** */31 32333435 /* *36 * @inheritdoc IL2ERC20Bridge */37 383940 function finalizeDeposit(41 address _l1Token,42 address _l2Token,43 address _from,44 address _to,45 uint256 _amount,46 bytes calldata _dataAlle anzeigenDiese 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 und2 // verifiziert, dass der eingezahlte Token auf L1 mit der hier eingezahlten L2-Token-Repräsentation übereinstimmt3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Plausibilitätsprüfungen (Sanity Checks):
- Die richtige Schnittstelle wird unterstützt.
- 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 an3 // Token gut.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Wenn die Plausibilitätsprüfungen erfolgreich sind, schließen Sie die Einzahlung ab:
- Prägen Sie die Token.
- Geben Sie das entsprechende Ereignis aus.
1 } else {2 // Entweder ist der L2-Token, in den eingezahlt wird, nicht mit der korrekten Adresse3 // 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 irgendwie5 // 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 begrenzt9 // Benutzerfehler und mildert einige Formen von bösartigem Vertragsverhalten ab.Alle anzeigenWenn 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ückzusenden6 _from,7 _amount,8 _data9 );1011 // Sendet Nachricht hoch zur L1-kettenübergreifenden Brücke12 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Alle anzeigenFazit
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