Ana içeriğe atla

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.

// SPDX-License-Identifier: MIT

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

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

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.

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

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

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

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

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.

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.

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

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

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.

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.

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.

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

/* Arayüz İçe Aktarmaları */
import { 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.

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.

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.

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

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


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

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.

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    /********************************
     * Harici Sözleşme Referansları *
     ********************************/

    address public l2TokenBridge;

L2StandardBridge adresi.


    // K1 token'ını K2 token'ına, yatırılan K1 token bakiyesine eşler
    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.


    /***************
     * Yapıcı *
     ***************/

    // Bu sözleşme bir proxy arkasında bulunur, bu nedenle yapıcı parametreleri kullanılmayacaktır.
    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.

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.

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

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

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

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

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

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.

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

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
        // Çağrı verisini K2'ye gönder
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

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

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

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

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

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.

        // K1'de bir yatırma başlatıldığında, K1 Köprüsü gelecekteki
        // para çekme işlemleri için fonları kendisine aktarır. safeTransferFrom ayrıca sözleşmede kod olup olmadığını
        // kontrol eder, bu nedenle _from bir EOA veya address(0) ise bu işlem başarısız olur.
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        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.

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.

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

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

        // slither-disable-next-line reentrancy-events
        (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.

        require(success, "TransferHelper::safeTransferETH: ETH transferi başarısız");

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

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

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

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

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

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.

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

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

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

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


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

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.

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

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

import "./IL2StandardERC20.sol";

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

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

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

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.

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

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

/* Arayüz İçe Aktarmaları */
import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";
import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";
import { 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.

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.

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.

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


        // l1TokenBridge.finalizeERC20Withdrawal(_to, _amount) için çağrı verisi oluştur
        // slither-disable-next-line reentrancy-events
        address l1Token = IL2StandardERC20(_l2Token).l1Token();
        bytes memory message;

        if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {

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

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

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

        // Hedef token'ın uyumlu olup olmadığını kontrol edin ve
        // K1'de yatırılan token'ın buradaki K2 yatırılan token gösterimiyle eşleştiğini doğrulayın
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _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
        ) {
            // Bir yatırma sonlandırıldığında, K2'deki hesaba aynı miktarda
            // token yatırırız.
            // 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);

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

  1. Token'ları basma
  2. Uygun olayı yayınlama

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.

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üncellenme tarihi: 3 Nisan 2026

Bu eğitim faydalı oldu mu?