İçeriğe geç

Yükseltilebilir Akıllı Sözleşmeler Nasıl Yazılır?

Merkeziyetsiz uygulamalarda yükseltilebilir akıllı sözleşmelerin önemini keşfedin. Proxy modeli aracılığıyla güncellemeleri, hata düzeltmelerini ve gelişmiş güvenliği nasıl sağladıklarını öğrenin.

8 dk okuma
Yükseltilebilir Akıllı Sözleşmeler Nasıl Yazılır?

Akıllı sözleşmeler, aracılara olan ihtiyacı ortadan kaldıran kendi kendini yürüten programları hayata geçirerek merkeziyetsiz uygulamalar hakkındaki düşünce biçimimizi kökten değiştirdi. Merkeziyetsiz blok zinciri ağlarında şeffaf bir şekilde yürütülen bu sözleşmeler; güvensizlik ve artırılmış güvenlik gibi avantajlar sunar. Ancak bir zamanlar güçlü bir özellik olarak görülen değiştirilemezlikleri, önemli bir zorluk doğurur: Bir akıllı sözleşme dağıtıldıktan sonra artık değiştirilemez. Bu katılık, gerekli güncellemelerin hayata geçirilmesini veya hataların düzeltilmesini güçleştirir.

Bu yazıda yükseltilebilir akıllı sözleşme kavramını ele alacak, neden bu kadar önemli olduklarını açıklayacak ve nasıl çalıştıklarını ayrıntılı biçimde inceleyeceğiz; özellikle Linea zkEVM üzerindeki uygulamalarına odaklanacağız.

Yükseltilebilir akıllı sözleşmeler nedir?

Yükseltilebilir akıllı sözleşmeler, dağıtım sonrasında değişiklik ve iyileştirme yapılmasına olanak tanıyarak geleneksel akıllı sözleşmelerin sınırlamalarını aşar. Geleneksel akıllı sözleşmeler değiştirilemezlikleriyle bilinirken, yükseltilebilir sözleşmeler değiştirilemezliğin güvenliğini esneklikle birleştirir. Bu sayede geliştiriciler, kullanıcıları aksatmadan veya onları yeni bir sözleşmeye taşımak zorunda kalmadan güncellemeleri hayata geçirebilir.

Geleneksel sözleşmelerde hatalar ya da güvenlik açıkları, dağıtımın ardından kalıcı hale gelir. Yükseltilebilir sözleşmeler, dağıtım sonrasında güncellemelere olanak tanıyarak bu sorunu çözer ve şu temel sorunları giderir:

1. Hata düzeltmeleri ve güvenlik açıkları: Geliştiriciler, dağıtım sonrasında güvenlik açıklarını kapatarak kullanıcı varlıklarını koruyabilir.

2. Uyarlanabilirlik: Yükseltilebilir sözleşmeler, yeni protokollerle sorunsuz entegrasyona olanak tanıyarak uygulamaların teknolojik gelişmelerle güncel kalmasını sağlar.

3. Güvenlik iyileştirmeleri: Sürekli gelişen siber tehditler karşısında bu sözleşmeler, tüm sözleşmeyi yeniden dağıtmaya gerek kalmadan yeni güvenlik önlemlerinin kolayca uygulanmasını mümkün kılar.

