Ana içeriğe geç

Optimism standart köprü sözleşmesine genel bakış

solidity
köprü
katman 2
Orta düzey
Ori Pomerantz
30 Mart 2022
28 dakikalık okuma

Optimism (opens in a new tab) bir İyimser Toplama türüdür. İyimser toplamalar, işlemler ağdaki her düğüm yerine yalnızca birkaç düğüm tarafından işlendiği için Ethereum Ana Ağı'ndan (katman 1 veya K1 olarak da bilinir) çok daha düşük bir fiyata işlem yapabilir. Aynı zamanda, verilerin tümü K1'e yazılır, böylece her şey Ana Ağ'ın tüm bütünlük ve kullanılabilirlik garantileriyle kanıtlanabilir ve yeniden yapılandırılabilir.

Optimism'de (veya başka herhangi bir K2'de) K1 varlıklarını kullanmak için varlıkların köprülenmesi gerekir. Bunu başarmanın yollarından biri, kullanıcıların K1'de varlıkları (en yaygın olanları ETH ve ERC-20 token'larıdır) kilitlemesi ve K2'de kullanmak üzere eşdeğer varlıklar almasıdır. Nihayetinde, bu varlıkları elinde bulunduranlar onları K1'e geri köprülemek isteyebilir. Bunu yaparken, varlıklar K2'de yakılır ve ardından K1'de kullanıcıya geri verilir.

Optimism standart köprüsü (opens in a new tab) bu şekilde çalışır. Bu makalede, nasıl çalıştığını görmek için söz konusu köprünün kaynak kodunu gözden geçirecek ve onu, iyi yazılmış bir Solidity kodu örneği olarak inceleyeceğiz.

Kontrol akışları

Köprünün iki ana akışı vardır:

  • Yatırma (K1'den K2'ye)
  • Çekme (K2'den K1'e)

Yatırma akışı

Katman 1

  1. Bir ERC-20 yatırılırken, yatıran kişi köprüye yatırılan tutarı harcaması için bir izin verir.
  2. Yatıran kişi K1 köprüsünü çağırır (depositERC20, depositERC20To, depositETH veya depositETHTo)
  3. K1 köprüsü köprülenen varlığın mülkiyetini alır.
    • ETH: Varlık, çağrının bir parçası olarak yatıran kişi tarafından aktarılır.
    • ERC-20: Varlık, yatıran kişi tarafından sağlanan izni kullanarak köprü tarafından kendisine aktarılır.
  4. K1 köprüsü, K2 köprüsünde finalizeDeposit fonksiyonunu çağırmak için alanlar arası mesaj mekanizmasını kullanır.

Katman 2

  1. K2 köprüsü, finalizeDeposit çağrısının meşru olduğunu doğrular:
    • Alanlar arası mesaj sözleşmesinden gelmiştir
    • Aslen K1'deki köprüden gelmiştir
  2. K2 köprüsü, K2 üzerindeki ERC-20 token sözleşmesinin doğru olup olmadığını kontrol eder:
    • K2 sözleşmesi, K1'deki karşılığının, token'ların K1'den geldiği sözleşmeyle aynı olduğunu bildirir.
    • K2 sözleşmesi, doğru arayüzü desteklediğini bildirir (ERC-165 (opens in a new tab) kullanarak).
  3. K2 sözleşmesi doğruysa, uygun adrese uygun sayıda token basması için bu sözleşme çağrılır. Değilse, kullanıcının K1'deki token'ları talep etmesine olanak tanıyan bir çekme işlemi başlatılır.

Çekme akışı

Katman 2

  1. Çekme işlemini yapan kişi K2 köprüsünü çağırır (withdraw veya withdrawTo)
  2. K2 köprüsü, msg.sender'a ait uygun sayıda token'ı yakar.
  3. K2 köprüsü, K1 köprüsünde finalizeETHWithdrawal veya finalizeERC20Withdrawal fonksiyonlarını çağırmak için alanlar arası mesaj mekanizmasını kullanır.

Katman 1

  1. K1 köprüsü, finalizeETHWithdrawal veya finalizeERC20Withdrawal çağrısının meşru olduğunu doğrular:
    • Alanlar arası mesaj mekanizmasından gelmiştir
    • Aslen K2'deki köprüden gelmiştir
  2. K1 köprüsü, uygun varlığı (ETH veya ERC-20) uygun adrese aktarır.

Katman 1 kodu

Bu, K1'de, yani Ethereum Ana Ağı'nda çalışan koddur.

IL1ERC20Bridge

Bu arayüz burada tanımlanmıştır (opens in a new tab). ERC-20 token'larını köprülemek için gereken fonksiyonları ve tanımları içerir.

1// SPDX-License-Identifier: MIT

Optimism'in kodlarının çoğu MIT lisansı altında yayınlanmıştır (opens in a new tab).

1pragma solidity >0.5.0 <0.9.0;

Bu yazı yazıldığı sırada Solidity'nin en son sürümü 0.8.12'dir. Sürüm 0.9.0 yayınlanana kadar bu kodun onunla uyumlu olup olmadığını bilemeyiz.

