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.

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT

Sebagian besar kode Optimism dirilis di bawah lisensi MIT (opens in a new tab).

pragma 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.

Dalam terminologi jembatan Optimism, deposit berarti transfer dari L1 ke L2, dan withdrawal (penarikan) berarti transfer dari L2 ke L1.

        address indexed _l1Token,
        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).

        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

Dimungkinkan untuk menambahkan catatan pada transfer, dalam hal ini catatan tersebut ditambahkan ke event yang melaporkannya.

    event ERC20WithdrawalFinalized(
        address indexed _l1Token,
        address indexed _l2Token,
        address indexed _from,
        address _to,
        uint256 _amount,
        bytes _data
    );

Kontrak jembatan yang sama menangani transfer di kedua arah. Dalam kasus jembatan L1, ini berarti inisialisasi deposit dan finalisasi penarikan.

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.

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.

Fungsi ini hampir identik dengan depositERC20, tetapi memungkinkan Anda mengirim ERC-20 ke alamat yang berbeda.

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.

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.

CrossDomainEnabled

Kontrak ini (opens in a new tab) diwarisi oleh kedua jembatan (L1 dan L2) untuk mengirim pesan ke layer lainnya.

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity >0.5.0 <0.9.0;

/* Impor Antarmuka */
/* Interface Imports */
import { 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.

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.

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.

        require(
            msg.sender == address(getCrossDomainMessenger()),
            "OVM_XCHAIN: messenger contract unauthenticated"
        );

Hanya pesan dari pengirim pesan lintas domain yang sesuai (messenger, seperti yang Anda lihat di bawah) yang dapat dipercaya.


        require(
            getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
            "OVM_XCHAIN: wrong sender of cross-domain message"
        );

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.

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.

Terakhir, fungsi yang mengirim pesan ke layer lainnya.

    ) internal {
        // 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)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

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).

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma 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.

/* Impor Antarmuka */
/* Interface Imports */
import { IL1StandardBridge } from "./IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge dan IL1StandardBridge telah dijelaskan di atas.

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

Antarmuka ini (opens in a new tab) memungkinkan kita membuat pesan untuk mengontrol jembatan standar di L2.

import { 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.

/* Impor Pustaka */
/* Library Imports */
import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Seperti yang dijelaskan di atas, kontrak ini digunakan untuk pengiriman pesan antar-layer.

import { 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.

import { 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.

import { 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).

Baris ini adalah cara kita menentukan untuk menggunakan pembungkus (wrapper) SafeERC20 setiap kali kita menggunakan antarmuka IERC20.

Alamat dari L2StandardBridge.


    // 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
    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.

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.

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.

    function initialize(address _l1messenger, address _l2TokenBridge) public {
        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.
        messenger = _l1messenger;
        l2TokenBridge = _l2TokenBridge;
    }

Ini adalah dua parameter yang perlu diketahui oleh jembatan.

Inilah alasan kita membutuhkan utilitas Address dari OpenZeppelin.

Fungsi ini ada untuk tujuan pengujian. Perhatikan bahwa fungsi ini tidak muncul dalam definisi antarmuka - ini bukan untuk penggunaan normal.

Kedua fungsi ini adalah pembungkus (wrapper) di sekitar _initiateETHDeposit, fungsi yang menangani deposit ETH yang sebenarnya.

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.

            IL2ERC20Bridge.finalizeDeposit.selector,
            address(0),
            Lib_PredeployAddresses.OVM_ETH,
            _from,
            _to,
            msg.value,
            _data
        );

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
        // Send calldata into L2 // Mengirim calldata ke L2
        // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Kirim pesan melalui pengirim pesan lintas domain.

        // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
        emit ETHDepositInitiated(_from, _to, msg.value, _data);
    }

Pancarkan (emit) event untuk memberi tahu aplikasi terdesentralisasi mana pun yang mendengarkan transfer ini.

Kedua fungsi ini adalah pembungkus di sekitar _initiateERC20Deposit, fungsi yang menangani deposit ERC-20 yang sebenarnya.

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).

        // 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
        // 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
        // _from is an EOA or address(0). // _from adalah EOA atau address(0).
        // slither-disable-next-line reentrancy-events, reentrancy-benign // slither-disable-next-line reentrancy-events, reentrancy-benign
        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.

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.

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).

    ) 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.

        // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
        (bool success, ) = _to.call{ value: _amount }(new bytes(0));

Cara untuk mentransfer ETH adalah dengan memanggil penerima dengan jumlah wei di dalam msg.value.

        require(success, "TransferHelper::safeTransferETH: ETH transfer failed");

        // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
        emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Pancarkan event tentang penarikan tersebut.

Fungsi ini mirip dengan finalizeETHWithdrawal di atas, dengan perubahan yang diperlukan untuk token ERC-20.

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

Perbarui struktur data deposits.

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.

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { 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.

import { 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).

interface IL2StandardERC20 is IERC20, IERC165 {
    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.


    function mint(address _to, uint256 _amount) external;

    function burn(address _from, uint256 _amount) external;

    event Mint(address indexed _account, uint256 _amount);
    event Burn(address indexed _account, uint256 _amount);
}

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.

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import { 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.

import "./IL2StandardERC20.sol";

contract L2StandardERC20 is IL2StandardERC20, ERC20 {
    address public l1Token;
    address public l2Bridge;

Ini adalah dua parameter konfigurasi tambahan yang kita perlukan dan biasanya tidak diperlukan oleh ERC-20.

Pertama panggil konstruktor untuk kontrak yang kita warisi (ERC20(_name, _symbol)) dan kemudian atur variabel kita sendiri.

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.

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).

// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/* Impor Antarmuka */
/* Interface Imports */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { 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).

Lacak alamat jembatan L1. Perhatikan bahwa berbeda dengan padanan L1, di sini kita membutuhkan variabel ini. Alamat jembatan L1 tidak diketahui sebelumnya.

Kedua fungsi ini menginisiasi penarikan. Perhatikan bahwa tidak perlu menentukan alamat token L1. Token L2 diharapkan memberi tahu kita alamat padanan L1-nya.

Perhatikan bahwa kita tidak mengandalkan parameter _from melainkan pada msg.sender yang jauh lebih sulit untuk dipalsukan (tidak mungkin, sejauh yang saya tahu).


        // Construct calldata for l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) // Membangun calldata untuk l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
        // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

Di L1 perlu untuk membedakan antara ETH dan ERC-20.

Fungsi ini dipanggil oleh L1StandardBridge.

    ) 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.

        // Check the target token is compliant and // Memeriksa apakah token target mematuhi dan
        // 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
        if (
            // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _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
        ) {
            // 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
            // tokens. // token.
            // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Jika pemeriksaan kewarasan berhasil, finalisasi deposit:

  1. Mint token
  2. Pancarkan event yang sesuai

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.

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 April 2026

Apakah tutorial ini bermanfaat?