4. Yönetim esnekliği: Merkeziyetsiz Otonom Organizasyonlar (DAO'lar), yükseltme sürecini yönetmek için topluluk oylamasını kullanabilir; böylece şeffaflık ve paydaş katılımı güvence altına alınır.

5. Basitleştirilmiş güncellemeler: Karmaşık yeniden dağıtım ve veri taşıma gerektiren geleneksel sözleşmelerin aksine, yükseltilebilir sözleşmeler güncellemeleri daha verimli ve kullanıcı dostu bir deneyim için kolaylaştırır.

Yükseltilebilir akıllı sözleşmeler nasıl çalışır?

Yükseltilebilir bir akıllı sözleşme oluşturmanın en yaygın yöntemi proxy kalıbıdır. Bu yaklaşım, sözleşmenin mantığını verisinden ayırarak geliştiricilerin aynı veri depolamasını korurken mantığı güncellemesine olanak tanır.

İşleyiş şu şekildedir:

  1. Proxy Sözleşmesi: Bu sözleşme, depolamayı (durum değişkenlerini) tutar ve kullanıcılar için arayüz işlevi görür.

  2. Uygulama Sözleşmesi: Bu sözleşme, yürütülebilir kodu (iş mantığını) içerir.

  3. Yetkilendirme: Proxy sözleşmesi, Ethereum'un delegatecall özelliğini kullanarak fonksiyon çağrılarını uygulama sözleşmesine devreder. Bu sayede proxy, aynı depolamayı korurken uygulama kodunu yürütebilir.

Bir yükseltme gerektiğinde, proxy sözleşmesi yeni bir uygulama sözleşmesine yönlendirilir; böylece sözleşmenin depolaması değiştirilmeden veya kullanıcılar aksatılmadan mantık güncellemeleri yapılabilir. Bu süreci örneklemek için bir DAO proxy sözleşmesi kullanan şu örneğe bakabilirsiniz:

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

contract DAOProxy {
    // Storage slot for the address of the implementation contract
    bytes32 private constant implementationSlot = keccak256("simple.dao.proxy.implementation");
    // Storage slot for the admin address
    bytes32 private constant adminSlot = keccak256("simple.dao.proxy.admin");

    // Modifier to restrict access to admin
    modifier onlyAdmin() {
        require(msg.sender == _admin(), "Caller is not admin");
        _;
    }

    // Constructor to set the initial implementation and admin
    constructor(address _implementation) {
        _setImplementation(_implementation);
        _setAdmin(msg.sender);
    }

    // Fallback function to delegate calls to the implementation
    fallback() external payable {
        _delegate(implementation());
    }

    // Receive function to handle plain Ether transfers
    receive() external payable {
        _delegate(implementation());
    }

    // Internal function to delegate the call to the implementation
    function _delegate(address _implementation) internal {
        assembly {
            // Copy msg.data
            calldatacopy(0, 0, calldatasize())
            // Call the implementation
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
            // Copy the returned data
            returndatacopy(0, 0, returndatasize())
            // Handle result
            switch result
            case 0 {
                // Revert if the call failed
                revert(0, returndatasize())
            }
            default {
                // Return data if the call succeeded
                return(0, returndatasize())
            }
        }
    }

    // Getter for the implementation address
    function implementation() internal view returns (address impl) {
        bytes32 slot = implementationSlot;
        assembly {
            impl := sload(slot)
        }
    }

    // Getter for the admin address
    function _admin() internal view returns (address adm) {
        bytes32 slot = adminSlot;
        assembly {
            adm := sload(slot)
        }
    }

    // Function to upgrade the implementation contract
    function upgradeTo(address newImplementation) external onlyAdmin {
        _setImplementation(newImplementation);
    }

    // Internal function to set the implementation address
    function _setImplementation(address newImplementation) internal {
        bytes32 slot = implementationSlot;
        assembly {
            sstore(slot, newImplementation)
        }
    }

    // Function to change admin
    function changeAdmin(address newAdmin) external onlyAdmin {
        _setAdmin(newAdmin);
    }

    // Internal function to set the admin address
    function _setAdmin(address newAdmin) internal {
        bytes32 slot = adminSlot;
        assembly {
            sstore(slot, newAdmin)
        }
    }
}
Kopyala
    bytes32 private constant implementationSlot = keccak256("simple.dao.proxy.implementation");
    bytes32 private constant adminSlot = keccak256("simple.dao.proxy.admin");
Kopyala

Dikkat edilmesi gereken iki önemli nokta, uygulama sözleşmesinin adresi için kullanılan depolama slotu ve yönetici adresi için kullanılan depolama slotudur. Bunlar, proxy sözleşmesi içinde uygulama adresi için bir depolama slotu hesaplamak amacıyla kullanılan kasıtlı ve benzersiz tanımlayıcılardır.

Proxy kalıbını delegatecall ile kullanırken (bu fonksiyona yakında daha ayrıntılı bakacağız), uygulama sözleşmesi proxy'nin depolama bağlamında çalışır. Bu şu anlama gelir:

  • Paylaşılan depolama alanı: Her iki sözleşme de aynı depolama düzenini paylaşır.

  • Üzerine yazma riski: Dikkatli bir yönetim olmadan, proxy sözleşmesindeki değişkenler uygulama sözleşmesi tarafından üzerine yazılabilir.

Karma ile oluşturulmuş benzersiz bir tanımlayıcı kullanılarak:

  • Yalıtım: Proxy tarafından kullanılan depolama slotlarının uygulama sözleşmesindeki herhangi bir değişkenle çakışma olasılığı en aza indirilir.

  • Tutarlılık: Uygulama adresinin gerektiği her yerde sözleşme genelinde aynı depolama slotu tutarlı biçimde kullanılır.

constructor(address _implementation) {
        _setImplementation(_implementation);
        _setAdmin(msg.sender);
    }
Kopyala

Bir diğer önemli husus, proxy sözleşmesinin constructor'ıdır. Bu constructor bir _implementation parametresi içerir; yani uygulama sözleşmesinin önce dağıtılması ve adresinin proxy sözleşmesi dağıtılırken argüman olarak iletilmesi gerekir.

 fallback() external payable {
        _delegate(implementation());
    }

    receive() external payable {
        _delegate(implementation());
    }
Kopyala

Şimdi fallback ve receive fonksiyonlarına geliyoruz; bunlar Solidity fonksiyonlarıdır ve çağrıları ile Ether transferlerini uygulama sözleşmesine iletmede kritik roller üstlenir. Bir sözleşmenin açıkça tanımlanmış fonksiyonlarından hiçbiriyle eşleşmeyen çağrılar veya fonlar aldığı durumları ele alırlar. Bu fonksiyonların ne işe yaradığına dair ayrıntılı bir açıklama şu şekildedir:

1. Fallback fonksiyonu

  • Fallback mekanizması: Solidity'deki fallback fonksiyonu, iki ana koşulda çalışan özel bir fonksiyondur:- Sözleşmede mevcut olmayan bir fonksiyon çağrıldığında.

  • Herhangi bir fonksiyon imzasıyla eşleşmeden sözleşmeye veri gönderildiğinde.

  • Yetkilendirme: DAOProxy sözleşmesi bağlamında, fallback fonksiyonu tüm bu çağrıları mevcut uygulama sözleşmesine devreder.

DAOProxy'de nasıl çalışır:

  • Fonksiyon çağrısı yönetimi:- Bir kullanıcı proxy sözleşmesinde tanımlanmayan bir fonksiyonu çağırdığında fallback fonksiyonu tetiklenir.

  • Proxy sözleşmesi ağırlıklı olarak upgradeTo ve changeAdmin gibi yönetimsel fonksiyonlar içerdiğinden, uygulama sözleşmesinin iş mantığı fonksiyonlarını barındırmadığından bu durum yaygındır.

  • Uygulamaya yetkilendirme:- Fallback fonksiyonu, çağrıyı uygulama sözleşmesine ileterek _delegate(implementation()) çağrısını yapar.

  • _delegate, proxy'nin depolama bağlamında çalışırken uygulamanın kodunu yürütmek için delegatecall kullanan dahili bir fonksiyondur.

  • Yürütme bağlamı:- Tüm durum değişiklikleri proxy'nin depolamasında gerçekleşir.

  • msg.sender ve msg.value değişmeden kalır; orijinal çağıranın bağlamı korunur.

2. Receive fonksiyonu

  • Ether alımı: Solidity 0.6.0 ile tanıtılan receive fonksiyonu, bir sözleşme herhangi bir eşlik eden veri olmaksızın Ether aldığında (yani msg.data boş olduğunda) çalışır.

  • Ether transferlerinin yetkilendirilmesi: DAOProxy sözleşmesinde, düz Ether transferleri uygulama sözleşmesine devredilir.

DAOProxy'de nasıl çalışır:

  • Ether transfer yönetimi:- Birisi proxy sözleşmesinin adresine veri olmaksızın Ether gönderdiğinde receive fonksiyonu tetiklenir.

  • Bu durum, yalnızca to adresini ve değeri (Ether miktarını) belirten, belirli bir fonksiyonu çağırmayan bir işlem aracılığıyla gerçekleşir.

  • Uygulamaya yetkilendirme:- Fallback fonksiyonuna benzer şekilde, receive de _delegate(implementation()) çağrısını yaparak Ether transferini uygulama sözleşmesine iletir.

  • Uygulama sözleşmesi, alınan Ether'in nasıl işleneceğini tanımlar (örn. bakiyeleri güncelleme, olay yayma).

 function _delegate(address _implementation) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }
Kopyala

Şimdi _delegate fonksiyonuna geliyoruz; bu fonksiyon, proxy'nin orijinal çağrının bağlamını korurken fonksiyon çağrılarını uygulama sözleşmesine iletmesini sağlar. Bu sayede sözleşme mantığı, sözleşmenin adresi değiştirilmeden veya durumu bozulmadan zaman içinde yükseltilebilir.

Diğer fonksiyonlar ise proxy sözleşmemizin durum değişkenlerini yönetmemize yardımcı olan getter ve setter'lardır. Bunu başarmak için assembly anahtar kelimesini kullanırız; bu anahtar kelime, geliştiricilerin sözleşmelerine doğrudan düşük seviyeli assembly kodu eklemesine olanak tanır. Bu özellik, Ethereum Sanal Makinesi'ne (EVM) doğrudan erişim sağlayarak sözleşme davranışı üzerinde daha hassas kontrol, gaz tüketiminin optimizasyonu ve Solidity'nin üst düzey kodunda doğrudan mevcut olmayan ya da daha az verimli olan işlemlerin yürütülmesini mümkün kılar.

Şimdi asıl büyük soru şu: Bu proxy sözleşmesini kullanarak uygulama akıllı sözleşmemizle nasıl etkileşime geçeriz?

Bunun için uygulama akıllı sözleşmesinin ABI'sine ihtiyacımız var. Proxy, çağrıları uygulama sözleşmesine devrettiğinden, hangi fonksiyonları çağırabileceğinizi bilmek için ABI'ye ihtiyaç duyarsınız. Ayrıca blok zinciriyle etkileşimde uygulama sözleşmesinin değil, proxy sözleşmesinin adresini kullanacağız. Proxy, durumu tutar ve çağrıları uygulamaya devreder; dolayısıyla tüm etkileşimler proxy üzerinden gerçekleşmelidir.

