Ana içeriğe atla

Optimism standart köprü sözleşmesi incelemesi

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

Optimism (opens in a new tab) bir İyimser rollup'tır. İyimser toplamalar (optimistic rollups), işlemleri Ethereum Ana Ağı'ndan (katman 1 veya l1 olarak da bilinir) çok daha düşük bir fiyata işleyebilir çünkü işlemler ağdaki her düğüm yerine yalnızca birkaç düğüm tarafından işlenir. Aynı zamanda, tüm veriler l1'e yazılır, böylece her şey Ana Ağ'ın tüm bütünlük ve erişilebilirlik garantileriyle kanıtlanabilir ve yeniden oluşturulabilir.

L1 varlıklarını Optimism'de (veya başka herhangi bir l2'de) kullanmak için varlıkların köprülenmesi gerekir. Bunu başarmanın bir yolu, kullanıcıların varlıkları (ETH ve ERC-20 token'ları en yaygın olanlarıdır) l1'de kilitlemesi ve l2'de kullanmak üzere eşdeğer varlıklar almasıdır. Sonunda, bunlara sahip olan kişi onları l1'e geri köprülemek isteyebilir. Bunu yaparken, varlıklar l2'de yakılır ve ardından l1'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 bu köprünün kaynak kodunu inceliyor ve iyi yazılmış bir Solidity kodu örneği olarak çalışıyoruz.

Kontrol akışları

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

  • Yatırma (l1'den l2'ye)
  • Çekim (l2'den l1'e)

Yatırma akışı

Katman 1

  1. Bir ERC-20 yatırılıyorsa, yatıran kişi köprüye yatırılan miktarı harcaması için bir harcama izni verir
  2. Yatıran kişi l1 köprüsünü çağırır (depositERC20, depositERC20To, depositETH veya depositETHTo)
  3. L1 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 transfer edilir
    • ERC-20: Varlık, yatıran kişi tarafından sağlanan harcama izni kullanılarak köprü tarafından kendisine transfer edilir
  4. L1 köprüsü, l2 köprüsündeki finalizeDeposit işlevini çağırmak için alanlar arası mesaj (cross-domain message) mekanizmasını kullanır

Katman 2

  1. L2 köprüsü, finalizeDeposit çağrısının meşru olduğunu doğrular:
    • Alanlar arası mesaj sözleşmesinden geldiğini
    • Orijinal olarak l1'deki köprüden geldiğini
  2. L2 köprüsü, l2'deki ERC-20 token sözleşmesinin doğru olup olmadığını kontrol eder:
    • L2 sözleşmesi, l1 karşılığının l1'de token'ların geldiği sözleşmeyle aynı olduğunu bildirir
    • L2 sözleşmesi, doğru arayüzü desteklediğini bildirir (ERC-165 kullanarak (opens in a new tab)).
  3. L2 sözleşmesi doğruysa, uygun adrese uygun sayıda token basmak için onu çağırır. Değilse, kullanıcının l1'deki token'ları talep etmesine izin vermek için bir çekim işlemi başlatır.

Çekim akışı

Katman 2

  1. Çeken kişi l2 köprüsünü çağırır (withdraw veya withdrawTo)
  2. L2 köprüsü, msg.sender adresine ait uygun sayıda token'ı yakar
  3. L2 köprüsü, l1 köprüsündeki finalizeETHWithdrawal veya finalizeERC20Withdrawal işlevini çağırmak için alanlar arası mesaj mekanizmasını kullanır

Katman 1

  1. L1 köprüsü, finalizeETHWithdrawal veya finalizeERC20Withdrawal çağrısının meşru olduğunu doğrular:
    • Alanlar arası mesaj mekanizmasından geldiğini
    • Orijinal olarak l2'deki köprüden geldiğini
  2. L1 köprüsü, uygun varlığı (ETH veya ERC-20) uygun adrese transfer eder

Katman 1 kodu

Bu, l1'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 işlevleri ve tanımları içerir.

// SPDX-License-Identifier: MIT

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

pragma solidity >0.5.0 <0.9.0;

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

Optimism köprü terminolojisinde yatırma (deposit), l1'den l2'ye transfer anlamına gelir ve çekim (withdrawal), l2'den l1'e transfer anlamına gelir.

        address indexed _l1Token,
        address indexed _l2Token,

Çoğu durumda l1'deki bir ERC-20'nin adresi, l2'deki eşdeğer ERC-20'nin adresiyle aynı değildir. Token adreslerinin listesini buradan görebilirsiniz (opens in a new tab). chainId 1 olan adres l1'de (Ana Ağ) ve chainId 10 olan adres l2'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 bunları bildiren olaylara eklenirler.

    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. L1 köprüsü durumunda bu, yatırma işlemlerinin başlatılması ve çekim işlemlerinin tamamlanması anlamına gelir.

Bu işleve aslında gerek yoktur, çünkü l2'de önceden dağıtılmış bir sözleşmedir, bu nedenle her zaman 0x4200000000000000000000000000000000000010 adresindedir. L2 köprüsüyle simetri sağlamak için buradadır, çünkü l1 köprüsünün adresini bilmek kolay değildir.

_l2Gas parametresi, işlemin harcamasına izin verilen l2 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 işlev, bir kullanıcının varlıkları farklı bir blokzincirdeki aynı adrese köprülediği yaygın senaryoyu ele alır.

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

Optimism'de çekim işlemleri (ve l2'den l1'e giden diğer mesajlar) iki adımlı bir süreçtir:

  1. L2'de başlatan bir işlem.
  2. L1'de tamamlayan veya talep eden bir işlem. Bu işlemin, l2 işlemi için hata itiraz süresi (fault challenge period) (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 işlev 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 dosya arasında 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 uygulayabilir ve aynı zamanda ETH'yi köprülemek zorunda kalmaz.

Bu olay, l1 ve l2 token adresleri olmaması dışında ERC-20 sürümüyle (ERC20DepositInitiated) neredeyse aynıdır. Aynı durum diğer olaylar ve işlevler 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ü (l1 ve l2) tarafından miras alınır.

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

/* Arayüz İçe Aktarımları */
import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";

Bu arayüz (opens in a new tab), alanlar arası mesajlaşma aracını (cross domain messenger) kullanarak sözleşmeye diğer katmana nasıl mesaj göndereceğini söyler. Bu alanlar arası mesajlaşma aracı tamamen başka bir sistemdir ve gelecekte yazmayı umduğum kendi makalesini hak etmektedir.

Sözleşmenin bilmesi gereken tek parametre, bu katmandaki alanlar arası mesajlaşma aracının adresidir. Bu parametre kurucu içinde bir kez ayarlanır ve asla değişmez.

Alanlar arası mesajlaşmaya, çalıştığı blokzincirdeki (Ethereum Ana Ağı veya Optimism) herhangi bir sözleşme tarafından erişilebilir. Ancak her iki taraftaki köprünün yalnızca diğer taraftaki köprüden gelmeleri durumunda belirli mesajlara güvenmesine ihtiyacımız var.

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

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


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

Alanlar arası mesajlaşma aracının diğer katmanla mesaj gönderen adresi sağlama yolu .xDomainMessageSender() işlevidir (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 işlev alanlar arası mesajlaşma aracını döndürür. Bundan miras alan sözleşmelerin hangi alanlar arası mesajlaşma aracını kullanacağını belirlemek için bir algoritma kullanmasına izin vermek amacıyla messenger değişkeni yerine bir işlev kullanıyoruz.

Son olarak, diğer katmana mesaj gönderen işlev.

    ) 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 olası sorunları aramak için her sözleşmede çalıştırdığı statik bir 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, Slither'ın bunu bilmesinin bir yolu olmasa bile getCrossDomainMessenger() işlevinin güvenilir bir adres döndürdüğünü biliyoruz.

L1 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 Aktarımları */
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), l2'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. Bunun hakkında daha fazla bilgiyi buradan okuyabilirsiniz.

/* Kütüphane İçe Aktarımları */
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 l2 sözleşmelerinin adreslerine sahiptir. Buna l2'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ı ayırt etmek için kullanılır.

Bunun mükemmel bir çözüm olmadığını unutmayın, çünkü doğrudan çağrılar ile bir sözleşmenin kurucusundan yapılan çağrıları ayırt etmenin bir yolu yoktur, ancak en azından bu, bazı yaygın kullanıcı hatalarını belirlememize ve önlememize olanak tanır.

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

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

  1. Geri al (Revert)
  2. false döndür

Her iki durumu da ele almak kodumuzu daha karmaşık hale getirecektir, bu nedenle bunun yerine tüm başarısızlıkların bir geri alma ile sonuçlanmasını (opens in a new tab) sağlayan OpenZeppelin'in SafeERC20 (opens in a new tab) sarmalayıcısını kullanıyoruz.

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


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

    address public l2TokenBridge;

L2StandardBridge adresi.


    // Yatırılan l1 Token bakiyesi için l1 Token'ı l2 Token'a eşler
    mapping(address => mapping(address => uint256)) public deposits;

Bunun gibi çift bir eşleme (mapping) (opens in a new tab), iki boyutlu seyrek bir dizi (opens in a new tab) tanımlamanın yoludur. Bu veri yapısındaki değerler deposit[L1 token addr][L2 token addr] olarak tanımlanır. Varsayılan değer sıfırdır. Yalnızca farklı bir değere ayarlanan hücreler depolamaya yazılır.


    /***************
     * Kurucu *
     ***************/

    // Bu Sözleşme bir proxy arkasında yaşar, bu nedenle kurucu 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, çağrıları adresi vekil kontrat tarafından saklanan ayrı bir sözleşmeye aktarmak için delegatecall (opens in a new tab) kullanan bir sözleşme olan Proxy (opens in a new tab) kullanıyoruz (yükseltme yaptığınızda vekile bu adresi değiştirmesini söylersiniz). delegatecall kullandığınızda depolama, çağıran sözleşmenin depolaması olarak kalır, bu nedenle tüm sözleşme durum değişkenlerinin değerleri etkilenmez.