1/**
2 * @title IL1ERC20Bridge
3 */
4interface IL1ERC20Bridge {
5 /**********
6 * Olaylar *
7 **********/
8
9 event ERC20DepositInitiated(
Tümünü göster

Optimism köprü terminolojisinde yatırma işlemi K1'den K2'ye transfer, çekme işlemi ise K2'den K1'e transfer anlamına gelir.

1 address indexed _l1Token,
2 address indexed _l2Token,

Çoğu durumda bir ERC-20'nin K1'deki adresi, K2'deki eşdeğer ERC-20'nin adresiyle aynı değildir. Token adreslerinin listesini buradan görebilirsiniz (opens in a new tab). chainId'si 1 olan adres K1'de (Ana Ağ), chainId'si 10 olan adres ise K2'dedir (Optimism). Diğer iki chainId değeri Kovan test ağı (42) ve Optimistic Kovan test ağı (69) içindir.

1 address indexed _from,
2 address _to,
3 uint256 _amount,
4 bytes _data
5 );

Transferlere not eklemek mümkündür, bu durumda bu notlar onları bildiren olaylara eklenir.

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

Aynı köprü sözleşmesi her iki yöndeki transferleri de yönetir. K1 köprüsü söz konusu olduğunda bu, yatırma işlemlerinin başlatılması ve çekme işlemlerinin sonlandırılması anlamına gelir.

1
2 /********************
3 * Herkese Açık Fonksiyonlar *
4 ********************/
5
6 /**
7 * @dev ilgili K2 köprü sözleşmesinin adresini alır.
8 * @return İlgili K2 köprü sözleşmesinin adresi.
9 */
10 function l2TokenBridge() external returns (address);
Tümünü göster

Bu fonksiyona aslında gerek yoktur çünkü K2'de bu, önceden dağıtılmış bir sözleşmedir ve bu nedenle her zaman 0x4200000000000000000000000000000000000010 adresindedir. K1 köprüsünün adresini bilmek kolay olmadığından, bu fonksiyon K2 köprüsüyle simetri sağlamak için buradadır.

1 /**
2 * @dev bir miktar ERC20'yi K2'deki arayanın bakiyesine yatırır.
3 * @param _l1Token Yatırmakta olduğumuz K1 ERC20'sinin adresi
4 * @param _l2Token İlgili K2 ERC20'sinin adresi
5 * @param _amount Yatırılacak ERC20 miktarı
6 * @param _l2Gas K2'de yatırma işlemini tamamlamak için gereken gaz limiti.
7 * @param _data K2'ye iletilecek isteğe bağlı veri. Bu veri yalnızca
8 * harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
9 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
10 */
11 function depositERC20(
12 address _l1Token,
13 address _l2Token,
14 uint256 _amount,
15 uint32 _l2Gas,
16 bytes calldata _data
17 ) external;
Tümünü göster

_l2Gas parametresi, işlemin harcamasına izin verilen K2 gazı miktarıdır. Belirli bir (yüksek) sınıra kadar bu ücretsizdir (opens in a new tab), bu nedenle ERC-20 sözleşmesi basım sırasında gerçekten garip bir şey yapmadığı sürece bu bir sorun olmamalıdır. Bu fonksiyon, bir kullanıcının varlıkları farklı bir blokzincirdeki aynı adrese köprülediği yaygın senaryoyu ele alır.

1 /**
2 * @dev bir miktar ERC20'yi K2'deki bir alıcının bakiyesine yatırır.
3 * @param _l1Token Yatırmakta olduğumuz K1 ERC20'sinin adresi
4 * @param _l2Token İlgili K2 ERC20'sinin adresi
5 * @param _to Çekme işleminin yatırılacağı K2 adresi.
6 * @param _amount Yatırılacak ERC20 miktarı.
7 * @param _l2Gas K2'de yatırma işlemini tamamlamak için gereken gaz limiti.
8 * @param _data K2'ye iletilecek isteğe bağlı veri. Bu veri yalnızca
9 * harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
10 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
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;
Tümünü göster

Bu fonksiyon depositERC20 ile neredeyse aynıdır, ancak ERC-20'yi farklı bir adrese göndermenize olanak tanır.

1 /*************************
2 * Zincirler Arası Fonksiyonlar *
3 *************************/
4
5 /**
6 * @dev K2'den K1'e bir çekme işlemini tamamlar ve fonları alıcının
7 * K1 ERC20 token bakiyesine yatırır.
8 * K2'den başlatılan çekme işlemi sonlandırılmamışsa bu çağrı başarısız olur.
9 *
10 * @param _l1Token finalizeWithdrawal işleminin yapılacağı K1 token'ının adresi.
11 * @param _l2Token Çekme işleminin başlatıldığı K2 token'ının adresi.
12 * @param _from Transferi başlatan K2 adresi.
13 * @param _to Çekme işleminin yatırılacağı K1 adresi.
14 * @param _amount Yatırılacak ERC20 miktarı.
15 * @param _data K2'deki gönderici tarafından sağlanan veri. Bu veri
16 * yalnızca harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
17 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
18 */
19 function finalizeERC20Withdrawal(
20 address _l1Token,
21 address _l2Token,
22 address _from,
23 address _to,
24 uint256 _amount,
25 bytes calldata _data
26 ) external;
27}
Tümünü göster

Optimism'de çekme işlemleri (ve K2'den K1'e diğer mesajlar) iki adımlı bir süreçtir:

  1. K2'de bir başlatma işlemi.
  2. K1'de bir sonlandırma veya talep etme işlemi. Bu işlemin, K2 işlemi için hata itiraz süresi (opens in a new tab) sona erdikten sonra gerçekleşmesi gerekir.

IL1StandardBridge

Bu arayüz burada tanımlanmıştır (opens in a new tab). Bu dosya, ETH için olay ve fonksiyon tanımlarını içerir. Bu tanımlar, yukarıda ERC-20 için IL1ERC20Bridge içinde tanımlananlara çok benzer.

Köprü arayüzü iki dosyaya bölünmüştür çünkü bazı ERC-20 token'ları özel işlem gerektirir ve standart köprü tarafından işlenemez. Bu şekilde, böyle bir token'ı işleyen özel köprü, IL1ERC20Bridge'i uygulayabilir ve ayrıca ETH köprülemek zorunda kalmaz.

1// 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 * Olaylar *
12 **********/
13 event ETHDepositInitiated(
14 address indexed _from,
15 address indexed _to,
16 uint256 _amount,
17 bytes _data
18 );
Tümünü göster

Bu olay, K1 ve K2 token adresleri dışında ERC-20 sürümüyle (ERC20DepositInitiated) neredeyse aynıdır. Aynısı diğer olaylar ve fonksiyonlar için de geçerlidir.

1 event ETHWithdrawalFinalized(
2 .
3 .
4 .
5 );
6
7 /********************
8 * Herkese Açık Fonksiyonlar *
9 ********************/
10
11 /**
12 * @dev Bir miktar ETH'yi K2'deki çağıranın bakiyesine yatırır.
13 .
14 .
15 .
16 */
17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;
18
19 /**
20 * @dev Bir miktar ETH'yi K2'deki bir alıcının bakiyesine yatırır.
21 .
22 .
23 .
24 */
25 function depositETHTo(
26 address _to,
27 uint32 _l2Gas,
28 bytes calldata _data
29 ) external payable;
30
31 /*************************
32 * Zincirler Arası Fonksiyonlar *
33 *************************/
34
35 /**
36 * @dev K2'den K1'e bir çekme işlemini tamamlar ve fonları alıcının
37 * K1 ETH token bakiyesine yatırır. Bu fonksiyonu yalnızca xDomainMessenger çağırabildiğinden, çekme işlemi
38 * sonlandırılmadan asla çağrılmaz.
39 .
40 .
41 .
42 */
43 function finalizeETHWithdrawal(
44 address _from,
45 address _to,
46 uint256 _amount,
47 bytes calldata _data
48 ) external;
49}
Tümünü göster

