Lompat ke konten utama

Panduan kontrak jembatan standar Optimism

Solidity
jembatan
lapisan 2
Menengah
Ori Pomerantz
30 Maret 2022
30 menit baca

Optimism (opens in a new tab) adalah sebuah rollup Optimistic. Rollup Optimistic dapat memproses transaksi dengan harga yang jauh lebih rendah daripada Mainnet Ethereum (juga dikenal sebagai lapisan 1 atau l1) karena transaksi hanya diproses oleh beberapa node, alih-alih 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 lapisan 2 (l2) lainnya), aset tersebut perlu dijembatani. Salah satu cara untuk mencapainya adalah dengan 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 menjembataninya kembali ke l1. 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 guna melihat 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

Lapisan 1

  1. Jika mendepositkan ERC-20, pendeposit memberikan jatah kepada jembatan untuk membelanjakan jumlah yang didepositkan
  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 panggilan
    • ERC-20: Aset ditransfer oleh jembatan ke dirinya sendiri menggunakan jatah yang diberikan oleh pendeposit
  4. Jembatan l1 menggunakan mekanisme pesan lintas domain untuk memanggil finalizeDeposit di jembatan l2

Lapisan 2

  1. Jembatan l2 memverifikasi bahwa panggilan 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 mencetak jumlah token yang sesuai ke alamat yang sesuai. Jika tidak, mulai proses penarikan untuk memungkinkan pengguna mengklaim token di l1.

Alur penarikan

Lapisan 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 di jembatan l1

Lapisan 1

  1. Jembatan l1 memverifikasi bahwa panggilan 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 lapisan 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

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

pragma solidity >0.5.0 <0.9.0;

Pada 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 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 pengujian Kovan (42) dan jaringan pengujian 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 peristiwa 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 diperlukan, karena di l2 ini adalah kontrak yang telah disebarkan sebelumnya, sehingga selalu berada di alamat 0x4200000000000000000000000000000000000010. 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 benar-benar aneh saat pencetakan, ini seharusnya tidak menjadi masalah. Fungsi ini menangani skenario umum, di mana pengguna menjembatani aset ke alamat yang sama di rantai blok 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 perlu terjadi setelah periode tantangan kesalahan (opens in a new tab) untuk transaksi l2 berakhir.

IL1StandardBridge

Antarmuka ini didefinisikan di sini (opens in a new tab). File ini berisi definisi peristiwa 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.

Peristiwa ini hampir identik dengan versi ERC-20 (ERC20DepositInitiated), kecuali tanpa alamat token l1 dan l2. Hal yang sama berlaku untuk peristiwa dan fungsi lainnya.

CrossDomainEnabled

Kontrak ini (opens in a new tab) diwarisi oleh kedua jembatan (l1 dan l2) untuk mengirim pesan ke lapisan lainnya.

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

/* Impor Antarmuka */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Antarmuka ini (opens in a new tab) memberi tahu kontrak cara mengirim pesan ke lapisan lain, menggunakan pengirim pesan lintas domain. 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 kontrak, alamat pengirim pesan lintas domain di lapisan ini. Parameter ini ditetapkan sekali, di dalam konstruktor, dan tidak pernah berubah.

Pesan lintas domain dapat diakses oleh kontrak apa pun di rantai blok tempat ia berjalan (baik Mainnet Ethereum atau Optimism). Namun kita memerlukan jembatan di setiap sisi untuk hanya memercayai 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 lapisan lain adalah fungsi .xDomainMessageSender() (opens in a new tab). Selama dipanggil dalam transaksi yang diinisiasi oleh pesan tersebut, ia dapat memberikan informasi ini.

Kita perlu memastikan bahwa pesan yang kita terima berasal dari jembatan lain.

Fungsi ini mengembalikan pengirim pesan lintas domain. Kita menggunakan fungsi alih-alih 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 lapisan lain.

    ) internal {
        // 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. Peristiwa reentransi (opens in a new tab)
  2. Reentransi jinak (opens in a new tab)
        getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
    }
}

Dalam kasus ini kita tidak khawatir tentang reentransi, 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
pragma solidity ^0.8.9;

Antarmuka dapat menjadi bagian dari kontrak lain, sehingga harus mendukung berbagai versi Solidity. Namun jembatan itu sendiri adalah kontrak kita, dan kita bisa bersikap ketat tentang versi Solidity apa yang digunakannya.

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

IL1ERC20Bridge dan IL1StandardBridge 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 itu di sini.

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

Seperti yang dijelaskan di atas, kontrak ini digunakan untuk pengiriman pesan antarlapisan.

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 Alamat OpenZeppelin (opens in a new tab). Ini digunakan untuk membedakan antara alamat kontrak dan alamat milik akun yang dimiliki secara eksternal (EOA).

Perhatikan bahwa ini bukanlah solusi yang sempurna, karena tidak ada cara untuk membedakan antara panggilan langsung dan panggilan yang dibuat 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. Mengembalikan
  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 pengembalian (opens in a new tab).

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


    /********************************
     * Referensi Kontrak Eksternal *
     ********************************/

    address public l2TokenBridge;

Alamat L2StandardBridge.


    // Memetakan token l1 ke token l2 ke saldo token l1 yang didepositkan
    mapping(address => mapping(address => uint256)) public deposits;

Pemetaan (opens in a new tab) ganda seperti ini adalah cara Anda mendefinisikan array jarang dua dimensi (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 berbeda yang ditulis ke penyimpanan.


    /***************
     * Konstruktor *
     ***************/

    // Kontrak ini berada di balik proksi, sehingga parameter konstruktor tidak akan digunakan.
    constructor() CrossDomainEnabled(address(0)) {}

Untuk dapat meningkatkan 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 panggilan ke kontrak terpisah yang alamatnya disimpan oleh kontrak proksi (saat Anda meningkatkan, Anda memberi tahu proksi untuk mengubah alamat tersebut). Saat Anda menggunakan delegatecall, penyimpanan tetap menjadi penyimpanan kontrak yang memanggil, sehingga nilai semua variabel state kontrak tidak terpengaruh.

Salah satu efek dari pola ini adalah bahwa penyimpanan kontrak yang dipanggil dari 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 dalam data panggilan. Fungsi yang dideklarasikan sebagai public harus dapat diakses dari dalam kontrak. Kontrak tidak dapat memodifikasi data panggilannya sendiri, sehingga parameter harus berada di memori. Ketika fungsi semacam itu dipanggil secara eksternal, data panggilan perlu disalin ke memori, yang membutuhkan biaya gas. Dalam kasus ini, fungsi hanya dipanggil sekali, sehingga inefisiensi 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 proksi baru dan jembatan baru yang memanggilnya. Hal ini tidak mungkin terjadi kecuali ketika seluruh sistem ditingkatkan, kejadian yang sangat langka.

Perhatikan bahwa fungsi ini tidak memiliki mekanisme apa pun yang membatasi siapa yang dapat memanggilnya. Ini berarti bahwa secara teori seorang penyerang dapat menunggu hingga kita menyebarkan proksi dan versi pertama jembatan lalu melakukan front-running (opens in a new tab) untuk mencapai fungsi initialize sebelum pengguna yang sah melakukannya. Namun ada dua metode untuk mencegah hal ini:

  1. Jika kontrak disebarkan tidak secara langsung oleh EOA melainkan dalam transaksi yang meminta kontrak lain untuk membuatnya (opens in a new tab), seluruh proses dapat bersifat atomik, dan selesai sebelum transaksi lain dieksekusi.
  2. Jika panggilan yang sah ke initialize gagal, selalu dimungkinkan untuk mengabaikan proksi 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 ini tidak muncul dalam definisi antarmuka - ini bukan untuk penggunaan normal.

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

Cara kerja pesan lintas domain adalah kontrak tujuan dipanggil dengan pesan sebagai data panggilannya. Kontrak Solidity selalu menafsirkan data panggilannya sesuai dengan spesifikasi ABI (opens in a new tab). Fungsi Solidity abi.encodeWithSelector (opens in a new tab) membuat data panggilan 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 untuk mewakili ETH (yang bukan 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
        // Kirim data panggilan ke l2
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Kirim pesan melalui pengirim pesan lintas domain.

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

Pancarkan peristiwa untuk memberi tahu aplikasi terdesentralisasi (dapp) 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, panggilan ke jembatan sudah mencakup transfer aset ke akun jembatan (msg.value).

        // Ketika deposit diinisialisasi di l1, jembatan l1 mentransfer dana ke dirinya sendiri untuk
        // penarikan di masa mendatang. safeTransferFrom juga memeriksa apakah kontrak memiliki kode, sehingga ini akan gagal jika
        // _from adalah EOA atau alamat(0).
        // 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 jatah 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 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 didepositkan ke struktur data deposits. Mungkin 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 fungsi ini hanya dipanggil oleh pemanggil yang berwenang.

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

Cara mentransfer ETH adalah dengan memanggil penerima dengan jumlah Wei di msg.value.

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

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

Pancarkan peristiwa tentang penarikan.

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 dapat dipindahkan begitu saja. Namun, untuk mentransfer ETH ke kontrak, Anda memerlukan persetujuan kontrak tersebut, yang mana itulah yang diberikan oleh donateETH kepada kita.

Token ERC-20 di L2

Agar token ERC-20 sesuai dengan jembatan standar, token tersebut perlu mengizinkan jembatan standar, dan hanya jembatan standar, untuk mencetak 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 tepercaya, pada dasarnya kita akan menciptakan kembali perbankan cadangan fraksional (opens in a new tab). Jika ada terlalu banyak token di l1, beberapa 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 peristiwa yang dibutuhkan jembatan standar.

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

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Antarmuka ERC-20 standar (opens in a new tab) tidak menyertakan fungsi mint dan burn. 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 yang disediakan oleh 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 peristiwa untuk mencetak (membuat) dan membakar (menghancurkan) token. Jembatan harus menjadi satu-satunya entitas yang dapat menjalankan 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
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, 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 (opens in a new tab) dari pemilih fungsi ABI (opens in a new tab) dari fungsi-fungsi tersebut.

Jembatan l2 menggunakan ERC-165 sebagai pemeriksaan kewarasan 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 mencetak dan membakar aset.

_mint dan _burn sebenarnya didefinisikan dalam kontrak ERC-20 OpenZeppelin. Kontrak tersebut hanya tidak mengeksposnya secara eksternal, karena kondisi untuk mencetak 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
pragma solidity ^0.8.9;

/* Impor Antarmuka */
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.

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


        // Bangun data panggilan untuk l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)
        // 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.

        // Periksa token target mematuhi dan
        // verifikasi token yang didepositkan di l1 cocok dengan representasi token yang didepositkan l2 di sini
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

Pemeriksaan kewarasan:

  1. Antarmuka yang benar didukung
  2. Alamat l1 kontrak ERC-20 l2 cocok dengan sumber l1 dari token tersebut
        ) {
            // Ketika deposit diselesaikan, kami mengkreditkan akun di l2 dengan jumlah yang sama dari
            // token.
            // slither-disable-next-line reentrancy-events
            IL2StandardERC20(_l2Token).mint(_to, _amount);
            // slither-disable-next-line reentrancy-events
            emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Jika pemeriksaan kewarasan berhasil, finalisasi deposit:

  1. Cetak token
  2. Pancarkan peristiwa 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 sedikit biaya (sering kali 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 lapisan 2, dan cara menulis kode Solidity yang jelas dan aman.

Lihat di sini untuk karya saya yang lain (opens in a new tab).