Lompat ke konten utama

Panduan kontrak jembatan standar Optimism

Solidity
jembatan
layer 2
Menengah
Ori Pomerantz
30 Maret 2022
34 menit baca

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

  1. Jika mendepositokan ERC-20, pendeposit memberikan jembatan izin (allowance) untuk membelanjakan jumlah yang didepositokan
  2. Pendeposit memanggil jembatan L1 (depositERC20, depositERC20To, depositETH, atau depositETHTo)
  3. 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
  4. Jembatan L1 menggunakan mekanisme pesan lintas domain untuk memanggil finalizeDeposit pada jembatan L2

Layer 2

  1. Jembatan L2 memverifikasi bahwa pemanggilan ke finalizeDeposit adalah sah:
    • Berasal dari kontrak pesan lintas domain
    • Awalnya berasal dari jembatan di L1
  2. Jembatan L2 memeriksa apakah kontrak token ERC-20 di L2 adalah yang benar:
  3. 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

  1. Penarik memanggil jembatan L2 (withdraw atau withdrawTo)
  2. Jembatan L2 membakar jumlah token yang sesuai milik msg.sender
  3. Jembatan L2 menggunakan mekanisme pesan lintas domain untuk memanggil finalizeETHWithdrawal atau finalizeERC20Withdrawal pada jembatan L1

Layer 1

  1. Jembatan L1 memverifikasi bahwa pemanggilan ke finalizeETHWithdrawal atau finalizeERC20Withdrawal adalah sah:
    • Berasal dari mekanisme pesan lintas domain
    • Awalnya berasal dari jembatan di L2
  2. 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: MIT

Sebagian 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 IL1ERC20Bridge
3 */
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 _data
5 );

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

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 depositkan
4 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L1
5 * @param _amount Jumlah ERC20 yang akan didepositkan
6 * @param _l2Gas Batas gas yang diperlukan untuk menyelesaikan deposit di L2.
7 * @param _data Data opsional untuk diteruskan ke L2. Data ini disediakan
8 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
9 * 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 _data
17 ) 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 depositkan
4 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L1
5 * @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 disediakan
9 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
10 * 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 _data
19 ) 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 dari
10 * 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 disediakan
19 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
20 * 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 _data
29 ) external;
30}

Penarikan (dan pesan lain dari L2 ke L1) di Optimism adalah proses dua langkah:

  1. Transaksi inisiasi di L2.
  2. 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: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4import "./IL1ERC20Bridge.sol";
5
6/**
7 * @title IL1StandardBridge
8 */
9interface IL1StandardBridge is IL1ERC20Bridge {
10 /* *********
11 * Event *
12 ********* */
13 /**********
14 * Events *
15 **********/
16 event ETHDepositInitiated(
17 address indexed _from,
18 address indexed _to,
19 uint256 _amount,
20 bytes _data
21 );

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 _data
32 ) 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 dari
43 * token ETH L1. Karena hanya xDomainMessenger yang dapat memanggil fungsi ini, fungsi ini tidak akan pernah dipanggil
44 * sebelum penarikan diselesaikan.
45 .
46 .
47 .
48 */
49 function finalizeETHWithdrawal(
50 address _from,
51 address _to,
52 uint256 _amount,
53 bytes calldata _data
54 ) 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: MIT
2pragma 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 CrossDomainEnabled
3 * @dev Kontrak pembantu untuk kontrak yang melakukan komunikasi lintas-domain
4 *
5 * Kompiler yang digunakan: ditentukan oleh kontrak yang mewarisi
6 */
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 yang
12 * 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 anak
14 * 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 lain
4 * @param _crossDomainTarget Penerima yang dituju di domain tujuan
5 * @param _message Data yang akan dikirim ke target (biasanya calldata ke fungsi dengan
6 * `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 _message

Terakhir, 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-benign

Slither (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. Event reentrancy (opens in a new tab)
  2. Reentrancy jinak (benign) (opens in a new tab)
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: MIT
2pragma 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:

  1. Revert
  2. 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 L1StandardBridge
3 * @dev Jembatan ETH dan ERC20 L1 adalah kontrak yang menyimpan dana L1 yang didepositkan dan token
4 * standar yang digunakan di L2. Ini menyinkronkan jembatan L2 yang sesuai, menginformasikannya tentang deposit
5 * 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 didepositkan
3 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-function

Pengujian 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:

  1. 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.
  2. Jika pemanggilan yang sah ke initialize gagal, 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 kontrak
10 * 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 data
3 * untuk mendepositkan sejumlah ETH ke saldo pemanggil di L2.
4 * Karena fungsi receive tidak menerima data, jumlah default
5 * 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 IL1StandardBridge
3 */
4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
6 }
7
8 /**
9 * @inheritdoc IL1StandardBridge
10 */
11 function depositETHTo(
12 address _to,
13 uint32 _l2Gas,
14 bytes calldata _data
15 ) external payable {
16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
17 }

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 tentang
3 * 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 disediakan
8 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
9 * 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 _data
16 ) internal {
17 // Construct calldata for finalizeDeposit call // Membangun calldata untuk panggilan finalizeDeposit
18 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 _data
8 );

Pesan di sini adalah untuk memanggil fungsi finalizeDeposit (opens in a new tab) dengan parameter berikut:

ParameterNilaiArti
_l1Tokenaddress(0)Nilai khusus yang mewakili ETH (yang bukan merupakan token ERC-20) di L1
_l2TokenLib_PredeployAddresses.OVM_ETHKontrak L2 yang mengelola ETH di Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (kontrak ini hanya untuk penggunaan internal Optimism)
_from_fromAlamat di L1 yang mengirimkan ETH
_to_toAlamat di L2 yang menerima ETH
amountmsg.valueJumlah wei yang dikirim (yang telah dikirim ke jembatan)
_data_dataData tambahan untuk dilampirkan pada deposit
1 // Send calldata into L2 // Mengirim calldata ke L2
2 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Kirim pesan melalui pengirim pesan lintas domain.

1 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
2 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 IL1ERC20Bridge
3 */
4 function depositERC20(
5 .
6 .
7 .
8 ) external virtual onlyEOA {
9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
10 }
11
12 /**
13 * @inheritdoc IL1ERC20Bridge
14 */
15 function depositERC20To(
16 .
17 .
18 .
19 ) external virtual {
20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
21 }

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 L2
3 * tentang deposit tersebut dan memanggil penangan untuk mengunci dana L1. (misalnya, transferFrom)
4 *
5 * @param _l1Token Alamat ERC20 L1 yang kita depositkan
6 * @param _l2Token Alamat ERC20 L2 yang masing-masing dari L1
7 * @param _from Akun untuk menarik deposit di L1
8 * @param _to Akun untuk memberikan deposit di L2
9 * @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 disediakan
12 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
13 * 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 _data
23 ) 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 untuk
2 // 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 jika
3 // _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-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

Transfer token ERC-20 mengikuti proses yang berbeda dari ETH:

  1. Pengguna (_from) memberikan izin (allowance) kepada jembatan untuk mentransfer token yang sesuai.
  2. Pengguna memanggil jembatan dengan alamat kontrak token, jumlah, dll.
  3. 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 _data
10 );
11
12 // Send calldata into L2 // Mengirim calldata ke L2
13 // slither-disable-next-line reentrancy-events, reentrancy-benign // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign // slither-disable-next-line reentrancy-benign
17 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-events
3 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 IL1StandardBridge
15 */
16 function finalizeETHWithdrawal(
17 address _from,
18 address _to,
19 uint256 _amount,
20 bytes calldata _data

Jembatan 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-events
2 (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-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Pancarkan event tentang penarikan tersebut.

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

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 penarik
3 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
4 IERC20(_l1Token).safeTransfer(_to, _amount);
5
6 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
7 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 ETH
20 * dimigrasikan dari gateway lama ke gateway baru.
21 * CATATAN: Ini dibiarkan hanya untuk satu peningkatan sehingga kita dapat menerima ETH yang dimigrasikan dari
22 * kontrak lama
23 */
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: MIT
2pragma 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: MIT
2pragma 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 _symbol
13 ) 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-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 // ERC165
11 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-function
2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {
3 _mint(_to, _amount);
4
5 emit Mint(_to, _amount);
6 }
7
8 // slither-disable-next-line external-function // slither-disable-next-line external-function
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}

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: MIT
2pragma 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:

  1. Di L1 Anda menginisiasi deposit dan memfinalisasi penarikan. Di sini Anda menginisiasi penarikan dan memfinalisasi deposit.
  2. 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 L2StandardBridge
13 * @dev Jembatan Standar L2 adalah kontrak yang bekerja sama dengan jembatan Standar L1 untuk
14 * memungkinkan transisi ETH dan ERC20 antara L1 dan L2.
15 * Kontrak ini bertindak sebagai minter untuk token baru ketika mendengar tentang deposit ke jembatan
16 * Standar L1.
17 * Kontrak ini juga bertindak sebagai pembakar token yang ditujukan untuk penarikan, menginformasikan jembatan
18 * 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 IL2ERC20Bridge
28 */
29 function withdraw(
30 address _l2Token,
31 uint256 _amount,
32 uint32 _l1Gas,
33 bytes calldata _data
34 ) external virtual {
35 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
36 }
37
38 /**
39 * @inheritdoc IL2ERC20Bridge
40 */
41 function withdrawTo(
42 address _l2Token,
43 address _to,
44 uint256 _amount,
45 uint32 _l1Gas,
46 bytes calldata _data
47 ) 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 menginformasikan
4 * 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 disediakan
11 * semata-mata sebagai kemudahan untuk kontrak eksternal. Selain dari menegakkan
12 * 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 _data
21 ) 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 L2
23 // usage // selanjutnya
24 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
25 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-events
4 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 _data
7 );
8 } else {
9 message = abi.encodeWithSelector(
10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
11 l1Token,
12 _l2Token,
13 _from,
14 _to,
15 _amount,
16 _data
17 );
18 }
19
20 // Send message up to L1 bridge // Mengirim pesan ke jembatan L1
21 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
25 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 IL2ERC20Bridge
37 */
38 function finalizeDeposit(
39 address _l1Token,
40 address _l2Token,
41 address _from,
42 address _to,
43 uint256 _amount,
44 bytes calldata _data

Fungsi 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 dan
2 // 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 sini
3 if (
4 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Pemeriksaan kewarasan (sanity checks):

  1. Antarmuka yang benar didukung
  2. 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 dari
3 // tokens. // token.
4 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Jika pemeriksaan kewarasan berhasil, finalisasi deposit:

  1. Mint token
  2. 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 benar
3 // 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 bagaimana
5 // 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 pesan
7 // 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 membatasi
9 // 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 pengirim
6 _from,
7 _amount,
8 _data
9 );
10
11 // Send message up to L1 bridge // Mengirim pesan ke jembatan L1
12 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
15 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

Apakah tutorial ini membantu?