Bir JavaScript projesinde Ethers.js kullanarak blok zinciriyle etkileşime geçtiğimizi hayal edelim. İşte nasıl görüneceği:

const proxyAddress = "0xProxyContractAddress";
const implementationABI = [ /* ABI array of the implementation contract */ ];

const proxyContract = new ethers.Contract(proxyAddress, implementationABI, provider);

// For a function 'setValue(uint256 newValue)'
await proxyContract.connect(signer).setValue(42);

// For a function 'getValue()'
const value = await proxyContract.getValue();
console.log("Value:", value);
Kopyala

Bu şekilde, proxy sözleşmemizi kullanarak uygulama sözleşmemizdeki varsayımsal setValue fonksiyonunu çağırıyoruz.

Yükseltilebilir sözleşmeleri ele aldığımıza göre, bir diğer önemli soru da şu: Sözleşmelerimi nasıl yükseltirim?

Her şeyden önce, yalnızca yönetici (proxy sözleşmesini dağıtan kişi) uygulama sözleşmesini yükseltebilir. Bunun için yöneticinin önce blok zincirine uygulama sözleşmesinin yeni bir sürümünü dağıtması gerekir. Ardından proxy sözleşmesindeki upgradeTo fonksiyonunu çağırarak yeni sözleşme adresini parametre olarak iletmelidir. Görünümü şu şekilde olacaktır:

await proxyContract.connect(adminSigner).upgradeTo(newImplementationAddress);

Akıllı sözleşmelerin geleceğini kucaklamak

Yükseltilebilir akıllı sözleşmeler, dağıtım sonrasında değişiklik ve iyileştirmelere olanak tanıdıkları için blok zinciri geliştirmede büyük önem taşır. Proxy kalıbından yararlanarak geliştiriciler, kullanıcı deneyimini aksatmadan veya varlıkları taşımak zorunda kalmadan yeni özellikler sorunsuzca sunabilir, hataları düzeltebilir ve değişen gereksinimlere yanıt verebilir.

Ancak büyük güç, büyük sorumluluk getirir. Yükseltilebilir sözleşmelere, mekaniklerini, güvenlik etkilerini ve en iyi uygulamaları kapsamlı biçimde anlayarak yaklaşmak son derece önemlidir. Depolama düzeni tutarlılığının sağlanması, güçlü erişim kontrollerinin uygulanması ve yükseltmelerin kapsamlı biçimde test edilmesi, olası risklere karşı korunmak için atılması gereken temel adımlardır.

Yükseltilebilir sözleşmeler geliştirme yolculuğunuza başlarken, süreci kolaylaştırmak ve güvenliği artırmak için OpenZeppelin'in Upgrades Plugin'i gibi köklü araçlardan yararlanmayı düşünün. Keyifli kodlamalar!

Yapay zeka tarafından çevrildi. Hatalar içerebilir. Lütfen her zaman bilgileri doğrulayın.

Çeviriyi değerlendir

Bu makalenin yazarı: