Panduan kontrak jembatan standar Optimism
Optimism (opens in a new tab) adalah sebuah Optimistic Rollup. Optimistic rollup dapat memproses transaksi dengan harga yang jauh lebih rendah daripada Mainnet Ethereum (juga dikenal sebagai layer 1 atau L1) karena transaksi hanya diproses oleh beberapa node, bukan setiap node di jaringan. Pada saat yang sama, semua data ditulis ke L1 sehingga semuanya dapat dibuktikan dan direkonstruksi dengan semua jaminan integritas dan ketersediaan dari Mainnet.
Untuk menggunakan aset L1 di Optimism (atau L2 lainnya), aset tersebut perlu dihubungkan melalui jembatan. Salah satu cara untuk mencapai ini adalah pengguna mengunci aset (ETH dan token ERC-20 adalah yang paling umum) di L1, dan menerima aset yang setara untuk digunakan di L2. Pada akhirnya, siapa pun yang memilikinya mungkin ingin menghubungkannya kembali ke L1 melalui jembatan. Saat melakukan ini, aset dibakar di L2 dan kemudian dilepaskan kembali ke pengguna di L1.
Inilah cara kerja jembatan standar Optimism (opens in a new tab). Dalam artikel ini kita akan membahas kode sumber untuk jembatan tersebut untuk melihat bagaimana cara kerjanya dan mempelajarinya sebagai contoh kode Solidity yang ditulis dengan baik.
Alur kontrol
Jembatan ini memiliki dua alur utama:
- Deposit (dari L1 ke L2)
- Penarikan (dari L2 ke L1)
Alur deposit
Layer 1
- Jika mendepositokan ERC-20, pendeposit memberikan jembatan izin (allowance) untuk membelanjakan jumlah yang didepositokan
- Pendeposit memanggil jembatan L1 (
depositERC20,depositERC20To,depositETH, ataudepositETHTo) - Jembatan L1 mengambil alih kepemilikan aset yang dijembatani
- ETH: Aset ditransfer oleh pendeposit sebagai bagian dari pemanggilan
- ERC-20: Aset ditransfer oleh jembatan ke dirinya sendiri menggunakan izin yang diberikan oleh pendeposit
- Jembatan L1 menggunakan mekanisme pesan lintas domain untuk memanggil
finalizeDepositpada jembatan L2
Layer 2
- Jembatan L2 memverifikasi bahwa pemanggilan ke
finalizeDepositadalah sah:- Berasal dari kontrak pesan lintas domain
- Awalnya berasal dari jembatan di L1
- Jembatan L2 memeriksa apakah kontrak token ERC-20 di L2 adalah yang benar:
- Kontrak L2 melaporkan bahwa pasangannya di L1 sama dengan asal token di L1
- Kontrak L2 melaporkan bahwa ia mendukung antarmuka yang benar (menggunakan ERC-165 (opens in a new tab)).
- Jika kontrak L2 adalah yang benar, panggil kontrak tersebut untuk melakukan mint jumlah token yang sesuai ke alamat yang sesuai. Jika tidak, mulai proses penarikan untuk memungkinkan pengguna mengklaim token di L1.
Alur penarikan
Layer 2
- Penarik memanggil jembatan L2 (
withdrawatauwithdrawTo) - Jembatan L2 membakar jumlah token yang sesuai milik
msg.sender - Jembatan L2 menggunakan mekanisme pesan lintas domain untuk memanggil
finalizeETHWithdrawalataufinalizeERC20Withdrawalpada jembatan L1
Layer 1
- Jembatan L1 memverifikasi bahwa pemanggilan ke
finalizeETHWithdrawalataufinalizeERC20Withdrawaladalah sah:- Berasal dari mekanisme pesan lintas domain
- Awalnya berasal dari jembatan di L2
- Jembatan L1 mentransfer aset yang sesuai (ETH atau ERC-20) ke alamat yang sesuai
Kode Layer 1
Ini adalah kode yang berjalan di L1, Mainnet Ethereum.
IL1ERC20Bridge
Antarmuka ini didefinisikan di sini (opens in a new tab). Ini mencakup fungsi dan definisi yang diperlukan untuk menjembatani token ERC-20.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MITSebagian besar kode Optimism dirilis di bawah lisensi MIT (opens in a new tab).
1pragma solidity >0.5.0 <0.9.0;Saat penulisan, versi terbaru Solidity adalah 0.8.12. Hingga versi 0.9.0 dirilis, kita tidak tahu apakah kode ini kompatibel dengannya atau tidak.
1/**2 * @title IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /* *********6 * Event *7 ********* */8 /**********9 * Events *10 **********/11
12 event ERC20DepositInitiated(Dalam terminologi jembatan Optimism, deposit berarti transfer dari L1 ke L2, dan withdrawal (penarikan) berarti transfer dari L2 ke L1.
1 address indexed _l1Token,2 address indexed _l2Token,Dalam kebanyakan kasus, alamat ERC-20 di L1 tidak sama dengan alamat ERC-20 yang setara di L2.
Anda dapat melihat daftar alamat token di sini (opens in a new tab).
Alamat dengan chainId 1 berada di L1 (Mainnet) dan alamat dengan chainId 10 berada di L2 (Optimism).
Dua nilai chainId lainnya adalah untuk jaringan testnet Kovan (42) dan jaringan testnet Optimistic Kovan (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );Dimungkinkan untuk menambahkan catatan pada transfer, dalam hal ini catatan tersebut ditambahkan ke event yang melaporkannya.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );Kontrak jembatan yang sama menangani transfer di kedua arah. Dalam kasus jembatan L1, ini berarti inisialisasi deposit dan finalisasi penarikan.
1
2 /* *******************3 * Fungsi Publik *4 ******************* */5 /********************6 * Public Functions *7 ********************/8
9 /**10 * @dev mendapatkan alamat dari kontrak jembatan L2 yang sesuai.11 * @return Alamat dari kontrak jembatan L2 yang sesuai.12 */13 function l2TokenBridge() external returns (address);Fungsi ini sebenarnya tidak terlalu dibutuhkan, karena di L2 ini adalah kontrak yang sudah di-deploy sebelumnya (predeployed), sehingga selalu berada di alamat 0x4200000000000000000000000000000000000010.
Fungsi ini ada di sini untuk simetri dengan jembatan L2, karena alamat jembatan L1 tidak mudah untuk diketahui.
1 /**2 * @dev mendepositkan sejumlah ERC20 ke saldo pemanggil di L2.3 * @param _l1Token Alamat ERC20 L1 yang kita depositkan4 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L15 * @param _amount Jumlah ERC20 yang akan didepositkan6 * @param _l2Gas Batas gas yang diperlukan untuk menyelesaikan deposit di L2.7 * @param _data Data opsional untuk diteruskan ke L2. Data ini disediakan8 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan9 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.10 */11 function depositERC20(12 address _l1Token,13 address _l2Token,14 uint256 _amount,15 uint32 _l2Gas,16 bytes calldata _data17 ) external;Parameter _l2Gas adalah jumlah gas L2 yang diizinkan untuk dihabiskan oleh transaksi.
Hingga batas (tinggi) tertentu, ini gratis (opens in a new tab), jadi kecuali kontrak ERC-20 melakukan sesuatu yang sangat aneh saat melakukan mint, ini seharusnya tidak menjadi masalah.
Fungsi ini menangani skenario umum, di mana pengguna menjembatani aset ke alamat yang sama di blockchain yang berbeda.
1 /**2 * @dev mendepositkan sejumlah ERC20 ke saldo penerima di L2.3 * @param _l1Token Alamat ERC20 L1 yang kita depositkan4 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L15 * @param _to Alamat L2 untuk mengkreditkan penarikan.6 * @param _amount Jumlah ERC20 yang akan didepositkan.7 * @param _l2Gas Batas gas yang diperlukan untuk menyelesaikan deposit di L2.8 * @param _data Data opsional untuk diteruskan ke L2. Data ini disediakan9 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan10 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.11 */12 function depositERC20To(13 address _l1Token,14 address _l2Token,15 address _to,16 uint256 _amount,17 uint32 _l2Gas,18 bytes calldata _data19 ) external;Fungsi ini hampir identik dengan depositERC20, tetapi memungkinkan Anda mengirim ERC-20 ke alamat yang berbeda.
1 /* ************************2 * Fungsi Lintas-rantai *3 ************************ */4 /*************************5 * Cross-chain Functions *6 *************************/7
8 /**9 * @dev Menyelesaikan penarikan dari L2 ke L1, dan mengkreditkan dana ke saldo penerima dari10 * token ERC20 L1.11 * Panggilan ini akan gagal jika penarikan yang diinisialisasi dari L2 belum diselesaikan.12 *13 * @param _l1Token Alamat token L1 untuk finalizeWithdrawal.14 * @param _l2Token Alamat token L2 tempat penarikan diinisiasi.15 * @param _from Alamat L2 yang menginisiasi transfer.16 * @param _to Alamat L1 untuk mengkreditkan penarikan.17 * @param _amount Jumlah ERC20 yang akan didepositkan.18 * @param _data Data yang disediakan oleh pengirim di L2. Data ini disediakan19 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan20 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.21 */22 function finalizeERC20Withdrawal(23 address _l1Token,24 address _l2Token,25 address _from,26 address _to,27 uint256 _amount,28 bytes calldata _data29 ) external;30}Penarikan (dan pesan lain dari L2 ke L1) di Optimism adalah proses dua langkah:
- Transaksi inisiasi di L2.
- Transaksi finalisasi atau klaim di L1. Transaksi ini harus terjadi setelah periode tantangan kesalahan (fault challenge period) (opens in a new tab) untuk transaksi L2 berakhir.
IL1StandardBridge
Antarmuka ini didefinisikan di sini (opens in a new tab).
File ini berisi definisi event dan fungsi untuk ETH.
Definisi ini sangat mirip dengan yang didefinisikan dalam IL1ERC20Bridge di atas untuk ERC-20.
Antarmuka jembatan dibagi menjadi dua file karena beberapa token ERC-20 memerlukan pemrosesan kustom dan tidak dapat ditangani oleh jembatan standar.
Dengan cara ini, jembatan kustom yang menangani token semacam itu dapat mengimplementasikan IL1ERC20Bridge dan tidak perlu juga menjembatani ETH.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;3
4import "./IL1ERC20Bridge.sol";5
6/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /* *********11 * Event *12 ********* */13 /**********14 * Events *15 **********/16 event ETHDepositInitiated(17 address indexed _from,18 address indexed _to,19 uint256 _amount,20 bytes _data21 );Event ini hampir identik dengan versi ERC-20 (ERC20DepositInitiated), kecuali tanpa alamat token L1 dan L2.
Hal yang sama berlaku untuk event dan fungsi lainnya.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );6
7 /* *******************8 * Fungsi Publik *9 ******************* */10 /********************11 * Public Functions *12 ********************/13
14 /**15 * @dev Mendepositkan sejumlah ETH ke saldo pemanggil di L2.16 .17 .18 .19 */20 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;21
22 /**23 * @dev Mendepositkan sejumlah ETH ke saldo penerima di L2.24 .25 .26 .27 */28 function depositETHTo(29 address _to,30 uint32 _l2Gas,31 bytes calldata _data32 ) external payable;33
34 /* ************************35 * Fungsi Lintas-rantai *36 ************************ */37 /*************************38 * Cross-chain Functions *39 *************************/40
41 /**42 * @dev Menyelesaikan penarikan dari L2 ke L1, dan mengkreditkan dana ke saldo penerima dari43 * token ETH L1. Karena hanya xDomainMessenger yang dapat memanggil fungsi ini, fungsi ini tidak akan pernah dipanggil44 * sebelum penarikan diselesaikan.45 .46 .47 .48 */49 function finalizeETHWithdrawal(50 address _from,51 address _to,52 uint256 _amount,53 bytes calldata _data54 ) external;55}CrossDomainEnabled
Kontrak ini (opens in a new tab) diwarisi oleh kedua jembatan (L1 dan L2) untuk mengirim pesan ke layer lainnya.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;3
4/* Impor Antarmuka */5/* Interface Imports */6import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";Antarmuka ini (opens in a new tab) memberi tahu kontrak bagaimana cara mengirim pesan ke layer lainnya, menggunakan pengirim pesan lintas domain (cross domain messenger). Pengirim pesan lintas domain ini adalah sistem yang sama sekali berbeda, dan layak mendapatkan artikelnya sendiri, yang saya harap dapat ditulis di masa mendatang.
1/**2 * @title CrossDomainEnabled3 * @dev Kontrak pembantu untuk kontrak yang melakukan komunikasi lintas-domain4 *5 * Kompiler yang digunakan: ditentukan oleh kontrak yang mewarisi6 */7contract CrossDomainEnabled {8 /* ************9 * Variabel *10 ************ */11 /*************12 * Variables *13 *************/14
15 // Messenger contract used to send and receive messages from the other domain. // Kontrak Messenger yang digunakan untuk mengirim dan menerima pesan dari domain lain.16 address public messenger;17
18 /* **************19 * Konstruktor *20 ************** */21 /***************22 * Constructor *23 ***************/24
25 /**26 * @param _messenger Alamat CrossDomainMessenger di lapisan saat ini.27 */28 constructor(address _messenger) {29 messenger = _messenger;30 }Satu parameter yang perlu diketahui oleh kontrak, yaitu alamat pengirim pesan lintas domain di layer ini. Parameter ini diatur sekali, di dalam konstruktor, dan tidak pernah berubah.
1
2 /* *********************3 * Pengubah Fungsi *4 ********************* */5 /**********************6 * Function Modifiers *7 **********************/8
9 /**10 * Menegakkan bahwa fungsi yang diubah hanya dapat dipanggil oleh akun lintas-domain tertentu.11 * @param _sourceDomainAccount Satu-satunya akun di domain asal yang12 * diautentikasi untuk memanggil fungsi ini.13 */14 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Pesan lintas domain dapat diakses oleh kontrak apa pun di blockchain tempat ia berjalan (baik mainnet Ethereum maupun Optimism). Tetapi kita membutuhkan jembatan di setiap sisi untuk hanya mempercayai pesan tertentu jika pesan tersebut berasal dari jembatan di sisi lain.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: messenger contract unauthenticated"4 );Hanya pesan dari pengirim pesan lintas domain yang sesuai (messenger, seperti yang Anda lihat di bawah) yang dapat dipercaya.
1
2 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );Cara pengirim pesan lintas domain menyediakan alamat yang mengirim pesan dengan layer lainnya adalah melalui fungsi .xDomainMessageSender() (opens in a new tab).
Selama fungsi ini dipanggil dalam transaksi yang diinisiasi oleh pesan tersebut, ia dapat memberikan informasi ini.
Kita perlu memastikan bahwa pesan yang kita terima berasal dari jembatan lainnya.
1
2 _;3 }4
5 /* *********************6 * Fungsi Internal *7 ********************* */8 /**********************9 * Internal Functions *10 **********************/11
12 /**13 * Mendapatkan messenger, biasanya dari penyimpanan. Fungsi ini diekspos jika kontrak anak14 * perlu menimpanya.15 * @return Alamat kontrak messenger lintas-domain yang harus digunakan.16 */17 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {18 return ICrossDomainMessenger(messenger);19 }Fungsi ini mengembalikan pengirim pesan lintas domain.
Kita menggunakan fungsi daripada variabel messenger untuk memungkinkan kontrak yang mewarisi dari kontrak ini menggunakan algoritma untuk menentukan pengirim pesan lintas domain mana yang akan digunakan.
1
2 /**3 * Mengirim pesan ke akun di domain lain4 * @param _crossDomainTarget Penerima yang dituju di domain tujuan5 * @param _message Data yang akan dikirim ke target (biasanya calldata ke fungsi dengan6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit Batas gas untuk penerimaan pesan di domain tujuan.8 */9 function sendCrossDomainMessage(10 address _crossDomainTarget,11 uint32 _gasLimit,12 bytes memory _messageTerakhir, fungsi yang mengirim pesan ke layer lainnya.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benign // slither-disable-next-line reentrancy-events, reentrancy-benignSlither (opens in a new tab) adalah penganalisis statis yang dijalankan Optimism pada setiap kontrak untuk mencari kerentanan dan potensi masalah lainnya. Dalam kasus ini, baris berikut memicu dua kerentanan:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}Dalam kasus ini kita tidak khawatir tentang reentrancy karena kita tahu getCrossDomainMessenger() mengembalikan alamat yang dapat dipercaya, meskipun Slither tidak memiliki cara untuk mengetahuinya.
Kontrak jembatan L1
Kode sumber untuk kontrak ini ada di sini (opens in a new tab).
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;Antarmuka dapat menjadi bagian dari kontrak lain, sehingga mereka harus mendukung berbagai versi Solidity. Tetapi jembatan itu sendiri adalah kontrak kita, dan kita bisa bersikap ketat tentang versi Solidity apa yang digunakannya.
1/* Impor Antarmuka */2/* Interface Imports */3import { IL1StandardBridge } from "./IL1StandardBridge.sol";4import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";IL1ERC20Bridge dan IL1StandardBridge telah dijelaskan di atas.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";Antarmuka ini (opens in a new tab) memungkinkan kita membuat pesan untuk mengontrol jembatan standar di L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Antarmuka ini (opens in a new tab) memungkinkan kita mengontrol kontrak ERC-20. Anda dapat membaca lebih lanjut tentang hal ini di sini.
1/* Impor Pustaka */2/* Library Imports */3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";Seperti yang dijelaskan di atas, kontrak ini digunakan untuk pengiriman pesan antar-layer.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";Lib_PredeployAddresses (opens in a new tab) memiliki alamat untuk kontrak L2 yang selalu memiliki alamat yang sama. Ini termasuk jembatan standar di L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";Utilitas Address dari OpenZeppelin (opens in a new tab). Ini digunakan untuk membedakan antara alamat kontrak dan alamat yang dimiliki oleh akun yang dimiliki secara eksternal (EOA).
Perhatikan bahwa ini bukanlah solusi yang sempurna, karena tidak ada cara untuk membedakan antara pemanggilan langsung dan pemanggilan yang dilakukan dari konstruktor kontrak, tetapi setidaknya ini memungkinkan kita mengidentifikasi dan mencegah beberapa kesalahan pengguna yang umum.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";Standar ERC-20 (opens in a new tab) mendukung dua cara bagi kontrak untuk melaporkan kegagalan:
- Revert
- Mengembalikan
false
Menangani kedua kasus tersebut akan membuat kode kita lebih rumit, jadi sebagai gantinya kita menggunakan SafeERC20 dari OpenZeppelin (opens in a new tab), yang memastikan semua kegagalan menghasilkan revert (opens in a new tab).
1/**2 * @title L1StandardBridge3 * @dev Jembatan ETH dan ERC20 L1 adalah kontrak yang menyimpan dana L1 yang didepositkan dan token4 * standar yang digunakan di L2. Ini menyinkronkan jembatan L2 yang sesuai, menginformasikannya tentang deposit5 * dan mendengarkannya untuk penarikan yang baru saja diselesaikan.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Baris ini adalah cara kita menentukan untuk menggunakan pembungkus (wrapper) SafeERC20 setiap kali kita menggunakan antarmuka IERC20.
1
2 /* *******************************3 * Referensi Kontrak Eksternal *4 ******************************* */5 /********************************6 * External Contract References *7 ********************************/8
9 address public l2TokenBridge;Alamat dari L2StandardBridge.
1
2 // Maps L1 token to L2 token to balance of the L1 token deposited // Memetakan token L1 ke token L2 ke saldo token L1 yang didepositkan3 mapping(address => mapping(address => uint256)) public deposits;Mapping (opens in a new tab) ganda seperti ini adalah cara Anda mendefinisikan array jarang dua dimensi (two-dimensional sparse array) (opens in a new tab).
Nilai dalam struktur data ini diidentifikasi sebagai deposit[L1 token addr][L2 token addr].
Nilai defaultnya adalah nol.
Hanya sel yang diatur ke nilai yang berbeda yang ditulis ke penyimpanan.
1
2 /* **************3 * Konstruktor *4 ************** */5 /***************6 * Constructor *7 ***************/8
9 // This contract lives behind a proxy, so the constructor parameters will go unused. // Kontrak ini berada di balik proxy, sehingga parameter konstruktor tidak akan digunakan.10 constructor() CrossDomainEnabled(address(0)) {}Kita ingin dapat meningkatkan (upgrade) kontrak ini tanpa harus menyalin semua variabel di penyimpanan.
Untuk melakukannya kita menggunakan Proxy (opens in a new tab), sebuah kontrak yang menggunakan delegatecall (opens in a new tab) untuk mentransfer pemanggilan ke kontrak terpisah yang alamatnya disimpan oleh kontrak proxy (saat Anda melakukan upgrade, Anda memberi tahu proxy untuk mengubah alamat tersebut).
Saat Anda menggunakan delegatecall, penyimpanan tetap menjadi penyimpanan dari kontrak yang memanggil, sehingga nilai dari semua variabel status kontrak tidak terpengaruh.
Salah satu efek dari pola ini adalah bahwa penyimpanan dari kontrak yang dipanggil oleh delegatecall tidak digunakan dan oleh karena itu nilai konstruktor yang diteruskan kepadanya tidak menjadi masalah.
Inilah alasan kita dapat memberikan nilai yang tidak masuk akal ke konstruktor CrossDomainEnabled.
Ini juga alasan inisialisasi di bawah ini terpisah dari konstruktor.
1 /* *****************2 * Inisialisasi *3 ***************** */4 /******************5 * Initialization *6 ******************/7
8 /**9 * @param _l1messenger Alamat Messenger L1 yang digunakan untuk komunikasi lintas-rantai.10 * @param _l2TokenBridge Alamat jembatan standar L2.11 */12 // slither-disable-next-line external-function // slither-disable-next-line external-functionPengujian Slither (opens in a new tab) ini mengidentifikasi fungsi yang tidak dipanggil dari kode kontrak dan oleh karena itu dapat dideklarasikan sebagai external alih-alih public.
Biaya gas dari fungsi external bisa lebih rendah, karena mereka dapat diberikan parameter di dalam calldata.
Fungsi yang dideklarasikan sebagai public harus dapat diakses dari dalam kontrak.
Kontrak tidak dapat memodifikasi calldata mereka sendiri, sehingga parameter harus berada di memori.
Ketika fungsi semacam itu dipanggil secara eksternal, calldata perlu disalin ke memori, yang memakan biaya gas.
Dalam kasus ini, fungsi tersebut hanya dipanggil sekali, sehingga inefisiensi tersebut tidak menjadi masalah bagi kita.
1 function initialize(address _l1messenger, address _l2TokenBridge) public {2 require(messenger == address(0), "Contract has already been initialized.");Fungsi initialize hanya boleh dipanggil sekali.
Jika alamat pengirim pesan lintas domain L1 atau jembatan token L2 berubah, kita membuat proxy baru dan jembatan baru yang memanggilnya.
Ini tidak mungkin terjadi kecuali ketika seluruh sistem di-upgrade, sebuah kejadian yang sangat langka.
Perhatikan bahwa fungsi ini tidak memiliki mekanisme apa pun yang membatasi siapa yang dapat memanggilnya.
Ini berarti secara teori seorang penyerang dapat menunggu hingga kita men-deploy proxy dan versi pertama dari jembatan lalu melakukan front-run (opens in a new tab) untuk mencapai fungsi initialize sebelum pengguna yang sah melakukannya. Tetapi ada dua metode untuk mencegah hal ini:
- Jika kontrak di-deploy tidak secara langsung oleh EOA tetapi dalam transaksi yang membuat kontrak lain membuatnya (opens in a new tab), seluruh proses dapat bersifat atomik, dan selesai sebelum transaksi lain dieksekusi.
- Jika pemanggilan yang sah ke
initializegagal, selalu dimungkinkan untuk mengabaikan proxy dan jembatan yang baru dibuat dan membuat yang baru.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }Ini adalah dua parameter yang perlu diketahui oleh jembatan.
1
2 /* *************3 * Mendepositkan *4 ************* */5 /**************6 * Depositing *7 **************/8
9 /** @dev Pengubah yang mengharuskan pengirim adalah EOA. Pemeriksaan ini dapat dilewati oleh kontrak10 * berbahaya melalui initcode, tetapi ini menangani kesalahan pengguna yang ingin kita hindari.11 */12 modifier onlyEOA() {13 // Used to stop deposits from contracts (avoid accidentally lost tokens) // Digunakan untuk menghentikan deposit dari kontrak (menghindari token hilang secara tidak sengaja)14 require(!Address.isContract(msg.sender), "Account not EOA");15 _;16 }Inilah alasan kita membutuhkan utilitas Address dari OpenZeppelin.
1 /**2 * @dev Fungsi ini dapat dipanggil tanpa data3 * untuk mendepositkan sejumlah ETH ke saldo pemanggil di L2.4 * Karena fungsi receive tidak menerima data, jumlah default5 * yang konservatif diteruskan ke L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Fungsi ini ada untuk tujuan pengujian. Perhatikan bahwa fungsi ini tidak muncul dalam definisi antarmuka - ini bukan untuk penggunaan normal.
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }7
8 /**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 }Kedua fungsi ini adalah pembungkus (wrapper) di sekitar _initiateETHDeposit, fungsi yang menangani deposit ETH yang sebenarnya.
1 /**2 * @dev Melakukan logika untuk deposit dengan menyimpan ETH dan menginformasikan Gateway ETH L2 tentang3 * deposit tersebut.4 * @param _from Akun untuk menarik deposit di L1.5 * @param _to Akun untuk memberikan deposit di L2.6 * @param _l2Gas Batas gas yang diperlukan untuk menyelesaikan deposit di L2.7 * @param _data Data opsional untuk diteruskan ke L2. Data ini disediakan8 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan9 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.10 */11 function _initiateETHDeposit(12 address _from,13 address _to,14 uint32 _l2Gas,15 bytes memory _data16 ) internal {17 // Construct calldata for finalizeDeposit call // Membangun calldata untuk panggilan finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Cara kerja pesan lintas domain adalah kontrak tujuan dipanggil dengan pesan sebagai calldata-nya.
Kontrak Solidity selalu menginterpretasikan calldata mereka sesuai dengan
spesifikasi ABI (opens in a new tab).
Fungsi Solidity abi.encodeWithSelector (opens in a new tab) membuat calldata tersebut.
1 IL2ERC20Bridge.finalizeDeposit.selector,2 address(0),3 Lib_PredeployAddresses.OVM_ETH,4 _from,5 _to,6 msg.value,7 _data8 );Pesan di sini adalah untuk memanggil fungsi finalizeDeposit (opens in a new tab) dengan parameter berikut:
| Parameter | Nilai | Arti |
|---|---|---|
| _l1Token | address(0) | Nilai khusus yang mewakili ETH (yang bukan merupakan token ERC-20) di L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Kontrak L2 yang mengelola ETH di Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (kontrak ini hanya untuk penggunaan internal Optimism) |
| _from | _from | Alamat di L1 yang mengirimkan ETH |
| _to | _to | Alamat di L2 yang menerima ETH |
| amount | msg.value | Jumlah wei yang dikirim (yang telah dikirim ke jembatan) |
| _data | _data | Data tambahan untuk dilampirkan pada deposit |
1 // Send calldata into L2 // Mengirim calldata ke L22 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Kirim pesan melalui pengirim pesan lintas domain.
1 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Pancarkan (emit) event untuk memberi tahu aplikasi terdesentralisasi mana pun yang mendengarkan transfer ini.
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 }11
12 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Kedua fungsi ini adalah pembungkus di sekitar _initiateERC20Deposit, fungsi yang menangani deposit ERC-20 yang sebenarnya.
1 /**2 * @dev Melakukan logika untuk deposit dengan menginformasikan kontrak Token yang Didepositkan L23 * tentang deposit tersebut dan memanggil penangan untuk mengunci dana L1. (misalnya, transferFrom)4 *5 * @param _l1Token Alamat ERC20 L1 yang kita depositkan6 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L17 * @param _from Akun untuk menarik deposit di L18 * @param _to Akun untuk memberikan deposit di L29 * @param _amount Jumlah ERC20 yang akan didepositkan.10 * @param _l2Gas Batas gas yang diperlukan untuk menyelesaikan deposit di L2.11 * @param _data Data opsional untuk diteruskan ke L2. Data ini disediakan12 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan13 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.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 {Fungsi ini mirip dengan _initiateETHDeposit di atas, dengan beberapa perbedaan penting.
Perbedaan pertama adalah bahwa fungsi ini menerima alamat token dan jumlah yang akan ditransfer sebagai parameter.
Dalam kasus ETH, pemanggilan ke jembatan sudah mencakup transfer aset ke akun jembatan (msg.value).
1 // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future // Ketika deposit diinisiasi di L1, Jembatan L1 mentransfer dana ke dirinya sendiri untuk2 // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if // penarikan di masa mendatang. safeTransferFrom juga memeriksa apakah kontrak memiliki kode, sehingga ini akan gagal jika3 // _from is an EOA or address(0). // _from adalah EOA atau address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Transfer token ERC-20 mengikuti proses yang berbeda dari ETH:
- Pengguna (
_from) memberikan izin (allowance) kepada jembatan untuk mentransfer token yang sesuai. - Pengguna memanggil jembatan dengan alamat kontrak token, jumlah, dll.
- Jembatan mentransfer token (ke dirinya sendiri) sebagai bagian dari proses deposit.
Langkah pertama mungkin terjadi dalam transaksi yang terpisah dari dua langkah terakhir.
Namun, front-running bukanlah masalah karena dua fungsi yang memanggil _initiateERC20Deposit (depositERC20 dan depositERC20To) hanya memanggil fungsi ini dengan msg.sender sebagai parameter _from.
1 // Construct calldata for _l2Token.finalizeDeposit(_to, _amount) // Membangun calldata untuk _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 );11
12 // Send calldata into L2 // Mengirim calldata ke L213 // slither-disable-next-line reentrancy-events, reentrancy-benign // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);15
16 // slither-disable-next-line reentrancy-benign // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Tambahkan jumlah token yang didepositokan ke struktur data deposits.
Bisa jadi ada beberapa alamat di L2 yang sesuai dengan token ERC-20 L1 yang sama, sehingga tidak cukup menggunakan saldo jembatan dari token ERC-20 L1 untuk melacak deposit.
1
2 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }5
6 /* ************************7 * Fungsi Lintas-rantai *8 ************************ */9 /*************************10 * Cross-chain Functions *11 *************************/12
13 /**14 * @inheritdoc IL1StandardBridge15 */16 function finalizeETHWithdrawal(17 address _from,18 address _to,19 uint256 _amount,20 bytes calldata _dataJembatan L2 mengirim pesan ke pengirim pesan lintas domain L2 yang menyebabkan pengirim pesan lintas domain L1 memanggil fungsi ini (tentu saja, setelah transaksi yang memfinalisasi pesan (opens in a new tab) dikirimkan di L1).
1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Pastikan bahwa ini adalah pesan yang sah, berasal dari pengirim pesan lintas domain dan berawal dari jembatan token L2. Fungsi ini digunakan untuk menarik ETH dari jembatan, jadi kita harus memastikan bahwa fungsi ini hanya dipanggil oleh pemanggil yang berwenang.
1 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));Cara untuk mentransfer ETH adalah dengan memanggil penerima dengan jumlah wei di dalam msg.value.
1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");2
3 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Pancarkan event tentang penarikan tersebut.
1 }2
3 /**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) {Fungsi ini mirip dengan finalizeETHWithdrawal di atas, dengan perubahan yang diperlukan untuk token ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;Perbarui struktur data deposits.
1
2 // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer // Ketika penarikan diselesaikan di L1, Jembatan L1 mentransfer dana ke penarik3 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);5
6 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }9
10
11 /* ****************************12 * Sementara - Migrasi ETH *13 **************************** */14 /*****************************15 * Temporary - Migrating ETH *16 *****************************/17
18 /**19 * @dev Menambahkan saldo ETH ke akun. Ini dimaksudkan untuk memungkinkan ETH20 * dimigrasikan dari gateway lama ke gateway baru.21 * CATATAN: Ini dibiarkan hanya untuk satu peningkatan sehingga kita dapat menerima ETH yang dimigrasikan dari22 * kontrak lama23 */24 function donateETH() external payable {}25}Ada implementasi jembatan sebelumnya.
Ketika kita beralih dari implementasi tersebut ke implementasi ini, kita harus memindahkan semua aset.
Token ERC-20 bisa langsung dipindahkan.
Namun, untuk mentransfer ETH ke sebuah kontrak, Anda memerlukan persetujuan kontrak tersebut, yang mana itulah yang disediakan oleh donateETH kepada kita.
Token ERC-20 di L2
Agar token ERC-20 sesuai dengan jembatan standar, ia perlu mengizinkan jembatan standar, dan hanya jembatan standar, untuk melakukan mint token. Ini diperlukan karena jembatan perlu memastikan bahwa jumlah token yang beredar di Optimism sama dengan jumlah token yang terkunci di dalam kontrak jembatan L1. Jika ada terlalu banyak token di L2, beberapa pengguna tidak akan dapat menjembatani aset mereka kembali ke L1. Alih-alih jembatan yang tepercaya, kita pada dasarnya akan menciptakan kembali perbankan cadangan fraksional (fractional reserve banking) (opens in a new tab). Jika ada terlalu banyak token di L1, beberapa dari token tersebut akan tetap terkunci di dalam kontrak jembatan selamanya karena tidak ada cara untuk melepaskannya tanpa membakar token L2.
IL2StandardERC20
Setiap token ERC-20 di L2 yang menggunakan jembatan standar perlu menyediakan antarmuka ini (opens in a new tab), yang memiliki fungsi dan event yang dibutuhkan oleh jembatan standar.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Antarmuka standar ERC-20 (opens in a new tab) tidak menyertakan fungsi mint dan burn.
Metode-metode tersebut tidak diwajibkan oleh standar ERC-20 (opens in a new tab), yang membiarkan mekanisme untuk membuat dan menghancurkan token tidak ditentukan.
1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";Antarmuka ERC-165 (opens in a new tab) digunakan untuk menentukan fungsi apa saja yang disediakan oleh sebuah kontrak. Anda dapat membaca standarnya di sini (opens in a new tab).
1interface IL2StandardERC20 is IERC20, IERC165 {2 function l1Token() external returns (address);Fungsi ini menyediakan alamat token L1 yang dijembatani ke kontrak ini. Perhatikan bahwa kita tidak memiliki fungsi serupa ke arah yang berlawanan. Kita harus dapat menjembatani token L1 apa pun, terlepas dari apakah dukungan L2 direncanakan saat diimplementasikan atau tidak.
1
2 function mint(address _to, uint256 _amount) external;3
4 function burn(address _from, uint256 _amount) external;5
6 event Mint(address indexed _account, uint256 _amount);7 event Burn(address indexed _account, uint256 _amount);8}Fungsi dan event untuk melakukan mint (membuat) dan membakar (menghancurkan) token. Jembatan harus menjadi satu-satunya entitas yang dapat menjalankan fungsi-fungsi ini untuk memastikan jumlah token sudah benar (sama dengan jumlah token yang terkunci di L1).
L2StandardERC20
Ini adalah implementasi kita dari antarmuka IL2StandardERC20 (opens in a new tab).
Kecuali Anda memerlukan semacam logika kustom, Anda harus menggunakan yang ini.
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";Kontrak ERC-20 OpenZeppelin (opens in a new tab). Optimism tidak percaya pada penemuan kembali roda (reinventing the wheel), terutama ketika roda tersebut telah diaudit dengan baik dan harus cukup tepercaya untuk menyimpan aset.
1import "./IL2StandardERC20.sol";2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;Ini adalah dua parameter konfigurasi tambahan yang kita perlukan dan biasanya tidak diperlukan oleh ERC-20.
1
2 /**3 * @param _l2Bridge Alamat jembatan standar L2.4 * @param _l1Token Alamat token L1 yang sesuai.5 * @param _name Nama ERC20.6 * @param _symbol Simbol 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 }Pertama panggil konstruktor untuk kontrak yang kita warisi (ERC20(_name, _symbol)) dan kemudian atur variabel kita sendiri.
1
2 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }6
7
8 // slither-disable-next-line external-function // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Inilah cara kerja ERC-165 (opens in a new tab). Setiap antarmuka adalah sejumlah fungsi yang didukung, dan diidentifikasi sebagai exclusive or (XOR) (opens in a new tab) dari pemilih fungsi ABI (ABI function selectors) (opens in a new tab) dari fungsi-fungsi tersebut.
Jembatan L2 menggunakan ERC-165 sebagai pemeriksaan kewarasan (sanity check) untuk memastikan bahwa kontrak ERC-20 tempat ia mengirim aset adalah IL2StandardERC20.
Catatan: Tidak ada yang mencegah kontrak nakal memberikan jawaban palsu ke supportsInterface, jadi ini adalah mekanisme pemeriksaan kewarasan, bukan mekanisme keamanan.
1 // slither-disable-next-line external-function // slither-disable-next-line external-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);4
5 emit Mint(_to, _amount);6 }7
8 // slither-disable-next-line external-function // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);11
12 emit Burn(_from, _amount);13 }14}Hanya jembatan L2 yang diizinkan untuk melakukan mint dan membakar aset.
_mint dan _burn sebenarnya didefinisikan dalam kontrak ERC-20 OpenZeppelin.
Kontrak tersebut hanya tidak mengeksposnya secara eksternal, karena kondisi untuk melakukan mint dan membakar token sama bervariasinya dengan jumlah cara untuk menggunakan ERC-20.
Kode Jembatan L2
Ini adalah kode yang menjalankan jembatan di Optimism. Sumber untuk kontrak ini ada di sini (opens in a new tab).
1// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3
4/* Impor Antarmuka */5/* Interface Imports */6import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";7import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";8import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";Antarmuka IL2ERC20Bridge (opens in a new tab) sangat mirip dengan padanan L1 yang kita lihat di atas. Ada dua perbedaan signifikan:
- Di L1 Anda menginisiasi deposit dan memfinalisasi penarikan. Di sini Anda menginisiasi penarikan dan memfinalisasi deposit.
- Di L1 perlu untuk membedakan antara ETH dan token ERC-20. Di L2 kita dapat menggunakan fungsi yang sama untuk keduanya karena secara internal saldo ETH di Optimism ditangani sebagai token ERC-20 dengan alamat 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab).
1/* Impor Pustaka */2/* Library Imports */3import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";4import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";5import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";6
7/* Impor Kontrak */8/* Contract Imports */9import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";10
11/**12 * @title L2StandardBridge13 * @dev Jembatan Standar L2 adalah kontrak yang bekerja sama dengan jembatan Standar L1 untuk14 * memungkinkan transisi ETH dan ERC20 antara L1 dan L2.15 * Kontrak ini bertindak sebagai minter untuk token baru ketika mendengar tentang deposit ke jembatan16 * Standar L1.17 * Kontrak ini juga bertindak sebagai pembakar token yang ditujukan untuk penarikan, menginformasikan jembatan18 * L1 untuk melepaskan dana L1.19 */20contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {21 /* *******************************22 * Referensi Kontrak Eksternal *23 ******************************* */24 /********************************25 * External Contract References *26 ********************************/27
28 address public l1TokenBridge;Lacak alamat jembatan L1. Perhatikan bahwa berbeda dengan padanan L1, di sini kita membutuhkan variabel ini. Alamat jembatan L1 tidak diketahui sebelumnya.
1
2 /* **************3 * Konstruktor *4 ************** */5 /***************6 * Constructor *7 ***************/8
9 /**10 * @param _l2CrossDomainMessenger Messenger lintas-domain yang digunakan oleh kontrak ini.11 * @param _l1TokenBridge Alamat jembatan L1 yang disebarkan ke rantai utama.12 */13 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)14 CrossDomainEnabled(_l2CrossDomainMessenger)15 {16 l1TokenBridge = _l1TokenBridge;17 }18
19 /* **************20 * Penarikan *21 ************** */22 /***************23 * Withdrawing *24 ***************/25
26 /**27 * @inheritdoc IL2ERC20Bridge28 */29 function withdraw(30 address _l2Token,31 uint256 _amount,32 uint32 _l1Gas,33 bytes calldata _data34 ) external virtual {35 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);36 }37
38 /**39 * @inheritdoc IL2ERC20Bridge40 */41 function withdrawTo(42 address _l2Token,43 address _to,44 uint256 _amount,45 uint32 _l1Gas,46 bytes calldata _data47 ) external virtual {48 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);49 }Kedua fungsi ini menginisiasi penarikan. Perhatikan bahwa tidak perlu menentukan alamat token L1. Token L2 diharapkan memberi tahu kita alamat padanan L1-nya.
1
2 /**3 * @dev Melakukan logika untuk penarikan dengan membakar token dan menginformasikan4 * Gateway token L1 tentang penarikan tersebut.5 * @param _l2Token Alamat token L2 tempat penarikan diinisiasi.6 * @param _from Akun untuk menarik penarikan di L2.7 * @param _to Akun untuk memberikan penarikan di L1.8 * @param _amount Jumlah token yang akan ditarik.9 * @param _l1Gas Tidak digunakan, tetapi disertakan untuk pertimbangan kompatibilitas ke depan yang potensial.10 * @param _data Data opsional untuk diteruskan ke L1. Data ini disediakan11 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan12 * panjang maksimum, kontrak ini tidak memberikan jaminan tentang kontennya.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 // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 // Ketika penarikan diinisiasi, kami membakar dana penarik untuk mencegah penggunaan L223 // usage // selanjutnya24 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Perhatikan bahwa kita tidak mengandalkan parameter _from melainkan pada msg.sender yang jauh lebih sulit untuk dipalsukan (tidak mungkin, sejauh yang saya tahu).
1
2 // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) // Membangun calldata untuk l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {Di L1 perlu untuk membedakan antara ETH dan 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 }19
20 // Send message up to L1 bridge // Mengirim pesan ke jembatan L121 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);23
24 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }27
28 /* ***********************************29 * Fungsi Lintas-rantai: Mendepositkan *30 *********************************** */31 /************************************32 * Cross-chain Function: Depositing *33 ************************************/34
35 /**36 * @inheritdoc IL2ERC20Bridge37 */38 function finalizeDeposit(39 address _l1Token,40 address _l2Token,41 address _from,42 address _to,43 uint256 _amount,44 bytes calldata _dataFungsi ini dipanggil oleh L1StandardBridge.
1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {Pastikan sumber pesan tersebut sah.
Ini penting karena fungsi ini memanggil _mint dan dapat digunakan untuk memberikan token yang tidak dicakup oleh token yang dimiliki jembatan di L1.
1 // Check the target token is compliant and // Memeriksa apakah token target mematuhi dan2 // verify the deposited token on L1 matches the L2 deposited token representation here // memverifikasi token yang didepositkan di L1 cocok dengan representasi token yang didepositkan di L2 di sini3 if (4 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Pemeriksaan kewarasan (sanity checks):
- Antarmuka yang benar didukung
- Alamat L1 dari kontrak ERC-20 L2 cocok dengan sumber L1 dari token tersebut
1 ) {2 // When a deposit is finalized, we credit the account on L2 with the same amount of // Ketika deposit diselesaikan, kami mengkreditkan akun di L2 dengan jumlah yang sama dari3 // tokens. // token.4 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Jika pemeriksaan kewarasan berhasil, finalisasi deposit:
- Mint token
- Pancarkan event yang sesuai
1 } else {2 // Either the L2 token which is being deposited-into disagrees about the correct address // Entah token L2 yang sedang didepositkan tidak setuju tentang alamat yang benar3 // of its L1 token, or does not support the correct interface. // dari token L1-nya, atau tidak mendukung antarmuka yang benar.4 // This should only happen if there is a malicious L2 token, or if a user somehow // Ini seharusnya hanya terjadi jika ada token L2 yang berbahaya, atau jika pengguna entah bagaimana5 // specified the wrong L2 token address to deposit into. // menentukan alamat token L2 yang salah untuk didepositkan.6 // In either case, we stop the process here and construct a withdrawal // Dalam kedua kasus tersebut, kami menghentikan proses di sini dan membangun pesan7 // message so that users can get their funds out in some cases. // penarikan sehingga pengguna bisa mengeluarkan dana mereka dalam beberapa kasus.8 // There is no way to prevent malicious token contracts altogether, but this does limit // Tidak ada cara untuk mencegah kontrak token berbahaya sepenuhnya, tetapi ini membatasi9 // user error and mitigate some forms of malicious contract behavior. // kesalahan pengguna dan memitigasi beberapa bentuk perilaku kontrak yang berbahaya.Jika pengguna membuat kesalahan yang dapat dideteksi dengan menggunakan alamat token L2 yang salah, kita ingin membatalkan deposit dan mengembalikan token di L1. Satu-satunya cara kita dapat melakukan ini dari L2 adalah dengan mengirim pesan yang harus menunggu periode tantangan kesalahan, tetapi itu jauh lebih baik bagi pengguna daripada kehilangan token secara permanen.
1 bytes memory message = abi.encodeWithSelector(2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,3 _l1Token,4 _l2Token,5 _to, // switched the _to and _from here to bounce back the deposit to the sender // menukar _to dan _from di sini untuk memantulkan kembali deposit ke pengirim6 _from,7 _amount,8 _data9 );10
11 // Send message up to L1 bridge // Mengirim pesan ke jembatan L112 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Kesimpulan
Jembatan standar adalah mekanisme paling fleksibel untuk transfer aset. Namun, karena sangat generik, ini tidak selalu menjadi mekanisme termudah untuk digunakan. Terutama untuk penarikan, sebagian besar pengguna lebih suka menggunakan jembatan pihak ketiga (opens in a new tab) yang tidak menunggu periode tantangan dan tidak memerlukan bukti Merkle untuk memfinalisasi penarikan.
Jembatan ini biasanya bekerja dengan memiliki aset di L1, yang mereka sediakan segera dengan biaya kecil (seringkali kurang dari biaya gas untuk penarikan jembatan standar). Ketika jembatan (atau orang yang menjalankannya) mengantisipasi kekurangan aset L1, ia mentransfer aset yang cukup dari L2. Karena ini adalah penarikan yang sangat besar, biaya penarikan diamortisasi dalam jumlah besar dan persentasenya jauh lebih kecil.
Semoga artikel ini membantu Anda lebih memahami tentang cara kerja layer 2, dan cara menulis kode Solidity yang jelas dan aman.
Lihat di sini untuk karya saya yang lain (opens in a new tab).
Pembaruan terakhir halaman: 3 Maret 2026