Bu modelin bir etkisi, delegatecall tarafından çağrılan sözleşmenin depolamasının kullanılmaması ve bu nedenle ona aktarılan kurucu değerlerinin önemli olmamasıdır. Bu, CrossDomainEnabled kurucusuna anlamsız bir değer sağlayabilmemizin nedenidir. Aşağıdaki başlatmanın kurucudan 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 işlevleri tanımlar. external işlevlerinin gaz maliyeti daha düşük olabilir, çünkü onlara çağrı verisinde (calldata) parametreler sağlanabilir. public olarak bildirilen işlevlerin sözleşme içinden erişilebilir olması gerekir. Sözleşmeler kendi çağrı verilerini değiştiremezler, bu nedenle parametrelerin bellekte olması gerekir. Böyle bir işlev dışarıdan çağrıldığında, çağrı verisini belleğe kopyalamak gerekir, bu da gaza mal olur. Bu durumda işlev yalnızca 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), "Contract has already been initialized.");

initialize işlevi yalnızca bir kez çağrılmalıdır. L1 alanlar arası mesajlaşma aracının veya l2 token köprüsünün adresi değişirse, yeni bir vekil ve onu çağıran yeni bir köprü oluştururuz. Tüm sistemin yükseltilmesi dışında bunun gerçekleşmesi pek olası değildir, bu çok nadir görülen bir durumdur.

Bu işlevin onu kimin çağırabileceğini kısıtlayan herhangi bir mekanizması olmadığını unutmayın. Bu, teorik olarak bir saldırganın vekili 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 işlevine ulaşmak için önden koşma (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, başka bir sözleşmenin onları oluşturduğu 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 bitebilir.
  2. initialize işlevine yapılan meşru çağrı başarısız olursa, yeni oluşturulan vekili ve köprüyü görmezden gelmek 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 işlev test amacıyla mevcuttur. Arayüz tanımlarında görünmediğine dikkat edin - normal kullanım için değildir.

Bu iki işlev, asıl ETH yatırma işlemini gerçekleştiren işlev olan _initiateETHDeposit etrafındaki sarmalayıcılardır.

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

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

Buradaki mesaj, şu parametrelerle finalizeDeposit işlevini (opens in a new tab) çağırmaktır:

ParametreDeğerAnlamı
_l1Tokenaddress(0)L1'de ETH'yi (bir ERC-20 token'ı olmayan) temsil eden özel değer
_l2TokenLib_PredeployAddresses.OVM_ETHOptimism'de ETH'yi yöneten l2 sözleşmesi, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (bu sözleşme yalnızca dahili Optimism kullanımı içindir)
_from_fromL1'de ETH'yi gönderen adres
_to_toL2'de ETH'yi alan adres
amountmsg.valueGönderilen Wei miktarı (zaten köprüye gönderilmiş olan)
_data_dataYatırma işlemine eklenecek ek veri
        // çağrı verisini l2'ye gönder
        // slither-disable-next-line reentrancy-events
        sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);

Mesajı alanlar arası mesajlaşma aracı üzerinden gönderin.

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

Dinleyen herhangi bir merkeziyetsiz uygulamayı (dapp) bu transfer hakkında bilgilendirmek için bir olay yayınlayın.

Bu iki işlev, asıl ERC-20 yatırma işlemini gerçekleştiren işlev olan _initiateERC20Deposit etrafındaki sarmalayıcılardır.

Bu işlev, birkaç önemli farkla yukarıdaki _initiateETHDeposit işlevine benzer. İlk fark, bu işlevin token adreslerini ve transfer edilecek miktarı parametre olarak almasıdır. ETH durumunda, köprüye yapılan çağrı zaten varlığın köprü hesabına transferini içerir (msg.value).

        // l1 üzerinde bir yatırma işlemi başlatıldığında, l1 köprüsü gelecekteki
        // çekim işlemleri için fonları kendisine transfer eder. safeTransferFrom ayrıca Sözleşmenin koda sahip olup olmadığını da kontrol eder, bu nedenle
        // _from bir EOA veya address(0) ise bu başarısız olacaktır.
        // slither-disable-next-line reentrancy-events, reentrancy-benign
        IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

ERC-20 token transferleri ETH'den farklı bir süreç izler:

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

İlk adım, son ikisinden ayrı bir işlemde gerçekleşebilir. Ancak, önden koşma bir sorun değildir çünkü _initiateERC20Deposit işlevini çağıran iki işlev (depositERC20 ve depositERC20To), bu işlevi yalnızca _from parametresi olarak msg.sender ile çağırır.

Yatırılan token miktarını deposits veri yapısına ekleyin. L2'de aynı l1 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 l1 ERC-20 token bakiyesini kullanmak yeterli değildir.

L2 köprüsü, l2 alanlar arası mesajlaşma aracına bir mesaj gönderir ve bu da l1 alanlar arası mesajlaşma aracının bu işlevi çağırmasına neden olur (elbette mesajı tamamlayan işlem (opens in a new tab) l1'de gönderildikten sonra).

    ) external onlyFromCrossDomainAccount(l2TokenBridge) {

Bunun alanlar arası mesajlaşma aracından gelen ve l2 token köprüsünden kaynaklanan meşru bir mesaj olduğundan emin olun. Bu işlev köprüden ETH ç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 transfer etmenin yolu, alıcıyı msg.value içindeki Wei miktarıyla çağırmaktır.

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

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

Çekim işlemi hakkında bir olay yayınlayın.

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

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

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

Köprünün daha eski bir uygulaması vardı. O uygulamadan buna geçtiğimizde tüm varlıkları taşımak zorunda kaldık. ERC-20 token'ları kolayca taşınabilir. Ancak, bir sözleşmeye ETH transfer etmek için o sözleşmenin onayına ihtiyacınız vardır, donateETH bize bunu sağlar.

L2'deki ERC-20 Token'ları

Bir ERC-20 token'ının standart köprüye uyması için, standart köprünün ve yalnızca standart köprünün token basmasına izin vermesi gerekir. Bu gereklidir çünkü köprülerin Optimism'de dolaşan token sayısının l1 köprü sözleşmesinde kilitli olan token sayısına eşit olduğundan emin olması gerekir. L2'de çok fazla token varsa, bazı kullanıcılar varlıklarını l1'e geri köprüleyemezler. Güvenilir bir köprü yerine, esasen kısmi rezerv bankacılığını (opens in a new tab) yeniden yaratmış olurduk. L1'de çok fazla token varsa, bu token'ların bazıları sonsuza kadar köprü sözleşmesinde kilitli kalacaktır çünkü l2 token'larını yakmadan onları serbest bırakmanın bir yolu yoktur.

IL2StandardERC20

Standart köprüyü kullanan l2'deki her ERC-20 token'ının, standart köprünün ihtiyaç duyduğu işlevlere ve olaylara sahip olan bu arayüzü (opens in a new tab) sağlaması gerekir.

// 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 işlevlerini içermez. Bu yöntemler, token'ları oluşturma ve yok etme mekanizmalarını belirtmeyen ERC-20 standardı (opens in a new tab) tarafından gerekli kılınmaz.

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

ERC-165 arayüzü (opens in a new tab), bir sözleşmenin hangi işlevleri 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 işlev, bu sözleşmeye köprülenen l1 token'ının adresini sağlar. Ters yönde benzer bir işlevimiz olmadığını unutmayın. Uygulandığında l2 desteğinin planlanıp planlanmadığına bakılmaksızın herhangi bir l1 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 işlevler ve olaylar. Token sayısının doğru olduğundan (l1'de kilitli olan token sayısına eşit olduğundan) emin olmak için bu işlevleri çalıştırabilen tek varlık köprü olmalıdır.

L2StandardERC20

Bu, IL2StandardERC20 arayüzü 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, özellikle tekerlek iyi denetlenmişse ve varlıkları tutacak kadar güvenilir olması gerekiyorsa, tekerleği yeniden icat etmeye inanmaz.

import "./IL2StandardERC20.sol";

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

Bunlar, bizim gerektirdiğimiz ve ERC-20'nin normalde gerektirmediği iki ek yapılandırma parametresidir.

Önce miras aldığımız sözleşme için kurucuyu çağırın (ERC20(_name, _symbol)) ve ardından kendi değişkenlerimizi ayarlayın.

ERC-165 (opens in a new tab) bu şekilde çalışır. Her arayüz, desteklenen bir dizi işlevdir ve bu işlevlerin ABI işlev seçicilerinin (opens in a new tab) özel veya (exclusive or) (opens in a new tab) işlemi olarak tanımlanır.

L2 köprüsü, varlık gönderdiği ERC-20 sözleşmesinin bir IL2StandardERC20 olduğundan emin olmak için bir mantık kontrolü (sanity check) olarak ERC-165'i kullanır.

Not: Kötü niyetli bir sözleşmenin supportsInterface işlevine yanlış yanıtlar vermesini engelleyecek hiçbir şey yoktur, bu nedenle bu bir mantık kontrolü mekanizmasıdır, bir güvenlik mekanizması değildir.

Yalnızca l2 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ı dışarıya açık hale getirmez, çünkü token'ları basma ve yakma koşulları, ERC-20'yi kullanma yollarının sayısı kadar çeşitlidir.

L2 Köprü Kodu

Bu, Optimism'de 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 Aktarımları */
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 l1 karşılığına çok benzer. İki önemli fark vardır:

  1. L1'de yatırma işlemlerini başlatır ve çekim işlemlerini tamamlarsınız. Burada çekim işlemlerini başlatır ve yatırma işlemlerini tamamlarsınız.
  2. L1'de ETH ve ERC-20 token'ları arasında ayrım yapmak gerekir. L2'de her ikisi için de aynı işlevleri kullanabiliriz çünkü dahili olarak Optimism'deki ETH bakiyeleri 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (opens in a new tab) adresine sahip bir ERC-20 token'ı olarak işlenir.

L1 köprüsünün adresini takip edin. L1 karşılığının aksine, burada bu değişkene ihtiyacımız olduğunu unutmayın. L1 köprüsünün adresi önceden bilinmemektedir.

Bu iki işlev çekim işlemlerini başlatır. L1 token adresini belirtmeye gerek olmadığını unutmayın. L2 token'larının bize l1 karşılığının adresini söylemesi beklenir.

_from parametresine değil, sahtesini yapması çok daha zor olan (bildiğim kadarıyla imkansız) msg.sender parametresine 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) {

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

Bu işlev L1StandardBridge tarafından çağrılır.

    ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {

Mesajın kaynağının meşru olduğundan emin olun. Bu önemlidir çünkü bu işlev _mint işlevini çağırır ve köprünün l1'de sahip olduğu token'lar tarafından kapsanmayan token'lar vermek için kullanılabilir.

        // Hedef Token'ın uyumlu olduğunu kontrol et ve
        // l1 üzerinde yatırılan Token'ın buradaki l2 yatırılan Token temsiliyle eşleştiğini doğrula
        if (
            // slither-disable-next-line reentrancy-events
            ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
            _l1Token == IL2StandardERC20(_l2Token).l1Token()

Mantık kontrolleri:

  1. Doğru arayüz destekleniyor
  2. L2 ERC-20 sözleşmesinin l1 adresi, token'ların l1 kaynağıyla eşleşiyor
        ) {
            // Bir yatırma işlemi sonuçlandırıldığında, l2 üzerindeki hesaba aynı miktarda
            // Token alacak kaydederiz.
            // 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);

Mantık kontrolleri geçerse, yatırma işlemini tamamlayın:

  1. Token'ları basın
  2. Uygun olayı yayınlayın

Bir kullanıcı yanlış l2 token adresini kullanarak tespit edilebilir bir hata yaptıysa, yatırma işlemini iptal etmek ve token'ları l1'de iade etmek istiyoruz. Bunu l2'den yapabilmemizin tek yolu, hata itiraz süresini beklemesi gerekecek 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 transferleri için en esnek mekanizmadır. Ancak, çok genel olduğu için her zaman kullanımı en kolay mekanizma değildir. Özellikle çekim işlemleri için çoğu kullanıcı, itiraz süresini beklemeyen ve çekim işlemini tamamlamak için bir Merkle kanıtı gerektirmeyen üçüncü taraf köprüleri (opens in a new tab) kullanmayı tercih eder.

Bu köprüler tipik olarak, küçük bir ücret karşılığında (genellikle standart bir köprü çekim işlemi için gereken gaz maliyetinden daha az) anında sağladıkları l1'deki varlıklara sahip olarak çalışır. Köprü (veya onu çalıştıran kişiler) l1 varlıklarının yetersiz kalacağını öngördüğünde, l2'den yeterli varlık transfer eder. Bunlar çok büyük çekim işlemleri olduğundan, çekim maliyeti büyük bir miktar üzerinden amorti edilir ve çok daha küçük bir yüzdeye denk gelir.

Umarım bu makale, katman 2'nin nasıl çalıştığı ve açık ve güvenli Solidity kodunun nasıl yazılacağı hakkında daha fazla bilgi edinmenize yardımcı olmuştur.

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