Przegląd standardowego kontraktu mostu Optimism
Optimismopens in a new tab to rollup optymistyczny. Rollupy optymistyczne mogą przetwarzać transakcje po znacznie niższej cenie niż sieć główna Ethereum (znana również jako warstwa 1 lub L1), ponieważ transakcje są przetwarzane tylko przez kilka węzłów, a nie przez każdy węzeł w sieci. Jednocześnie wszystkie dane są zapisywane w L1, dzięki czemu wszystko można udowodnić i zrekonstruować z zachowaniem wszystkich gwarancji integralności i dostępności sieci głównej.
Aby używać aktywów L1 na Optimism (lub dowolnej innej L2), aktywa te muszą być przeniesione przez most. Jednym ze sposobów na osiągnięcie tego jest zablokowanie przez użytkowników aktywów (najczęściej są to tokeny ETH i ERC-20) na L1 i otrzymanie równoważnych aktywów do wykorzystania na L2. Ostatecznie ten, kto je zdobędzie, może chcieć przenieść je z powrotem na L1 za pomocą mostu. W takim przypadku aktywa są spalane na L2, a następnie uwalniane z powrotem do użytkownika na L1.
W ten sposób działa standardowy most Optimismopens in a new tab. W tym artykule przeanalizujemy kod źródłowy tego mostu, aby zobaczyć, jak on działa i zbadać go jako przykład dobrze napisanego kodu Solidity.
Przepływy sterowania
Most ma dwa główne przepływy:
- Depozyt (z L1 do L2)
- Wypłata (z L2 do L1)
Przepływ depozytu
Warstwa 1
- W przypadku deponowania ERC-20 deponent udziela mostowi zgody na wydanie zdeponowanej kwoty
- Deponent wywołuje most L1 (
depositERC20,depositERC20To,depositETHlubdepositETHTo) - Most L1 przejmuje w posiadanie przeniesione aktywa
- ETH: Aktywa są przekazywane przez deponenta w ramach wywołania
- ERC-20: Aktywa są przenoszone przez most na siebie, korzystając ze zgody udzielonej przez deponenta
- Most L1 używa mechanizmu wiadomości między domenami do wywołania funkcji
finalizeDepositna moście L2
Warstwa 2
- Most L2 weryfikuje, czy wywołanie funkcji
finalizeDepositjest prawidłowe:- Pochodzi z kontraktu wiadomości między domenami
- Pochodził pierwotnie z mostu na L1
- Most L2 sprawdza, czy kontrakt tokenu ERC-20 na L2 jest prawidłowy:
- Kontrakt L2 informuje, że jego odpowiednik L1 jest taki sam jak ten, z którego pochodzą tokeny na L1
- Kontrakt L2 informuje, że obsługuje prawidłowy interfejs (używając ERC-165opens in a new tab).
- Jeśli kontrakt L2 jest prawidłowy, wywołaj go, aby wyemitować odpowiednią liczbę tokenów na odpowiedni adres. Jeśli nie, rozpocznij proces wypłaty, aby umożliwić użytkownikowi odebranie tokenów na L1.
Przepływ wypłat
Warstwa 2
- Wypłacający wywołuje most L2 (
withdrawlubwithdrawTo) - Most L2 spala odpowiednią liczbę tokenów należących do
msg.sender - Most L2 używa mechanizmu wiadomości między domenami do wywołania funkcji
finalizeETHWithdrawallubfinalizeERC20Withdrawalna moście L1
Warstwa 1
- Most L1 weryfikuje, czy wywołanie funkcji
finalizeETHWithdrawallubfinalizeERC20Withdrawaljest prawidłowe:- Pochodzi z mechanizmu wiadomości między domenami
- Pochodził pierwotnie z mostu na L2
- Most L1 przekazuje odpowiednie aktywa (ETH lub ERC-20) na odpowiedni adres
Kod warstwy 1
To jest kod, który działa na L1, w sieci głównej Ethereum.
IL1ERC20Bridge
Ten interfejs jest zdefiniowany tutajopens in a new tab. Zawiera on funkcje i definicje wymagane do przenoszenia tokenów ERC-20 za pomocą mostu.
1// SPDX-License-Identifier: MITWiększość kodu Optimism jest udostępniana na licencji MITopens in a new tab.
1pragma solidity >0.5.0 <0.9.0;W chwili pisania tego tekstu najnowsza wersja Solidity to 0.8.12. Dopóki wersja 0.9.0 nie zostanie wydana, nie wiemy, czy ten kod jest z nią kompatybilny, czy nie.
1/**2 * @title IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Zdarzenia *7 **********/89 event ERC20DepositInitiated(Pokaż wszystkoW terminologii mostów Optimism depozyt oznacza transfer z L1 do L2, a wypłata oznacza transfer z L2 do L1.
1 address indexed _l1Token,2 address indexed _l2Token,W większości przypadków adres ERC-20 na L1 nie jest taki sam jak adres równoważnego ERC-20 na L2.
Listę adresów tokenów można zobaczyć tutajopens in a new tab.
Adres z chainId 1 znajduje się na L1 (sieć główna), a adres z chainId 10 na L2 (Optimism).
Pozostałe dwie wartości chainId dotyczą sieci testowej Kovan (42) i sieci testowej Optimistic Kovan (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );Możliwe jest dodawanie notatek do transferów, w którym to przypadku są one dodawane do zdarzeń, które je raportują.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );Ten sam kontrakt mostu obsługuje transfery w obu kierunkach. W przypadku mostu L1 oznacza to inicjalizację depozytów i finalizację wypłat.
12 /********************3 * Funkcje publiczne *4 ********************/56 /**7 * @dev pobierz adres odpowiedniego kontraktu mostu L2.8 * @return Adres odpowiedniego kontraktu mostu L2.9 */10 function l2TokenBridge() external returns (address);Pokaż wszystkoTa funkcja nie jest tak naprawdę potrzebna, ponieważ na L2 jest to kontrakt wdrożony wstępnie, więc zawsze znajduje się pod adresem 0x4200000000000000000000000000000000000010.
Jest tu dla symetrii z mostem L2, ponieważ adres mostu L1 nie jest trywialny do poznania.
1 /**2 * @dev zdeponuj kwotę ERC20 na saldo wywołującego na L2.3 * @param _l1Token Adres ERC20 L1, który deponujemy4 * @param _l2Token Adres odpowiedniego ERC20 L25 * @param _amount Kwota ERC20 do zdeponowania6 * @param _l2Gas Limit gazu wymagany do ukończenia depozytu na L2.7 * @param _data Opcjonalne dane do przesłania do L2. Dane te są udostępniane8 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej9 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.10 */11 function depositERC20(12 address _l1Token,13 address _l2Token,14 uint256 _amount,15 uint32 _l2Gas,16 bytes calldata _data17 ) external;Pokaż wszystkoParametr _l2Gas to ilość gazu L2, którą transakcja może zużyć.
Do pewnego (wysokiego) limitu jest to darmoweopens in a new tab, więc o ile kontrakt ERC-20 nie robi czegoś naprawdę dziwnego podczas emisji, nie powinno to stanowić problemu.
Ta funkcja obsługuje typowy scenariusz, w którym użytkownik przenosi aktywa za pomocą mostu na ten sam adres na innym blockchainie.
1 /**2 * @dev zdeponuj kwotę ERC20 na saldo odbiorcy na L2.3 * @param _l1Token Adres ERC20 L1, który deponujemy4 * @param _l2Token Adres odpowiedniego ERC20 L25 * @param _to Adres L2, na który ma być zaksięgowana wypłata.6 * @param _amount Kwota ERC20 do zdeponowania.7 * @param _l2Gas Limit gazu wymagany do ukończenia depozytu na L2.8 * @param _data Opcjonalne dane do przesłania do L2. Dane te są udostępniane9 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej10 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.11 */12 function depositERC20To(13 address _l1Token,14 address _l2Token,15 address _to,16 uint256 _amount,17 uint32 _l2Gas,18 bytes calldata _data19 ) external;Pokaż wszystkoTa funkcja jest niemal identyczna jak depositERC20, ale pozwala na wysłanie ERC-20 na inny adres.
1 /*************************2 * Funkcje międzyłańcuchowe *3 *************************/45 /**6 * @dev Zakończ wypłatę z L2 do L1 i zasil saldo odbiorcy tokenem7 * L1 ERC20.8 * To wywołanie nie powiedzie się, jeśli zainicjowana wypłata z L2 nie została sfinalizowana.9 *10 * @param _l1Token Adres tokenu L1, dla którego finalizowana jest wypłata.11 * @param _l2Token Adres tokenu L2, na którym zainicjowano wypłatę.12 * @param _from Adres L2 inicjujący transfer.13 * @param _to Adres L1, na który ma zostać zaksięgowana wypłata.14 * @param _amount Kwota ERC20 do zdeponowania.15 * @param _data Dane dostarczone przez nadawcę na L2. Dane te są udostępniane16 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej17 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.18 */19 function finalizeERC20Withdrawal(20 address _l1Token,21 address _l2Token,22 address _from,23 address _to,24 uint256 _amount,25 bytes calldata _data26 ) external;27}Pokaż wszystkoWypłaty (i inne wiadomości z L2 do L1) w Optimism to proces dwuetapowy:
- Transakcja inicjująca na L2.
- Transakcja finalizująca lub odbierająca na L1. Ta transakcja musi nastąpić po zakończeniu okresu kwestionowania błęduopens in a new tab dla transakcji L2.
IL1StandardBridge
Ten interfejs jest zdefiniowany tutajopens in a new tab.
Ten plik zawiera definicje zdarzeń i funkcji dla ETH.
Definicje te są bardzo podobne do tych zdefiniowanych powyżej w IL1ERC20Bridge dla ERC-20.
Interfejs mostu jest podzielony na dwa pliki, ponieważ niektóre tokeny ERC-20 wymagają niestandardowego przetwarzania i nie mogą być obsługiwane przez standardowy most.
W ten sposób niestandardowy most, który obsługuje taki token, może zaimplementować IL1ERC20Bridge i nie musi również przenosić ETH.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Zdarzenia *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );Pokaż wszystkoTo zdarzenie jest prawie identyczne z wersją ERC-20 (ERC20DepositInitiated), z wyjątkiem braku adresów tokenów L1 i L2.
To samo dotyczy innych zdarzeń i funkcji.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * Funkcje publiczne *9 ********************/1011 /**12 * @dev Zdeponuj kwotę ETH na saldo wywołującego na L2.13 .14 .15 .16 */17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;1819 /**20 * @dev Zdeponuj kwotę ETH na saldo odbiorcy na L2.21 .22 .23 .24 */25 function depositETHTo(26 address _to,27 uint32 _l2Gas,28 bytes calldata _data29 ) external payable;3031 /*************************32 * Funkcje międzyłańcuchowe *33 *************************/3435 /**36 * @dev Zakończ wypłatę z L2 do L1 i zasil saldo odbiorcy tokenem37 * L1 ETH. Ponieważ tylko xDomainMessenger może wywołać tę funkcję, nigdy nie zostanie ona wywołana38 * przed sfinalizowaniem wypłaty.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}Pokaż wszystkoCrossDomainEnabled
Ten kontraktopens in a new tab jest dziedziczony przez oba mosty (L1 i L2) do wysyłania wiadomości do drugiej warstwy.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Importy interfejsów */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";Ten interfejsopens in a new tab informuje kontrakt, jak wysyłać wiadomości do drugiej warstwy, używając komunikatora między domenami. Ten komunikator między domenami to zupełnie inny system i zasługuje na osobny artykuł, który mam nadzieję napisać w przyszłości.
1/**2 * @title CrossDomainEnabled3 * @dev Kontrakt pomocniczy dla kontraktów wykonujących komunikację między domenami4 *5 * Użyty kompilator: zdefiniowany przez kontrakt dziedziczący6 */7contract CrossDomainEnabled {8 /*************9 * Zmienne *10 *************/1112 // Kontrakt komunikatora używany do wysyłania i odbierania wiadomości z innej domeny.13 address public messenger;1415 /***************16 * Konstruktor *17 ***************/1819 /**20 * @param _messenger Adres CrossDomainMessenger w bieżącej warstwie.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Pokaż wszystkoJedyny parametr, który kontrakt musi znać, to adres komunikatora między domenami w tej warstwie. Ten parametr jest ustawiany raz, w konstruktorze i nigdy się nie zmienia.
12 /**********************3 * Modyfikatory funkcji *4 **********************/56 /**7 * Wymusza, aby zmodyfikowana funkcja była wywoływana tylko przez określone konto między domenami.8 * @param _sourceDomainAccount Jedyne konto w domenie źródłowej, które jest9 * uwierzytelnione do wywołania tej funkcji.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Pokaż wszystkoKomunikacja między domenami jest dostępna dla każdego kontraktu na blockchainie, na którym jest uruchomiona (zarówno w sieci głównej Ethereum, jak i Optimism). Ale potrzebujemy, aby most po każdej stronie ufał tylko określonym wiadomościom, jeśli pochodzą one z mostu po drugiej stronie.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: kontrakt komunikatora nieuwierzytelniony"4 );Tylko wiadomości z odpowiedniego komunikatora między domenami (messenger, jak widać poniżej) mogą być zaufane.
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: zły nadawca wiadomości między domenami"5 );Sposób, w jaki komunikator między domenami dostarcza adres, który wysłał wiadomość z drugiej warstwy, to funkcja .xDomainMessageSender()opens in a new tab.
Dopóki jest wywoływana w transakcji zainicjowanej przez wiadomość, może dostarczyć tych informacji.
Musimy upewnić się, że otrzymana wiadomość pochodzi z drugiego mostu.
12 _;3 }45 /**********************6 * Funkcje wewnętrzne *7 **********************/89 /**10 * Pobiera komunikator, zazwyczaj z pamięci masowej. Ta funkcja jest udostępniana na wypadek, gdyby kontrakt potomny11 * musiał ją nadpisać.12 * @return Adres kontraktu komunikatora między domenami, który powinien być używany.13 */14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {15 return ICrossDomainMessenger(messenger);16 }Pokaż wszystkoTa funkcja zwraca komunikator między domenami.
Używamy funkcji, a nie zmiennej messenger, aby umożliwić kontraktom dziedziczącym po tym kontrakcie użycie algorytmu do określenia, którego komunikatora między domenami użyć.
12 /**3 * Wysyła wiadomość do konta w innej domenie4 * @param _crossDomainTarget Docelowy odbiorca w domenie docelowej5 * @param _message Dane do wysłania do celu (zazwyczaj calldata do funkcji z6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit Limit gazu na odbiór wiadomości w domenie docelowej.8 */9 function sendCrossDomainMessage(10 address _crossDomainTarget,11 uint32 _gasLimit,12 bytes memory _messagePokaż wszystkoNa koniec funkcja, która wysyła wiadomość do drugiej warstwy.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlitheropens in a new tab to statyczny analizator, który Optimism uruchamia na każdym kontrakcie w poszukiwaniu luk w zabezpieczeniach i innych potencjalnych problemów. W tym przypadku poniższa linia uruchamia dwie luki w zabezpieczeniach:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}W tym przypadku nie martwimy się o ponowne wejście, ponieważ wiemy, że getCrossDomainMessenger() zwraca zaufany adres, nawet jeśli Slither nie ma sposobu, aby to wiedzieć.
Kontrakt mostu L1
Kod źródłowy tego kontraktu znajduje się tutajopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;Interfejsy mogą być częścią innych kontraktów, więc muszą obsługiwać szeroki zakres wersji Solidity. Ale sam most jest naszym kontraktem i możemy być rygorystyczni co do wersji Solidity, której używa.
1/* Importy interfejsów */2import { IL1StandardBridge } from "./IL1StandardBridge.sol";3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";IL1ERC20Bridge i IL1StandardBridge zostały wyjaśnione powyżej.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";Ten interfejsopens in a new tab pozwala nam tworzyć wiadomości do kontrolowania standardowego mostu na L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Ten interfejsopens in a new tab pozwala nam kontrolować kontrakty ERC-20. Więcej na ten temat możesz przeczytać tutaj.
1/* Importy bibliotek */2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";Jak wyjaśniono powyżej, ten kontrakt jest używany do komunikacji międzywarstwowej.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";Lib_PredeployAddressesopens in a new tab zawiera adresy kontraktów L2, które zawsze mają ten sam adres. Obejmuje to standardowy most na L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";Narzędzia adresowe OpenZeppelinopens in a new tab. Jest używany do rozróżniania adresów kontraktów od adresów należących do kont zewnętrznych (EOA).
Należy pamiętać, że nie jest to idealne rozwiązanie, ponieważ nie ma sposobu, aby odróżnić wywołania bezpośrednie od wywołań z konstruktora kontraktu, ale przynajmniej pozwala nam to zidentyfikować i zapobiec niektórym typowym błędom użytkowników.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";Standard ERC-20opens in a new tab obsługuje dwa sposoby raportowania niepowodzenia przez kontrakt:
- Przywróć
- Zwróć
false
Obsługa obu przypadków skomplikowałaby nasz kod, dlatego zamiast tego używamy SafeERC20 OpenZeppelinopens in a new tab, co zapewnia, że wszystkie niepowodzenia skutkują przywróceniemopens in a new tab.
1/**2 * @title L1StandardBridge3 * @dev Most L1 ETH i ERC20 to kontrakt, który przechowuje zdeponowane środki L1 i standardowe4 * tokeny, które są w użyciu na L2. Synchronizuje on odpowiedni most L2, informując go o depozytach5 * i nasłuchując na nowo sfinalizowane wypłaty.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Pokaż wszystkoTa linia określa, że za każdym razem, gdy używamy interfejsu IERC20, używamy opakowania SafeERC20.
12 /********************************3 * Odniesienia do kontraktów zewnętrznych *4 ********************************/56 address public l2TokenBridge;Adres L2StandardBridge.
12 // Mapuje token L1 do tokenu L2 do salda zdeponowanego tokenu L13 mapping(address => mapping(address => uint256)) public deposits;Podwójne mapowanieopens in a new tab takie jak to jest sposobem na zdefiniowanie dwuwymiarowej tablicy rzadkiejopens in a new tab.
Wartości w tej strukturze danych są identyfikowane jako deposit[L1 token addr][L2 token addr].
Wartość domyślna to zero.
Tylko komórki, które są ustawione na inną wartość, są zapisywane w pamięci.
12 /***************3 * Konstruktor *4 ***************/56 // Ten kontrakt znajduje się za proxy, więc parametry konstruktora nie będą używane.7 constructor() CrossDomainEnabled(address(0)) {}Aby móc uaktualnić ten kontrakt bez konieczności kopiowania wszystkich zmiennych w pamięci.
Aby to zrobić, używamy Proxyopens in a new tab, kontraktu, który używa delegatecallopens in a new tab do przekazywania wywołań do osobnego kontraktu, którego adres jest przechowywany przez kontrakt proxy (podczas aktualizacji informujesz proxy o zmianie tego adresu).
Gdy używasz delegatecall, pamięć pozostaje pamięcią kontraktu wywołującego, więc wartości wszystkich zmiennych stanu kontraktu pozostają nienaruszone.
Jednym z efektów tego wzorca jest to, że pamięć kontraktu, który jest wywoływany przez delegatecall, nie jest używana, a zatem wartości konstruktora przekazane do niego nie mają znaczenia.
To jest powód, dla którego możemy podać bezsensowną wartość do konstruktora CrossDomainEnabled.
Jest to również powód, dla którego poniższa inicjalizacja jest oddzielona od konstruktora.
1 /******************2 * Inicjalizacja *3 ******************/45 /**6 * @param _l1messenger Adres komunikatora L1 używany do komunikacji międzyłańcuchowej.7 * @param _l2TokenBridge Adres standardowego mostu L2.8 */9 // slither-disable-next-line external-functionPokaż wszystkoTen test Slitheropens in a new tab identyfikuje funkcje, które nie są wywoływane z kodu kontraktu i dlatego mogą być zadeklarowane jako external zamiast public.
Koszt gazu funkcji external może być niższy, ponieważ mogą one być dostarczane z parametrami w calldata.
Funkcje zadeklarowane jako public muszą być dostępne z wnętrza kontraktu.
Kontrakty nie mogą modyfikować własnych calldata, więc parametry muszą znajdować się w pamięci.
Gdy taka funkcja jest wywoływana zewnętrznie, konieczne jest skopiowanie calldata do pamięci, co kosztuje gaz.
W tym przypadku funkcja jest wywoływana tylko raz, więc nieefektywność nie ma dla nas znaczenia.
1 function initialize(address _l1messenger, address _l2TokenBridge) public {2 require(messenger == address(0), "Kontrakt został już zainicjowany.");Funkcja initialize powinna być wywoływana tylko raz.
Jeśli zmieni się adres komunikatora między domenami L1 lub mostu tokenów L2, tworzymy nowe proxy i nowy most, który je wywołuje.
Jest to mało prawdopodobne, chyba że cały system zostanie uaktualniony, co jest bardzo rzadkim zjawiskiem.
Należy pamiętać, że ta funkcja nie ma żadnego mechanizmu ograniczającego, kto może ją wywołać.
Oznacza to, że teoretycznie napastnik może poczekać, aż wdrożymy proxy i pierwszą wersję mostu, a następnie wykonać front-runningopens in a new tab, aby dostać się do funkcji initialize przed legalnym użytkownikiem. Ale istnieją dwie metody, aby temu zapobiec:
- Jeśli kontrakty są wdrażane nie bezpośrednio przez EOA, ale w transakcji, w której inny kontrakt je tworzyopens in a new tab, cały proces może być atomowy i zakończyć się przed wykonaniem jakiejkolwiek innej transakcji.
- Jeśli legalne wywołanie
initializenie powiedzie się, zawsze można zignorować nowo utworzone proxy i most i utworzyć nowe.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }To są dwa parametry, które most musi znać.
12 /**************3 * Deponowanie *4 **************/56 /** @dev Modyfikator wymagający, aby nadawca był EOA. To sprawdzenie mogłoby zostać ominięte przez złośliwy7 * kontrakt poprzez initcode, ale zapobiega to błędowi użytkownika, którego chcemy uniknąć.8 */9 modifier onlyEOA() {10 // Używane do zatrzymywania depozytów z kontraktów (unikanie przypadkowej utraty tokenów)11 require(!Address.isContract(msg.sender), "Konto nie jest EOA");12 _;13 }Pokaż wszystkoTo jest powód, dla którego potrzebowaliśmy narzędzi Address od OpenZeppelin.
1 /**2 * @dev Tę funkcję można wywołać bez danych3 * w celu zdeponowania kwoty ETH na saldzie wywołującego na L2.4 * Ponieważ funkcja odbioru nie przyjmuje danych, konserwatywna5 * domyślna kwota jest przekazywana do L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Pokaż wszystkoTa funkcja istnieje w celach testowych. Zauważ, że nie pojawia się w definicjach interfejsu - nie jest przeznaczona do normalnego użytku.
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Pokaż wszystkoTe dwie funkcje są opakowaniami wokół _initiateETHDeposit, funkcji obsługującej faktyczny depozyt ETH.
1 /**2 * @dev Wykonuje logikę depozytów, przechowując ETH i informując bramę L2 ETH o3 * depozycie.4 * @param _from Konto, z którego ma być pobrany depozyt na L1.5 * @param _to Konto, na które ma być przekazany depozyt na L2.6 * @param _l2Gas Limit gazu wymagany do ukończenia depozytu na L2.7 * @param _data Opcjonalne dane do przesłania do L2. Dane te są udostępniane8 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej9 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.10 */11 function _initiateETHDeposit(12 address _from,13 address _to,14 uint32 _l2Gas,15 bytes memory _data16 ) internal {17 // Skonstruuj calldata dla wywołania finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Pokaż wszystkoSposób działania wiadomości między domenami polega na tym, że kontrakt docelowy jest wywoływany z wiadomością jako jego calldata.
Kontrakty Solidity zawsze interpretują swoje calldata zgodnie z
specyfikacjami ABIopens in a new tab.
Funkcja Solidity abi.encodeWithSelectoropens in a new tab tworzy te calldata.
1 IL2ERC20Bridge.finalizeDeposit.selector,2 address(0),3 Lib_PredeployAddresses.OVM_ETH,4 _from,5 _to,6 msg.value,7 _data8 );Wiadomość tutaj polega na wywołaniu funkcji finalizeDepositopens in a new tab z tymi parametrami:
| Parametr | Wartość | Znaczenie |
|---|---|---|
| _l1Token | address(0) | Specjalna wartość oznaczająca ETH (który nie jest tokenem ERC-20) na L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Kontrakt L2, który zarządza ETH na Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (ten kontrakt jest przeznaczony wyłącznie do użytku wewnętrznego Optimism) |
| _from | _from | Adres na L1, który wysyła ETH |
| _to | _to | Adres na L2, który odbiera ETH |
| kwota | msg.value | Ilość wysłanych wei (które zostały już wysłane na most) |
| _data | _data | Dodatkowe dane do dołączenia do depozytu |
1 // Wyślij calldata do L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Wyślij wiadomość za pośrednictwem komunikatora między domenami.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Wyemituj zdarzenie, aby poinformować o tym transferze każdą aplikację zdecentralizowaną, która go nasłuchuje.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Pokaż wszystkoTe dwie funkcje są opakowaniami wokół _initiateERC20Deposit, funkcji obsługującej faktyczny depozyt ERC-20.
1 /**2 * @dev Wykonuje logikę depozytów, informując kontrakt tokenu zdeponowanego L23 * o depozycie i wywołując obsługę w celu zablokowania środków L1. (np. transferFrom)4 *5 * @param _l1Token Adres ERC20 L1, który deponujemy6 * @param _l2Token Adres odpowiedniego ERC20 L27 * @param _from Konto, z którego ma być pobrany depozyt na L18 * @param _to Konto, na które ma być przekazany depozyt na L29 * @param _amount Kwota ERC20 do zdeponowania.10 * @param _l2Gas Limit gazu wymagany do ukończenia depozytu na L2.11 * @param _data Opcjonalne dane do przesłania do L2. Dane te są udostępniane12 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej13 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.14 */15 function _initiateERC20Deposit(16 address _l1Token,17 address _l2Token,18 address _from,19 address _to,20 uint256 _amount,21 uint32 _l2Gas,22 bytes calldata _data23 ) internal {Pokaż wszystkoTa funkcja jest podobna do _initiateETHDeposit powyżej, z kilkoma ważnymi różnicami.
Pierwsza różnica polega na tym, że ta funkcja otrzymuje adresy tokenów i kwotę do przeniesienia jako parametry.
W przypadku ETH wywołanie mostu już obejmuje transfer aktywów na konto mostu (msg.value).
1 // Gdy depozyt jest inicjowany na L1, most L1 przekazuje środki do siebie na przyszłe2 // wypłaty. safeTransferFrom sprawdza również, czy kontrakt ma kod, więc to się nie powiedzie, jeśli3 // _from jest EOA lub address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Transfery tokenów ERC-20 przebiegają inaczej niż transfery ETH:
- Użytkownik (
_from) daje mostowi upoważnienie do transferu odpowiednich tokenów. - Użytkownik wywołuje most z adresem kontraktu tokenu, kwotą itp.
- Most transferuje tokeny (do siebie) w ramach procesu depozytowego.
Pierwszy krok może nastąpić w osobnej transakcji niż dwa ostatnie.
Jednak front-running nie stanowi problemu, ponieważ dwie funkcje, które wywołują _initiateERC20Deposit (depositERC20 i depositERC20To) wywołują tę funkcję tylko z msg.sender jako parametrem _from.
1 // Skonstruuj calldata dla _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 // Wyślij calldata do 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;Pokaż wszystkoDodaj zdeponowaną kwotę tokenów do struktury danych deposits.
Na L2 może istnieć wiele adresów odpowiadających temu samemu tokenowi ERC-20 L1, więc nie wystarczy użyć salda mostu tokenu ERC-20 L1, aby śledzić depozyty.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Funkcje międzyłańcuchowe *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataPokaż wszystkoMost L2 wysyła wiadomość do komunikatora między domenami L2, co powoduje, że komunikator między domenami L1 wywołuje tę funkcję (oczywiście, gdy transakcja finalizująca wiadomośćopens in a new tab zostanie przesłana na L1).
1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Upewnij się, że jest to legalna wiadomość, pochodząca z komunikatora między domenami i pochodząca z mostu tokenów L2. Ta funkcja służy do wypłacania ETH z mostu, więc musimy upewnić się, że jest wywoływana tylko przez upoważnionego wywołującego.
1 // slither-disable-next-line reentrancy-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));Sposobem na transfer ETH jest wywołanie odbiorcy z kwotą wei w msg.value.
1 require(success, "TransferHelper::safeTransferETH: Transfer ETH nie powiódł się");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Wyemituj zdarzenie dotyczące wypłaty.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Pokaż wszystkoTa funkcja jest podobna do finalizeETHWithdrawal powyżej, z niezbędnymi zmianami dla tokenów ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;Zaktualizuj strukturę danych deposits.
12 // Gdy wypłata jest finalizowana na L1, most L1 przekazuje środki do wypłacającego3 // 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 * Tymczasowe – Migracja ETH *13 *****************************/1415 /**16 * @dev Dodaje saldo ETH do konta. Ma to na celu umożliwienie migracji ETH17 * ze starej bramy do nowej.18 * UWAGA: Jest to pozostawione tylko na jedną aktualizację, abyśmy mogli otrzymać zmigrowane ETH ze19 * starego kontraktu20 */21 function donateETH() external payable {}22}Pokaż wszystkoIstniała wcześniejsza implementacja mostu.
Kiedy przeszliśmy z tamtej implementacji na tę, musieliśmy przenieść wszystkie aktywa.
Tokeny ERC-20 można po prostu przenieść.
Jednak aby przetransferować ETH do kontraktu, potrzebna jest zgoda tego kontraktu, którą zapewnia nam donateETH.
Tokeny ERC-20 na L2
Aby token ERC-20 pasował do standardowego mostu, musi on zezwalać standardowemu mostowi, i tylko standardowemu mostowi, na emisję tokenów. Jest to konieczne, ponieważ mosty muszą zapewnić, że liczba tokenów w obiegu na Optimism jest równa liczbie tokenów zablokowanych w kontrakcie mostu L1. Jeśli na L2 będzie zbyt wiele tokenów, niektórzy użytkownicy nie będą mogli przenieść swoich aktywów z powrotem na L1. Zamiast zaufanego mostu, w zasadzie odtworzylibyśmy bankowość rezerw cząstkowychopens in a new tab. Jeśli na L1 jest zbyt wiele tokenów, niektóre z nich pozostaną na zawsze zablokowane w kontrakcie mostu, ponieważ nie ma sposobu, aby je uwolnić bez spalenia tokenów L2.
IL2StandardERC20
Każdy token ERC-20 na L2, który używa standardowego mostu, musi dostarczać ten interfejsopens in a new tab, który zawiera funkcje i zdarzenia potrzebne standardowemu mostowi.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Standardowy interfejs ERC-20opens in a new tab nie zawiera funkcji mint i burn.
Metody te nie są wymagane przez standard ERC-20opens in a new tab, który nie określa mechanizmów tworzenia i niszczenia tokenów.
1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";Interfejs ERC-165opens in a new tab służy do określania, jakie funkcje dostarcza kontrakt. Standard można przeczytać tutajopens in a new tab.
1interface IL2StandardERC20 is IERC20, IERC165 {2 function l1Token() external returns (address);Ta funkcja dostarcza adres tokenu L1, który jest przenoszony na ten kontrakt za pomocą mostu. Zauważ, że nie mamy podobnej funkcji w przeciwnym kierunku. Musimy być w stanie przenosić dowolny token L1 za pomocą mostu, niezależnie od tego, czy wsparcie L2 było planowane podczas jego implementacji, czy nie.
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}Funkcje i zdarzenia do emisji (tworzenia) i spalania (niszczenia) tokenów. Most powinien być jedynym podmiotem, który może uruchamiać te funkcje, aby zapewnić prawidłową liczbę tokenów (równą liczbie tokenów zablokowanych na L1).
L2StandardERC20
To jest nasza implementacja interfejsu IL2StandardERC20opens in a new tab.
O ile nie potrzebujesz jakiejś niestandardowej logiki, powinieneś użyć tej.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";Kontrakt ERC-20 OpenZeppelinopens in a new tab. Optimism nie wierzy w wynajdywanie koła na nowo, zwłaszcza gdy koło jest dobrze audytowane i musi być na tyle godne zaufania, aby przechowywać aktywa.
1import "./IL2StandardERC20.sol";23contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;Są to dwa dodatkowe parametry konfiguracyjne, których wymagamy, a których ERC-20 normalnie nie wymaga.
12 /**3 * @param _l2Bridge Adres standardowego mostu L2.4 * @param _l1Token Adres odpowiadającego mu tokenu L1.5 * @param _name Nazwa ERC20.6 * @param _symbol Symbol ERC20.7 */8 constructor(9 address _l2Bridge,10 address _l1Token,11 string memory _name,12 string memory _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Pokaż wszystkoNajpierw wywołaj konstruktor dla kontraktu, z którego dziedziczymy (ERC20(_name, _symbol)), a następnie ustaw nasze własne zmienne.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Tylko Most L2 może emitować i spalać");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 }Pokaż wszystkoTak działa ERC-165opens in a new tab. Każdy interfejs to liczba obsługiwanych funkcji i jest identyfikowany jako alternatywa wykluczającaopens in a new tab selektorów funkcji ABIopens in a new tab tych funkcji.
Most L2 używa ERC-165 jako testu poprawności, aby upewnić się, że kontrakt ERC-20, do którego wysyła aktywa, jest IL2StandardERC20.
Uwaga: Nic nie stoi na przeszkodzie, aby nieuczciwy kontrakt dostarczał fałszywych odpowiedzi do supportsInterface, więc jest to mechanizm sprawdzania poprawności, nie mechanizm bezpieczeństwa.
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}Pokaż wszystkoTylko most L2 może emitować i spalać aktywa.
_mint i _burn są w rzeczywistości zdefiniowane w kontrakcie ERC-20 OpenZeppelin.
Ten kontrakt po prostu nie udostępnia ich na zewnątrz, ponieważ warunki emisji i spalania tokenów są tak zróżnicowane, jak liczba sposobów wykorzystania ERC-20.
Kod mostu L2
To jest kod, który uruchamia most na Optimism. Źródło tego kontraktu znajduje się tutajopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34/* Importy interfejsów */5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";Interfejs IL2ERC20Bridgeopens in a new tab jest bardzo podobny do odpowiednika L1, który widzieliśmy powyżej. Istnieją dwie znaczące różnice:
- Na L1 inicjujesz depozyty i finalizujesz wypłaty. Tutaj inicjujesz wypłaty i finalizujesz depozyty.
- Na L1 konieczne jest rozróżnienie między tokenami ETH i ERC-20. Na L2 możemy używać tych samych funkcji dla obu, ponieważ wewnętrznie salda ETH na Optimism są obsługiwane jako token ERC-20 z adresem 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens in a new tab.
1/* Importy bibliotek */2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";56/* Importy kontraktów */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev Most standardowy L2 to kontrakt, który współpracuje z mostem standardowym L1, aby12 * umożliwić przejścia ETH i ERC20 między L1 i L2.13 * Ten kontrakt działa jako emitent nowych tokenów, gdy dowiaduje się o depozytach na moście standardowym14 * L1.15 * Ten kontrakt działa również jako spalacz tokenów przeznaczonych do wypłaty, informując most L116 * o uwolnieniu środków L1.17 */18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {19 /********************************20 * Odniesienia do kontraktów zewnętrznych *21 ********************************/2223 address public l1TokenBridge;Pokaż wszystkoŚledź adres mostu L1. Zauważ, że w przeciwieństwie do odpowiednika L1, tutaj potrzebujemy tej zmiennej. Adres mostu L1 nie jest znany z góry.
12 /***************3 * Konstruktor *4 ***************/56 /**7 * @param _l2CrossDomainMessenger Komunikator między domenami używany przez ten kontrakt.8 * @param _l1TokenBridge Adres mostu L1 wdrożonego w głównej sieci.9 */10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)11 CrossDomainEnabled(_l2CrossDomainMessenger)12 {13 l1TokenBridge = _l1TokenBridge;14 }1516 /***************17 * Wypłacanie *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Pokaż wszystkoTe dwie funkcje inicjują wypłaty. Zauważ, że nie ma potrzeby określania adresu tokenu L1. Oczekuje się, że tokeny L2 podadzą nam adres swojego odpowiednika L1.
12 /**3 * @dev Wykonuje logikę wypłat, spalając token i informując4 * bramę tokenów L1 o wypłacie.5 * @param _l2Token Adres tokenu L2, na którym inicjowana jest wypłata.6 * @param _from Konto, z którego ma być pobrana wypłata na L2.7 * @param _to Konto, na które ma być przekazana wypłata na L1.8 * @param _amount Ilość tokenu do wypłaty.9 * @param _l1Gas Nieużywane, ale zawarte w celu zapewnienia potencjalnej zgodności w przyszłości.10 * @param _data Opcjonalne dane do przesłania do L1. Dane te są udostępniane11 * wyłącznie dla wygody zewnętrznych kontraktów. Oprócz egzekwowania maksymalnej12 * długości, kontrakty te nie dają żadnych gwarancji co do ich zawartości.13 */14 function _initiateWithdrawal(15 address _l2Token,16 address _from,17 address _to,18 uint256 _amount,19 uint32 _l1Gas,20 bytes calldata _data21 ) internal {22 // Gdy inicjowana jest wypłata, spalamy środki wypłacającego, aby zapobiec późniejszemu wykorzystaniu na L223 // slither-disable-next-line reentrancy-events24 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Pokaż wszystkoZauważ, że nie polegamy na parametrze _from, ale na msg.sender, który jest znacznie trudniejszy do sfałszowania (o ile mi wiadomo, niemożliwy).
12 // Skonstruuj calldata dla 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) {Na L1 konieczne jest rozróżnienie między ETH i ERC-20.
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 // Wyślij wiadomość do mostu L121 // 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 * Funkcja międzyłańcuchowa: Deponowanie *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataPokaż wszystkoTa funkcja jest wywoływana przez L1StandardBridge.
1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {Upewnij się, że źródło wiadomości jest legalne.
Jest to ważne, ponieważ ta funkcja wywołuje _mint i może być użyta do wydawania tokenów, które nie są pokryte tokenami, które most posiada na L1.
1 // Sprawdź, czy token docelowy jest zgodny i2 // zweryfikuj, czy zdeponowany token na L1 odpowiada reprezentacji zdeponowanego tokenu L2 tutaj3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Testy poprawności:
- Obsługiwany jest prawidłowy interfejs
- Adres L1 kontraktu ERC-20 na L2 odpowiada źródłu tokenów na L1
1 ) {2 // Po sfinalizowaniu depozytu zasilamy konto na L2 taką samą kwotą3 // tokenów.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);Jeśli testy poprawności przejdą pomyślnie, sfinalizuj depozyt:
- Wyemituj tokeny
- Wyemituj odpowiednie zdarzenie
1 } else {2 // Albo token L2, do którego dokonywany jest depozyt, nie zgadza się co do prawidłowego adresu3 // swojego tokenu L1, albo nie obsługuje prawidłowego interfejsu.4 // Powinno to nastąpić tylko wtedy, gdy istnieje złośliwy token L2 lub jeśli użytkownik w jakiś sposób5 // podał nieprawidłowy adres tokenu L2 do zdeponowania.6 // W obu przypadkach zatrzymujemy tutaj proces i konstruujemy wiadomość7 // wypłaty, aby użytkownicy mogli w niektórych przypadkach odzyskać swoje środki.8 // Nie ma sposobu, aby całkowicie zapobiec złośliwym kontraktom tokenów, ale ogranicza to9 // błędy użytkowników i łagodzi niektóre formy złośliwego zachowania kontraktów.Pokaż wszystkoJeśli użytkownik popełnił wykrywalny błąd, używając niewłaściwego adresu tokenu L2, chcemy anulować depozyt i zwrócić tokeny na L1. Jedynym sposobem, w jaki możemy to zrobić z L2, jest wysłanie wiadomości, która będzie musiała poczekać na okres kwestionowania błędu, ale jest to znacznie lepsze dla użytkownika niż trwała utrata tokenów.
1 bytes memory message = abi.encodeWithSelector(2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,3 _l1Token,4 _l2Token,5 _to, // zamieniono tutaj _to i _from, aby zwrócić depozyt do nadawcy6 _from,7 _amount,8 _data9 );1011 // Wyślij wiadomość do mostu L112 // 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}Pokaż wszystkoWnioski
Standardowy most jest najbardziej elastycznym mechanizmem transferu aktywów. Jednakże, ponieważ jest tak ogólny, nie zawsze jest to najłatwiejszy mechanizm do użycia. Szczególnie w przypadku wypłat, większość użytkowników woli korzystać z mostów stron trzecichopens in a new tab, które nie czekają na okres kwestionowania i nie wymagają dowodu Merkle, aby sfinalizować wypłatę.
Te mosty zazwyczaj działają poprzez posiadanie aktywów na L1, które dostarczają natychmiast za niewielką opłatą (często niższą niż koszt gazu za standardową wypłatę z mostu). Gdy most (lub osoby go prowadzące) przewiduje brak aktywów L1, przekazuje wystarczającą ilość aktywów z L2. Ponieważ są to bardzo duże wypłaty, koszt wypłaty jest amortyzowany na dużą kwotę i stanowi znacznie mniejszy procent.
Mam nadzieję, że ten artykuł pomógł ci lepiej zrozumieć, jak działa warstwa 2 i jak pisać kod Solidity, który jest jasny i bezpieczny.
Zobacz więcej mojej pracy tutajopens in a new tab.
Strona ostatnio zaktualizowana: 22 października 2025