CrossDomainEnabled

Bu sözleşme (opens in a new tab), diğer katmana mesaj göndermek için her iki köprü (K1 ve K2) tarafından miras alınır.

1// SPDX-License-Identifier: MIT
2pragma solidity >0.5.0 <0.9.0;
3
4/* Arayüz İçe Aktarmaları */
5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Bu arayüz (opens in a new tab), alanlar arası mesajcıyı kullanarak sözleşmeye diğer katmana nasıl mesaj gönderileceğini bildirir. Bu alanlar arası mesajcı tamamen ayrı bir sistemdir ve gelecekte yazmayı umduğum kendi makalesini hak etmektedir.

1/**
2 * @title CrossDomainEnabled
3 * @dev Alanlar arası iletişim gerçekleştiren sözleşmeler için yardımcı sözleşme
4 *
5 * Kullanılan derleyici: miras alan sözleşme tarafından tanımlanır
6 */
7contract CrossDomainEnabled {
8 /*************
9 * Değişkenler *
10 *************/
11
12 // Diğer alandan mesaj göndermek ve almak için kullanılan Mesajcı sözleşmesi.
13 address public messenger;
14
15 /***************
16 * Yapıcı *
17 ***************/
18
19 /**
20 * @param _messenger Mevcut katmandaki CrossDomainMessenger'ın adresi.
21 */
22 constructor(address _messenger) {
23 messenger = _messenger;
24 }
Tümünü göster

Sözleşmenin bilmesi gereken tek parametre, bu katmandaki alanlar arası mesajcının adresidir. Bu parametre yapılandırıcıda bir kez ayarlanır ve asla değişmez.

1
2 /**********************
3 * Fonksiyon Değiştiricileri *
4 **********************/
5
6 /**
7 * Değiştirilen fonksiyonun yalnızca belirli bir alanlar arası hesap tarafından çağrılabilir olmasını zorunlu kılar.
8 * @param _sourceDomainAccount Kaynak alandaki, bu fonksiyonu çağırmak için
9 * doğrulanmış tek hesap.
10 */
11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {
Tümünü göster

Alanlar arası mesajlaşma, çalıştığı blokzincirdeki (Ethereum Ana Ağı veya Optimism) herhangi bir sözleşme tarafından erişilebilirdir. Ancak her iki taraftaki köprünün de, belirli mesajlara yalnızca diğer taraftaki köprüden geldiklerinde güvenmesi gerekir.

1 require(
2 msg.sender == address(getCrossDomainMessenger()),
3 "OVM_XCHAIN: mesajcı sözleşmesi doğrulanmadı"
4 );

Yalnızca uygun alanlar arası mesajcıdan (messenger, aşağıda gördüğünüz gibi) gelen mesajlara güvenilebilir.

1
2 require(
3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,
4 "OVM_XCHAIN: alanlar arası mesajın yanlış göndericisi"
5 );

Alanlar arası mesajcının, diğer katmandan bir mesaj gönderen adresi sağlama şekli, .xDomainMessageSender() fonksiyonudur (opens in a new tab). Mesaj tarafından başlatılan işlemde çağrıldığı sürece bu bilgiyi sağlayabilir.

Aldığımız mesajın diğer köprüden geldiğinden emin olmalıyız.

1
2 _;
3 }
4
5 /**********************
6 * Dahili Fonksiyonlar *
7 **********************/
8
9 /**
10 * Genellikle depolamadan mesajcıyı alır. Bu fonksiyon, bir alt sözleşmenin
11 * geçersiz kılması gerekmesi durumunda kullanıma sunulur.
12 * @return Kullanılması gereken alanlar arası mesajcı sözleşmesinin adresi.
13 */
14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {
15 return ICrossDomainMessenger(messenger);
16 }
Tümünü göster

Bu fonksiyon, alanlar arası mesajcıyı döndürür. Bundan miras alan sözleşmelerin hangi alanlar arası mesajcıyı kullanacağını belirtmek için bir algoritma kullanmasına izin vermek amacıyla messenger değişkeni yerine bir fonksiyon kullanırız.

1
2 /**
3 * Başka bir alandaki bir hesaba mesaj gönderir
4 * @param _crossDomainTarget Hedef alandaki amaçlanan alıcı
5 * @param _message Hedefe gönderilecek veri (genellikle `onlyFromCrossDomainAccount()` içeren bir fonksiyona
6 * çağrı verisi)
7 * @param _gasLimit Hedef alandaki mesajın alınması için gaz limiti.
8 */
9 function sendCrossDomainMessage(
10 address _crossDomainTarget,
11 uint32 _gasLimit,
12 bytes memory _message
Tümünü göster

Son olarak, diğer katmana mesaj gönderen fonksiyon.

1 ) internal {
2 // slither-disable-next-line reentrancy-events, reentrancy-benign

Slither (opens in a new tab), Optimism'in güvenlik açıklarını ve diğer potansiyel sorunları aramak için her sözleşmede çalıştırdığı bir statik analizördür. Bu durumda, aşağıdaki satır iki güvenlik açığını tetikler:

  1. Yeniden giriş olayları (opens in a new tab)
  2. Zararsız yeniden giriş (opens in a new tab)
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
2 }
3}

Bu durumda yeniden giriş konusunda endişelenmiyoruz çünkü Slither'ın bunu bilmesinin bir yolu olmasa bile getCrossDomainMessenger()'ın güvenilir bir adres döndürdüğünü biliyoruz.

K1 köprü sözleşmesi

Bu sözleşmenin kaynak kodu buradadır (opens in a new tab).

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

Arayüzler diğer sözleşmelerin bir parçası olabilir, bu nedenle çok çeşitli Solidity sürümlerini desteklemeleri gerekir. Ancak köprünün kendisi bizim sözleşmemizdir ve hangi Solidity sürümünü kullandığı konusunda katı olabiliriz.

1/* Arayüz İçe Aktarmaları */
2import { IL1StandardBridge } from "./IL1StandardBridge.sol";
3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";

IL1ERC20Bridge ve IL1StandardBridge yukarıda açıklanmıştır.

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

Bu arayüz (opens in a new tab) K2'deki standart köprüyü kontrol etmek için mesajlar oluşturmamızı sağlar.

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

Bu arayüz (opens in a new tab) ERC-20 sözleşmelerini kontrol etmemizi sağlar. Buradan daha fazlasını okuyabilirsiniz.

1/* Kütüphane İçe Aktarmaları */
2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";

Yukarıda açıklandığı gibi, bu sözleşme katmanlar arası mesajlaşma için kullanılır.

1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";

Lib_PredeployAddresses (opens in a new tab) her zaman aynı adrese sahip olan K2 sözleşmelerinin adreslerini içerir. Buna K2'deki standart köprü de dahildir.

1import { Address } from "@openzeppelin/contracts/utils/Address.sol";

OpenZeppelin'in Adres yardımcı programları (opens in a new tab). Sözleşme adresleri ile harici olarak sahip olunan hesaplara (EOA) ait olanlar arasında ayrım yapmak için kullanılır.

Bunun mükemmel bir çözüm olmadığını unutmayın. Bir sözleşmenin yapıcısı tarafından yapılan çağrılar ve doğrudan çağrıların ayrımını yapmanın bir yolu yoktur ama bu en azından bazı yaygın kullanıcı hatalarını tespit etmemizi ve önlememizi sağlar.

1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

ERC-20 standardı (opens in a new tab) bir sözleşmenin hata bildirmesi için iki yolu destekler:

  1. Geri Al
  2. false döndür

Her iki durumu da ele almak kodumuzu daha karmaşık hale getirirdi, bu yüzden bunun yerine tüm hataların bir geri almaya neden olmasını sağlayan (opens in a new tab) OpenZeppelin'in SafeERC20'sini (opens in a new tab) kullanıyoruz.

1/**
2 * @title L1StandardBridge
3 * @dev L1 ETH ve ERC20 Köprüsü, yatırılan L1 fonlarını ve K2'de kullanımda olan standart
4 * token'ları saklayan bir sözleşmedir. İlgili bir K2 Köprüsü ile senkronize olur, ona yatırma işlemleri hakkında bilgi verir
5 * ve yeni sonuçlandırılan çekme işlemleri için onu dinler.
6 *
7 */
8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
9 using SafeERC20 for IERC20;
Tümünü göster

Bu satır, IERC20 arayüzünü her kullandığımızda SafeERC20 sarmalayıcısını kullanmayı nasıl belirlediğimizdir.

1
2 /********************************
3 * Harici Sözleşme Referansları *
4 ********************************/
5
6 address public l2TokenBridge;

L2StandardBridge adresi.

1
2 // K1 token'ını K2 token'ına, yatırılan K1 token bakiyesine eşler
3 mapping(address => mapping(address => uint256)) public deposits;

Bunun gibi çift eşleme (opens in a new tab), iki boyutlu seyrek bir dizi (opens in a new tab) tanımlama yöntemidir. Bu veri yapısındaki değerler deposit[K1 token adresi][K2 token adresi] olarak tanımlanır. Varsayılan değer sıfırdır. Yalnızca farklı bir değere ayarlanmış hücreler depolamaya yazılır.

1
2 /***************
3 * Yapıcı *
4 ***************/
5
6 // Bu sözleşme bir proxy arkasında bulunur, bu nedenle yapıcı parametreleri kullanılmayacaktır.
7 constructor() CrossDomainEnabled(address(0)) {}

Depolamadaki tüm değişkenleri kopyalamak zorunda kalmadan bu sözleşmeyi yükseltebilmek istiyoruz. Bunu yapmak için bir Proxy (opens in a new tab) kullanıyoruz. Bu, çağrıları adresi proxy sözleşmesi tarafından saklanan ayrı bir sözleşmeye aktarmak için delegatecall (opens in a new tab) kullanan bir sözleşmedir (yükselttiğinizde proxy'ye bu adresi değiştirmesini söylersiniz). delegatecall kullandığınızda, depolama alanı çağırma sözleşmesinin deposu olarak kalır, bu nedenle tüm sözleşme durumu değişkenlerinin değerleri etkilenmez.

delegatecall'un çağrılanı olan sözleşmenin depolamasının kullanılmaması ve bu nedenle ona iletilen yapıcı değerlerinin önemli olmaması, bu modelin etkilerinden biridir. CrossDomainEnabled yapıcısına anlamsız bir değer sağlayabilmemizin nedeni budur. Aşağıdaki başlatmanın yapıcıdan ayrı olmasının nedeni de budur.

1 /******************
2 * Başlatma *
3 ******************/
4
5 /**
6 * @param _l1messenger Zincirler arası iletişim için kullanılan K1 Mesajcı adresi.
7 * @param _l2TokenBridge K2 standart köprü adresi.
8 */
9 // slither-disable-next-line external-function
Tümünü göster

Bu Slither testi (opens in a new tab), sözleşme kodundan çağrılmayan ve bu nedenle public yerine external olarak bildirilebilecek fonksiyonları tanımlar. external fonksiyonların gaz maliyeti, çağrı verilerinde parametrelerle sağlanabildikleri için daha düşük olabilir. public olarak tanımlanan fonksiyonlara sözleşme içinden erişilebilir olmalıdır. Sözleşmeler kendi çağrı verilerini değiştiremez, bu nedenle parametrelerin bellekte olması gerekir. Böyle bir fonksiyon harici olarak çağrıldığında, çağrı verilerini belleğe kopyalamak gerekir ve bu da gaz maliyetine neden olur. Bu durumda fonksiyon sadece bir kez çağrılır, bu nedenle verimsizlik bizim için önemli değildir.

1 function initialize(address _l1messenger, address _l2TokenBridge) public {
2 require(messenger == address(0), "Sözleşme zaten başlatıldı.");

initialize fonksiyonu yalnızca bir kez çağrılmalıdır. K1 alanlar arası mesajcısının veya K2 token köprüsünün adresi değişirse, yeni bir proxy ve onu çağıran yeni bir köprü oluştururuz. Bunun, çok nadir gerçekleşen tüm sistemin yükseltilmesi dışında gerçekleşmesi pek olası değildir.

Bu fonksiyonun, onu kimin arayabileceğini kısıtlayan herhangi bir mekanizmaya sahip olmadığını unutmayın. Bu, teorik olarak bir saldırganın biz proxy'yi ve köprünün ilk sürümünü dağıtana kadar bekleyebileceği ve ardından meşru kullanıcıdan önce initialize fonksiyonuna ulaşmak için front-run (opens in a new tab) yapabileceği anlamına gelir. Ancak bunu önlemenin iki yöntemi vardır:

  1. Sözleşmeler doğrudan bir EOA tarafından değil de onları oluşturan başka bir sözleşmeye sahip olan bir işlemde (opens in a new tab) dağıtılırsa tüm süreç atomik olabilir ve başka herhangi bir işlem yürütülmeden önce tamamlanabilir.
  2. Geçerli initialize çağrısı başarısız olursa, yeni oluşturulan proxy ve köprüyü yok saymak ve yenilerini oluşturmak her zaman mümkündür.
1 messenger = _l1messenger;
2 l2TokenBridge = _l2TokenBridge;
3 }

Bunlar, köprünün bilmesi gereken iki parametredir.

1
2 /**************
3 * Yatırma *
4 **************/
5
6 /** @dev Göndericinin EOA olmasını gerektiren değiştirici. Bu kontrol, kötü niyetli bir
7 * sözleşme tarafından initcode aracılığıyla atlatılabilir, ancak kaçınmak istediğimiz kullanıcı hatasını önler.
8 */
9 modifier onlyEOA() {
10 // Sözleşmelerden yatırma işlemlerini durdurmak için kullanılır (yanlışlıkla kaybedilen token'ları önlemek için)
11 require(!Address.isContract(msg.sender), "Hesap EOA değil");
12 _;
13 }
Tümünü göster

OpenZeppelin'in Address yardımcı programlarına ihtiyaç duymamızın nedeni budur.

1 /**
2 * @dev Bu fonksiyon, bir miktar ETH'yi çağıranın K2'deki bakiyesine
3 * yatırmak için veri olmadan çağrılabilir.
4 * Alım fonksiyonu veri almadığından,
5 * K2'ye muhafazakar bir varsayılan miktar iletilir.
6 */
7 receive() external payable onlyEOA {
8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));
9 }
Tümünü göster

Bu fonksiyon test amaçlı mevcuttur. Arayüz tanımlarında görünmediğine dikkat edin: Normal kullanım için değildir.

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 }
Tümünü göster

Bu iki fonksiyon, gerçek ETH yatırma işlemini yöneten fonksiyon olan _initiateETHDeposit etrafındaki sarmalayıcılardır.

1 /**
2 * @dev ETH'yi saklayarak ve K2 ETH Ağ Geçidi'ne yatırma hakkında bilgi vererek
3 * para yatırma mantığını gerçekleştirir.
4 * @param _from K1'de para yatırma işleminin çekileceği hesap.
5 * @param _to K2'de para yatırma işleminin verileceği hesap.
6 * @param _l2Gas K2'de para yatırma işlemini tamamlamak için gereken gaz limiti.
7 * @param _data K2'ye iletilecek isteğe bağlı veri. Bu veri yalnızca
8 * harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
9 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
10 */
11 function _initiateETHDeposit(
12 address _from,
13 address _to,
14 uint32 _l2Gas,
15 bytes memory _data
16 ) internal {
17 // finalizeDeposit çağrısı için çağrı verisi oluştur
18 bytes memory message = abi.encodeWithSelector(
Tümünü göster

Alanlar arası mesajların çalışma şekli, hedef sözleşmenin çağrı verileri olarak mesajla birlikte çağrılmasıdır. Solidity sözleşmeleri, çağrı verilerini her zaman ABI özelliklerine (opens in a new tab) uygun olarak yorumlar. Solidity'nin abi.encodeWithSelector (opens in a new tab) fonksiyonu bu çağrı verisini oluşturur.

1 IL2ERC20Bridge.finalizeDeposit.selector,
2 address(0),
3 Lib_PredeployAddresses.OVM_ETH,
4 _from,
5 _to,
6 msg.value,
7 _data
8 );

Buradaki mesaj, bu parametrelerle finalizeDeposit fonksiyonunu (opens in a new tab) çağırmaktır:

ParametreDeğerAnlamı
_l1Tokenaddress(0)K1'de ETH'yi (bir ERC-20 token'ı değildir) temsil eden özel değer
_l2TokenLib_PredeployAddresses.OVM_ETHOptimism'de ETH'yi yöneten K2 sözleşmesi, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (bu sözleşme yalnızca dahili Optimism kullanımı içindir)
_from_fromETH'yi gönderen K1'deki adres
_to_toETH'yi alan K2'deki adres
miktarmsg.valueGönderilen wei miktarı (zaten köprüye gönderildi)
_data_dataYatırmaya eklenecek ek veri
1 // Çağrı verisini K2'ye gönder
2 // slither-disable-next-line reentrancy-events
3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Mesajı, alanlar arası mesajcısı ile gönderin.

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

Bu transferi dinleyen herhangi bir merkeziyetsiz uygulamayı bilgilendirmek için bir olay yayınlayın.

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 }
Tümünü göster

Bu iki fonksiyon, gerçek ERC-20 yatırma işlemini yöneten fonksiyon olan _initiateERC20Deposit etrafındaki sarmalayıcılardır.

1 /**
2 * @dev K2 Yatırılan Token'ı bilgilendirerek yatırma mantığını gerçekleştirir
3 * sözleşmesi ve K1 fonlarını kilitlemek için bir işleyici çağırır. (örneğin, transferFrom)
4 *
5 * @param _l1Token Yatırmakta olduğumuz K1 ERC20'sinin adresi
6 * @param _l2Token İlgili K2 ERC20'sinin adresi
7 * @param _from K1'de para yatırma işleminin çekileceği hesap.
8 * @param _to K2'de para yatırma işleminin verileceği hesap.
9 * @param _amount Yatırılacak ERC20 miktarı.
10 * @param _l2Gas K2'de yatırma işlemini tamamlamak için gereken gaz limiti.
11 * @param _data K2'ye iletilecek isteğe bağlı veri. Bu veri yalnızca
12 * harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
13 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
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 {
Tümünü göster

Bu fonksiyon, birkaç önemli farklılık dışında yukarıdaki _initiateETHDeposit fonksiyonuna benzer. İlk fark, bu fonksiyonun token adreslerini ve aktarılacak miktarı parametre olarak almasıdır. ETH söz konusu olduğunda köprüye yapılan çağrı, varlığın köprü hesabına (msg.value) transferini zaten içerir.

1 // K1'de bir yatırma başlatıldığında, K1 Köprüsü gelecekteki
2 // para çekme işlemleri için fonları kendisine aktarır. safeTransferFrom ayrıca sözleşmede kod olup olmadığını
3 // kontrol eder, bu nedenle _from bir EOA veya address(0) ise bu işlem başarısız olur.
4 // slither-disable-next-line reentrancy-events, reentrancy-benign
5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

ERC-20 token transferleri, ETH'den farklı bir süreci takip eder:

  1. Kullanıcı (_from), köprüye uygun token'ları aktarması için bir izin verir.
  2. Kullanıcı, token sözleşmesinin adresi, miktarı vb. ile birlikte köprüyü çağırır.
  3. Köprü, token'ları yatırma işleminin bir parçası olarak (kendisine) aktarır.

İlk adım, son ikisinden ayrı bir işlemde gerçekleşebilir. Ancak _initiateERC20Deposit çağıran iki fonksiyon (depositERC20 ve depositERC20To), bu fonksiyonu _from parametresi olarak yalnızca msg.sender ile çağırdığından, front-running bir sorun olmaz.

1 // _l2Token.finalizeDeposit(_to, _amount) için çağrı verisi oluştur
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 // Çağrı verisini K2'ye gönder
13 // slither-disable-next-line reentrancy-events, reentrancy-benign
14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
15
16 // slither-disable-next-line reentrancy-benign
17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;
Tümünü göster

Yatırılan token miktarını deposits veri yapısına ekleyin. K2'de aynı K1 ERC-20 token'ına karşılık gelen birden fazla adres olabilir, bu nedenle yatırma işlemlerini takip etmek için köprünün K1 ERC-20 token bakiyesini kullanmak yeterli değildir.

1
2 // slither-disable-next-line reentrancy-events
3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);
4 }
5
6 /*************************
7 * Zincirler Arası Fonksiyonlar *
8 *************************/
9
10 /**
11 * @inheritdoc IL1StandardBridge
12 */
13 function finalizeETHWithdrawal(
14 address _from,
15 address _to,
16 uint256 _amount,
17 bytes calldata _data
Tümünü göster

K2 köprüsü, K2 alanlar arası mesajcısına, K1 alanlar arası mesajcısının bu fonksiyonu çağırmasına neden olan bir mesaj gönderir (tabii ki, mesajı sonlandıran işlem (opens in a new tab) K1'de gönderildikten sonra).

1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Bunun meşru bir mesaj olduğundan, alanlar arası mesajcısından gelen ve K2 token köprüsünden kaynaklanan bir mesaj olduğundan emin olun. Bu fonksiyon, ETH'yi köprüden çekmek için kullanılır, bu nedenle yalnızca yetkili arayan tarafından çağrıldığından emin olmalıyız.

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

ETH aktarmanın yolu, alıcıyı msg.value içindeki wei miktarıyla aramaktır.

1 require(success, "TransferHelper::safeTransferETH: ETH transferi başarısız");
2
3 // slither-disable-next-line reentrancy-events
4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);

Çekme işlemi ile ilgili bir olay yayınlayın.

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) {
Tümünü göster

Bu fonksiyon, ERC-20 token'ları için gerekli değişikliklerle birlikte yukarıdaki finalizeETHWithdrawal fonksiyonuna benzer.

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

deposits veri yapısını güncelleyin.

1 // K1'de bir çekme sonlandırıldığında, K1 Köprüsü fonları çekene aktarır
2 // slither-disable-next-line reentrancy-events
3 IERC20(_l1Token).safeTransfer(_to, _amount);
4
5 // slither-disable-next-line reentrancy-events
6 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
7 }
8
9
10 /*****************************
11 * Geçici - ETH Taşıma *
12 *****************************/
13
14 /**
15 * @dev ETH bakiyesini hesaba ekler. Bu, ETH'nin
16 * eski bir ağ geçidinden yeni bir ağ geçidine taşınmasına izin vermek içindir.
17 * NOT: Bu, yalnızca bir yükseltme için bırakılmıştır, böylece taşınan ETH'yi
18 * eski sözleşmeden alabiliriz
19 */
20 function donateETH() external payable {}
21}
Tümünü göster

Köprünün daha önce bir uygulaması vardı. Uygulamadan buna geçtiğimizde, tüm varlıkları taşımak zorunda kaldık. ERC-20 token'ları sadece taşınabilir. Ancak, ETH'yi bir sözleşmeye aktarmak için o sözleşmenin onayına ihtiyacınız var ve bu da donateETH'in bize sağladığı şeydir.

K2'deki ERC-20 Token'ları

Bir ERC-20 token'ının standart köprüye sığması için standart köprünün ve sadece standart köprünün token basmasına izin vermesi gerekir. Bu, köprülerin Optimism üzerinde dolaşan token sayısının K1 köprü sözleşmesi içinde kilitli token sayısına eşit olduğundan emin olması gerektiği için gereklidir. K2'de çok fazla token varsa, bazı kullanıcılar varlıklarını K1'e geri köprüleyemez. Güvenilir bir köprü yerine, esasen kısmi rezerv bankacılığını (opens in a new tab) yeniden yaratmış olurduk. K1'de çok fazla token varsa, bu token'lardan bazıları köprü sözleşmesinin içinde sonsuza kadar kilitli kalır çünkü K2 token'larını yakmadan onları serbest bırakmanın bir yolu yoktur.

IL2StandardERC20

Standart köprüyü kullanan K2'deki her ERC-20 token'ı, standart köprünün ihtiyaç duyduğu fonksiyonları ve olayları içeren bu arayüzü (opens in a new tab) sağlamalıdır.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Standart ERC-20 arayüzü (opens in a new tab), mint ve burn fonksiyonlarını içermez. Bu yöntemler, token'ları oluşturma ve yok etme mekanizmalarını belirsiz bırakan ERC-20 standardı (opens in a new tab) için gerekli değildir.

1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

ERC-165 arayüzü (opens in a new tab) bir sözleşmenin hangi fonksiyonları sağladığını belirtmek için kullanılır. Standardı buradan okuyabilirsiniz (opens in a new tab).

1interface IL2StandardERC20 is IERC20, IERC165 {
2 function l1Token() external returns (address);

Bu fonksiyon, bu sözleşmeye köprülenen K1 token'ının adresini sağlar. Ters yönde benzer bir fonksiyonumuz olmadığını unutmayın. Uygulandığında K2 desteğinin planlanıp planlanmadığına bakılmaksızın herhangi bir K1 token'ını köprüleyebilmemiz gerekir.

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}

Token'ları basmak (oluşturmak) ve yakmak (yok etmek) için fonksiyonlar ve olaylar. Köprü, token sayısının doğru (K1'de kilitli token sayısına eşit) olduğundan emin olmak için bu fonksiyonları çalıştırabilen tek varlık olmalıdır.

L2StandardERC20

Bu, IL2StandardERC20 arayüzünün bizim uygulamamızdır (opens in a new tab). Bir tür özel mantığa ihtiyacınız yoksa, bunu kullanmalısınız.

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

OpenZeppelin ERC-20 sözleşmesi (opens in a new tab). Optimism, mevcut özellikler iyi denetlendiğinde ve varlıkları elinde tutacak kadar güvenilir olması gerektiğinde yeni özellikler icat edilmemesi gerektiğine inanır.

1import "./IL2StandardERC20.sol";
2
3contract L2StandardERC20 is IL2StandardERC20, ERC20 {
4 address public l1Token;
5 address public l2Bridge;

Bunlar, bizim ihtiyaç duyduğumuz ve normalde ERC-20'nin gerektirmediği iki ek yapılandırma parametresidir.

1
2 /**
3 * @param _l2Bridge K2 standart köprüsünün adresi.
4 * @param _l1Token İlgili K1 token'ının adresi.
5 * @param _name ERC20 adı.
6 * @param _symbol ERC20 sembolü.
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 }
Tümünü göster

Önce kalıtım ile aldığımız sözleşmenin yapıcısını çağırın (ERC20(_name, _symbol)) ve sonra kendi değişkenlerimizi ayarlayın.

1
2 modifier onlyL2Bridge() {
3 require(msg.sender == l2Bridge, "Yalnızca K2 Köprüsü basım ve yakım yapabilir");
4 _;
5 }
6
7
8 // slither-disable-next-line external-function
9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {
10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165
11 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^
12 IL2StandardERC20.mint.selector ^
13 IL2StandardERC20.burn.selector;
14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;
15 }
Tümünü göster

ERC-165 (opens in a new tab) bu şekilde çalışır. Her arayüz bir dizi desteklenen fonksiyondur ve bu fonksiyonların ABI fonksiyon seçicilerinin (opens in a new tab) özel veya (opens in a new tab) işlemi olarak tanımlanır.

K2 köprüsü, varlıkları gönderdiği ERC-20 sözleşmesinin bir IL2StandardERC20 olduğundan emin olmak için doğruluk kontrolü olarak ERC-165'i kullanır.

Not: Hileli sözleşmenin supportsInterface için yanlış yanıtlar vermesini önleyecek hiçbir şey yoktur, bu nedenle bu bir güvenlik mekanizması değil, doğruluk kontrol mekanizmasıdır.

1 // 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
9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {
10 _burn(_from, _amount);
11
12 emit Burn(_from, _amount);
13 }
14}
Tümünü göster

Yalnızca K2 köprüsünün varlıkları basmasına ve yakmasına izin verilir.

_mint ve _burn aslında OpenZeppelin ERC-20 sözleşmesinde tanımlanmıştır. Bu sözleşme onları harici olarak ifşa etmez, çünkü token'ları basma ve yakma koşulları, ERC-20'yi kullanma yollarının sayısı kadar çeşitlidir.

K2 Köprü Kodu

Bu, Optimism üzerindeki köprüyü çalıştıran koddur. Bu sözleşmenin kaynağı buradadır (opens in a new tab).

1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.9;
3
4/* Arayüz İçe Aktarmaları */
5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";

IL2ERC20Bridge (opens in a new tab) arayüzü, yukarıda gördüğümüz K1 eşdeğerine çok benzer. İki önemli fark vardır:

  1. K1'de yatırma işlemini başlatır ve çekme işlemlerini sonlandırırsınız. Burada ise çekme işlemlerini başlatır ve yatırma işlemlerini sonlandırırsınız.
  2. K1'de ETH ve ERC-20 token'ları arasında ayrım yapmak gerekir. K2'de aynı fonksiyonları her ikisi için de kullanabiliriz çünkü Optimism üzerindeki dahili ETH bakiyeleri, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab) adresiyle bir ERC-20 token'ı olarak işlenir.
1/* Kütüphane İçe Aktarmaları */
2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";
4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";
5
6/* Sözleşme İçe Aktarmaları */
7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";
8
9/**
10 * @title L2StandardBridge
11 * @dev L2 Standart köprüsü, K1 ve K2 arasında ETH ve ERC20 geçişlerini sağlamak için
12 * K1 Standart köprüsü ile birlikte çalışan bir sözleşmedir.
13 * Bu sözleşme, K1 Standart köprüsüne yapılan yatırmaları duyduğunda yeni token'lar için bir basıcı olarak görev yapar.
14 *
15 * Bu sözleşme aynı zamanda çekme amaçlı token'ların yakıcısı olarak da görev yapar ve K1
16 * köprüsüne K1 fonlarını serbest bırakması için bilgi verir.
17 */
18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
19 /********************************
20 * Harici Sözleşme Referansları *
21 ********************************/
22
23 address public l1TokenBridge;
Tümünü göster

K1 köprüsünün adresini takip edin. K1 eş değerinin aksine, burada bu değişkene ihtiyacımız var. K1 köprüsünün adresi önceden bilinmiyor.

1
2 /***************
3 * Yapıcı *
4 ***************/
5
6 /**
7 * @param _l2CrossDomainMessenger Bu sözleşme tarafından kullanılan alanlar arası mesajcı.
8 * @param _l1TokenBridge Ana zincire dağıtılan K1 köprüsünün adresi.
9 */
10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)
11 CrossDomainEnabled(_l2CrossDomainMessenger)
12 {
13 l1TokenBridge = _l1TokenBridge;
14 }
15
16 /***************
17 * Çekme *
18 ***************/
19
20 /**
21 * @inheritdoc IL2ERC20Bridge
22 */
23 function withdraw(
24 address _l2Token,
25 uint256 _amount,
26 uint32 _l1Gas,
27 bytes calldata _data
28 ) external virtual {
29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);
30 }
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function withdrawTo(
36 address _l2Token,
37 address _to,
38 uint256 _amount,
39 uint32 _l1Gas,
40 bytes calldata _data
41 ) external virtual {
42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
43 }
Tümünü göster

Bu iki fonksiyon çekme işlemlerini başlatır. K1 token adresini belirtmeye gerek olmadığını unutmayın. K2 token'larının bize K1 eş değerinin adresini söylemesi bekleniyor.

1
2 /**
3 * @dev Token'ı yakarak ve K1 token Ağ Geçidi'ne
4 * çekme hakkında bilgi vererek çekme mantığını gerçekleştirir.
5 * @param _l2Token Çekme işleminin başlatıldığı K2 token adresi.
6 * @param _from K2'de para çekme işleminin çekileceği hesap.
7 * @param _to K1'de para çekme işleminin verileceği hesap.
8 * @param _amount Çekilecek token miktarı.
9 * @param _l1Gas Kullanılmıyor, ancak potansiyel ileriye dönük uyumluluk considerations için dahil edilmiştir.
10 * @param _data K1'e iletilecek isteğe bağlı veri. Bu veri yalnızca
11 * harici sözleşmeler için bir kolaylık olarak sağlanır. Maksimum
12 * uzunluğu zorunlu kılmanın yanı sıra, bu sözleşmeler içeriği hakkında hiçbir garanti vermez.
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 // Bir çekme başlatıldığında, sonraki K2 kullanımını önlemek için
23 // çekenin fonlarını yakarız
24 // slither-disable-next-line reentrancy-events
25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);
Tümünü göster

_from parametresine değil, sahte olması çok daha zor olan (bildiğim kadarıyla imkansız) msg.sender'a güvendiğimize dikkat edin.

1
2 // l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) için çağrı verisi oluştur
3 // slither-disable-next-line reentrancy-events
4 address l1Token = IL2StandardERC20(_l2Token).l1Token();
5 bytes memory message;
6
7 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

K1'de ETH ve ERC-20 arasında ayrım yapmak gerekir.

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 // Mesajı K1 köprüsüne gönder
21 // slither-disable-next-line reentrancy-events
22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);
23
24 // slither-disable-next-line reentrancy-events
25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);
26 }
27
28 /************************************
29 * Çapraz zincir Fonksiyonu: Yatırma *
30 ************************************/
31
32 /**
33 * @inheritdoc IL2ERC20Bridge
34 */
35 function finalizeDeposit(
36 address _l1Token,
37 address _l2Token,
38 address _from,
39 address _to,
40 uint256 _amount,
41 bytes calldata _data
Tümünü göster

Bu fonksiyon, L1StandardBridge tarafından çağrılır.

1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Mesajın kaynağının meşru olduğundan emin olun. Bu, fonksiyon _mint'i çağırdığı ve köprünün K1'de sahip olduğu token'lar tarafından kapsanmayan token'ları vermek için kullanılabileceği için önemlidir.

1 // Hedef token'ın uyumlu olup olmadığını kontrol edin ve
2 // K1'de yatırılan token'ın buradaki K2 yatırılan token gösterimiyle eşleştiğini doğrulayın
3 if (
4 // slither-disable-next-line reentrancy-events
5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
6 _l1Token == IL2StandardERC20(_l2Token).l1Token()

Doğruluk testleri:

  1. Doğru arayüz destekleniyor
  2. K2 ERC-20 sözleşmesinin K1 adresi, token'ların K1 kaynağıyla eşleşiyor
1 ) {
2 // Bir yatırma sonlandırıldığında, K2'deki hesaba aynı miktarda
3 // token yatırırız.
4 // slither-disable-next-line reentrancy-events
5 IL2StandardERC20(_l2Token).mint(_to, _amount);
6 // slither-disable-next-line reentrancy-events
7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);

Doğruluk testlerini geçerse yatırmayı tamamlayın:

  1. Token'ları basma
  2. Uygun olayı yayınlama
1 } else {
2 // Yatırılan K2 token'ı, K1 token'ının doğru adresi
3 // konusunda aynı fikirde değil veya doğru arayüzü desteklemiyor.
4 // Bu yalnızca kötü niyetli bir K2 token'ı olduğunda veya bir kullanıcı bir şekilde
5 // yatırmak için yanlış K2 token adresi belirttiğinde gerçekleşmelidir.
6 // Her iki durumda da süreci burada durdurur ve bir çekme
7 // mesajı oluştururuz, böylece kullanıcılar bazı durumlarda fonlarını geri alabilir.
8 // Kötü niyetli token sözleşmelerini tamamen önlemenin bir yolu yoktur, ancak bu
9 // kullanıcı hatasını sınırlar ve bazı kötü niyetli sözleşme davranışlarını azaltır.
Tümünü göster

Bir kullanıcı yanlış K2 token adresini kullanarak tespit edilebilir bir hata yaptıysa, yatırmayı iptal etmek ve token'ları K1'e iade etmek istiyoruz. Bunu K2'den yapabilmemizin tek yolu, hata meydan okuma süresini beklemek zorunda kalacak bir mesaj göndermektir ancak bu, kullanıcı için token'ları kalıcı olarak kaybetmekten çok daha iyidir.

1 bytes memory message = abi.encodeWithSelector(
2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,
3 _l1Token,
4 _l2Token,
5 _to, // yatırmayı gönderene geri göndermek için _to ve _from'u burada değiştirdik
6 _from,
7 _amount,
8 _data
9 );
10
11 // Mesajı K1 köprüsüne gönder
12 // slither-disable-next-line reentrancy-events
13 sendCrossDomainMessage(l1TokenBridge, 0, message);
14 // slither-disable-next-line reentrancy-events
15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);
16 }
17 }
18}
Tümünü göster

Sonuç

Standart köprü, varlık aktarımları için en esnek mekanizmadır. Ancak çok genel olduğu için her zaman kullanması en kolay olan mekanizma değildir. Özellikle çekimler için, çoğu kullanıcı meydan okuma süresini beklemeyen ve çekimi sonlandırmak için bir Merkle ispatı gerektirmeyen üçüncü parti köprüleri (opens in a new tab) kullanmayı tercih eder.

Bu köprüler genellikle K1 üzerinde küçük bir ücret (genelde bir standart köprü çekiminin gaz ücretinden daha azına) için anında sağladıkları varlıklara sahip olarak çalışırlar. Köprü (ya da onu çalıştıran insanlar) K1 varlıklarının azaldığını sezdiğinde K2'den yeteri kadar varlığı aktarır. Bunlar çok büyük çekimler olduğu için, çekim ücreti büyük bir miktar üzerinden amorti edilmiştir ve daha küçük bir yüzdeliktir.

Umarım bu makale katman 2'nin nasıl çalıştığı hakkında dahasını anlamanıza; temiz ve güvenli Solidity kodu yazmanıza yardımcı olmuştur.

Çalışmalarımdan daha fazlası için buraya bakın (opens in a new tab).

Sayfanın son güncellenmesi: 22 Ekim 2025

Bu rehber yararlı oldu mu?