Akıllı Sözleşmeler

Solidity’deki akıllı sözleşmeler nesne yönelimli programlama dillerine benzerdir. State değişkenlerinde kalıcı data içerirler ve fonksiyonlar bu değişkenlerin değerini değiştirebilir. Başka bir akıllı sözleşmedeki fonksiyonu çağırmak bir EVM fonksiyon çağrısı gerçekleştirir ve burada çağıran akıllı sözleşmenin state değişkenlerine erişilemez. Akıllı sözleşmede herhangi bir şeyin yaşanmasını istiyorsanız o akıllı sözleşmenin herhangi bir fonksiyonunu çağırmanız gerekir. Çünkü Ethereum’da “cron” konsepti yoktur, yani akıllı sözleşmeler kendi başlarına bir şeyler yapamaz. Dışarıdan tetiklenmeleri gerekir.

Akıllı Sözleşme Oluşturma

Akıllı sözleşmeler iki şekilde oluşturulabilir; “dışarıdan” bir Ethereum transactionı ile veya Solidity kullanarak direkt başka bir akıllı sözleşme içerisinde.

Remix gibi IDE’ler oluşturma aşamasını kullanıcı arayüzü kullanarak kolayca gerçekleştirmenize yardımcı olur.

Programlama ile akıllı sözleşme oluşturmanın bir yolu ise web3.js gibi bir JavaScript API kullanımıdır. Akıllı sözleşme oluşturmaya yarayan web3.eth.Contract isimli bir methodu vardır.

Bir akıllı sözleşme oluşturulduğu zaman constructor isimli bir fonksiyon sadece bir kere olmak üzere çalıştırılır. Bundan sonra bu fonksiyona erişim mümkün değildir.

Constructor kullanmak zorunlu değildir. Bir akıllı sözleşmede sadece bir adet constructor olabilir ve overloading yapılması mümkün değildir.

Constructor çalıştırıldıktan sonra akıllı sözleşme kodunun son hali blok zincirinde saklanır. Bu kod bütün public ve external fonksiyonları içerir. Deploy edilen kod constructor fonksiyonu ve sadece constructor içerisinde çağrılan internal fonksiyonları içermez.

Özünde constructor parametreleri ABI encoded olarak akıllı sözleşme kodunun sonuna eklenir (bytecode halinin), ama eğer web3.js kullanıyorsanız bunu umursamanıza gerek yok. Çünkü o sizin için bu işlemleri gerçekleştiriyor.

Eğer bir akıllı sözleşme başka bir akıllı sözleşme oluşturmak istiyorsa, oluşturmak istediği akıllı sözleşmenin kaynak kodunu (ve binary halini) bilmelidir. Bu demektir ki döngüsel olarak akıllı sözleşme oluşturmak mümkün değildir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;


contract OwnedToken {
    // `TokenCreator` aşağıda belirtilmiş bir akıllı sözleşme tipidir.
    // Yeni bir akıllı sözleşme oluşturmak için kullanılmadığı sürece
    // referans etmekte sorun yoktur.
    TokenCreator creator;
    address owner;
    bytes32 name;

    // Burası constructor fonksiyonumuz. Burada
    // belirtilen isim ve akıllı sözleşmeyi oluşturan adres
    // akıllı sözleşmede kaydedilir.
    constructor(bytes32 name_) {
        // State değişkenlerine isimleri kullanılarak
        // erişilir. `this.owner` şeklinde bir kullanım
        // ile değil. Fonksiyonlara direkt olarak kendi
        // isimlerini kullanarak veya `this.f` şeklinde
        // bir kullanım ile erişebiliriz. Ancak ikinci
        // şekildeki kullanım external olarak (dışarıdan)
        // bir görüş sağlar. Özellikle constructorlarda,
        // fonksiyonlara external olarak erişmemelisiniz.
        // Çünkü o fonksiyonlar henüz oluşturulmadı, yani
        // erişilebilir değil.
        // Daha fazlası için bir sonraki bölüme bakabilirsiniz.
        owner = msg.sender;

        // Burada `address` tipinden `TokenCreator` tipine
        // bir explicit (açık) dönüşüm sağlarız ve bu fonksiyonu
        // çağıran akıllı sözleşmenin bir `TokenCreator` olduğunu varsayarız.
        // Bunu doğrulamanın gerçek bir yöntemi bulunmamakta.
        // Bu işlem yeni bir akıllı sözleşme oluşturmaz.
        creator = TokenCreator(msg.sender);
        name = name_;
    }

    function changeName(bytes32 newName) public {
        // Sadece `creator` `name` değişkenini değiştirebilir.
        // Akıllı sözleşmenin adresini explicit bir dönüşüm ile
        // elde edebilir ve karşılaştırmamızı yapabiliriz.
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) public {
        // Sadece şu anki `owner` token transferi gerçekleştirebilir.
        if (msg.sender != owner) return;

        // `creator` adresindeki akıllı sözleşmenin bir fonksiyonunu
        // kullanarak, işlemin gerçekleştirilebilirliğini
        // kontrol edebilir. Eğer bu işlem hata verirse
        // (örneğin, out-of-gas (gazın tükenmesi)),
        // işlem burada son bulur.
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}


contract TokenCreator {
    function createToken(bytes32 name)
        public
        returns (OwnedToken tokenAddress)
    {
        // Yeni bir `Token` akıllı sözleşmeyi oluşturur ve adresini return eder.
        // JavaScript tarafında return tipi `address` tipidir.
        return new OwnedToken(name);
    }

    function changeName(OwnedToken tokenAddress, bytes32 name) public {
        // `tokenAddress` isimli parametrenin tipi
        // `address` tipindendir.
        tokenAddress.changeName(name);
    }

    // Bir transferin gerçekleşip gerçekleşmeyeceğini belirler
    function isTokenTransferOK(address currentOwner, address newOwner)
        public
        pure
        returns (bool ok)
    {
        // Keyfi bir koşul ile işlemin gerçekleşip gerçekleşmeyeceğini
        // belirler ve sonucu return eder.
        return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
    }
}

Görünürlük ve Getter Fonksiyonlar

Durum Değişkenlerinde Görünürlük

public

Public durum değişkenleri internallerden sadece bir açıdan farklıdır, o da derleyicinin direkt olarak bir getter fonksiyon oluşturmasıdır. Bu şekilde diğer akıllı sözleşmeler bu değerlere erişebilir. Aynı fonksiyon içerisinde external erişim sağlandığında (örneğin, this.x) getter fonksiyonu çağrılırken, internal erişimde (örneğin, x) değer direkt olarak storage’den alınır. Setter fonksiyonlar derleyici tarafından tanımlanmaz. Bu yüzden siz kendiniz bir setter fonksiyon eklemediyseniz diğer fonksiyonlar bu değişkeni değiştiremez.

internal

Internal durum değişkenleri sadece tanımlandıkları akıllı sözleşmeler ve o akıllı sözleşmelerden türetilen (inherited) akıllı sözleşmeler tarafından erişebilir durumdadır. External erişim mümkün değildir. Bütün durum değişkenlerinin default hali internal’dir.

private

Private durum değişkenlerine sadece tanımlandıkları akıllı sözleşmeden erişim mümkündür. Internal’den farklı olarak türetilen akıllı sözleşmelerden de erişilemez.

Uyarı

Bir şeyi private veya internal yapmak sadece diğer akıllı sözleşmelerin o bilgiye erişimini veya değiştirilmesini engeller. Ama bu bilgiler blok zinciri dışından erişilebilir durumdadır.

Fonksiyonlarda Görünürlük

Solidity iki tip fonksiyon çağrısı bilir: gerçek bir EVM mesaj çağrısı yapan external’lar ve bu çağrıyı yapmayan internal’lar. Ayrıca internal fonksiyonlar türetilen fonksiyonlardan erişilemez hale de getirilebilir. Bu da ortaya dört çeşit fonksiyon görünürlüğü çıkarır.

external

External fonksiyonlar akıllı sözleşme interface’inin bir parçasıdır, bu da demektir ki diğer akıllı sözleşmeler ve transactionlar tarafından çağrılabilirler. Bir f external fonksiyonu internal olarak çağrılamaz (yani, f() işe yaramaz, ama this.f() çalışır).

public

Public fonksiyonlar akıllı sözleşme interface’inin bir parçasıdır ve internal olarak veya mesaj çağrıları ile kullanılabilirler.

internal

Internal fonksiyonlar sadece tanımlandıkları akıllı sözleşmeden veya o akıllı sözleşmeden türetilen akıllı sözleşmeler tarafından erişilebilir. External olarak erişim mümkün değildir. ABI kullanılarak external olarak erişilemese bile mapping ve storage referanslarını parametre olarak alabilirler.

private

Private fonksiyonlar internaller gibidir ama bunlara türetilen fonksiyonlardan da erişim mümkün değildir.

Uyarı

Bir şeyi private veya internal yapmak sadece diğer akıllı sözleşmelerin o bilgiye erişimini veya değiştirilmesini engeller. Ama bu bilgiler blok zinciri dışından erişilebilir durumdadır.

Görünürlük parametreleri durum değişkenleri için değişkenin tipinden sonra yazılırken fonksiyonlarda parametreler ve return tanımının arasına yazılır.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

Aşağıdaki örnekte, D, c.getData() çağrısı yapabilir ve data değerini elde eder, ama f fonksiyonunu çağıramaz. E akıllı sözleşmeleri ise C akıllı sözleşmesinden türetildiği için compute fonksiyonunu çağırabilir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    uint private data;

    function f(uint a) private pure returns(uint b) { return a + 1; }
    function setData(uint a) public { data = a; }
    function getData() public view returns(uint) { return data; }
    function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}

// Bu akıllı sözleşme derlenemez, hata verir
contract D {
    function readData() public {
        C c = new C();
        uint local = c.f(7); // hata: `f` görünür değil
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // hata: `compute` görünür değil
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // internal fonksiyona türetilen fonksiyon sayesinde erişim sağlanabilir
    }
}

Getter Fonksiyonlar

Derleyici bütün public durum değişkenleri için getter fonksiyonu oluşturur. Örneğin aşağıdaki akıllı sözleşme için, derleyici data adında bir fonksiyon üretir. Bu fonksiyon hiçbir parametre almaz ve uint tipinde bir değişken return eder. Return edilen değer ise data değişkeninde saklanan değerdir. Durum değişkenleri tanımlandıkları yerde initialize edilebilir (initialize, bir değişkenin ilk defa tanımlanması olarak çevrilebilir).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    uint public data = 42;
}

contract Caller {
    C c = new C();
    function f() public view returns (uint) {
        return c.data();
    }
}

Getter fonksiyonların görünürlüğü external’dir. Eğer internal olarak erişim sağlandıysa (this. olmadan), bu bir durum değişkenine erişim anlamına gelir. Eğer external olarak erişildiyse (this. kullanarak), bu getter fonksiyonuna erişim anlamına gelir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract C {
    uint public data;
    function x() public returns (uint) {
        data = 3; // internal erişim
        return this.data(); // external erişim
    }
}

Eğer bir public görünürlüğe sahip dizi tipinden bir durum değişkenine sahipseniz, getter fonksiyonunu kullanarak sadece tek bir elemana erişim sağlayabilirsiniz. Bu mekanizma tüm diziyi return ederken oluşan yüksek gaz ücretlerinden sıyrılmak için kurulmuştur. Hangi elemanın return edileceğini belirtmek için parametreleri kullanabilirsiniz (örneğin, myArray(0)). Eğer bütün diziyi tek bir fonksiyon ile elde etmeniz gerekiyorsa, bunun için aşağıdaki gibi bir fonksiyon yazmanız gerekir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract arrayExample {
    // public durum değişkeni
    uint[] public myArray;

    // Derleyici tarafından tanımlanan getter fonksiyonu
    /*
    function myArray(uint i) public view returns (uint) {
        return myArray[i];
    }
    */

    // Bütün array'i return eden fonksiyon
    function getArray() public view returns (uint[] memory) {
        return myArray;
    }
}

Artık tüm dizine erişmek için her bir aramayı tek bir öğeye döndüren myArray(i) yerine getArray() kullanabilirsiniz.

Sıradaki örnek biraz daha karmaşık.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
        uint[3] c;
        uint[] d;
        bytes e;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

Derleyici bize aşağıdaki gibi bir getter fonksiyonu oluşturur. Struct’daki mapping’ler ve diziler (byte dizileri istisnadır) gözardı edilmiştir. Çünkü getter fonksiyonlarında onların spesifik bir elemanına uygun bir şekilde erişim mümkün değildir.

function data(uint arg1, bool arg2, uint arg3)
    public
    returns (uint a, bytes3 b, bytes memory e)
{
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
    e = data[arg1][arg2][arg3].e;
}

Fonksiyon Modifier’ları

Modifier’lar fonksiyonların tanımlandığı şekillerinden farklı davranmalarını sağlamak için kullanılabilir. Örneğin, bir fonksiyonun çalıştırılmasından hemen önce bir koşulun kontrolünü gerçekleştirebilirsiniz.

Modifier’lar akıllı sözleşmelerin türetilebilen özelliklerindendir. Bu yüzden türetilmiş bir akıllı sözleşme bir modifier’ı eğer virtual olarak belirtilmişse onu override edebilir. Daha fazla bilgi için Modifier Overriding kısmına bakabilirsiniz.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;

    // Bu akıllı sözleşme sadece bir tane modifier tanımlar ve onu da kullanmıyor.
    // Tanımlanan modifier türetilen fonksiyonda kullanılacaktır.
    // Fonksiyon içerisindeki kodların kullanılacağı yer
    // `_;` şeklinde modifier içerisinde belirtilir.
    // Yani `_;` gördüğünüz yerde o modifier'ın kullanıldığı fonksiyonun
    // içerisindeki kodlar yazılmış gibi düşünebilirsiniz.
    // Bu modifier eklendiği fonksiyonu sadece akıllı sözleşmeyi oluşturan
    // kişinin çağırmasını sağlar. Diğer erişimlerde ise işlemi revert eder.
    modifier onlyOwner {
        require(
            msg.sender == owner,
            "Only owner can call this function."
        );
        _;
    }
}

contract destructible is owned {
    // Bu akıllı sözleşme türetildiği `owned` fonksiyonundaki
    // `onlyOwner` modifier'ını `destroy` fonksiyonuna ekler.
    // Böylece `destroy` fonksiyonunu sadece `owner` çağırabilir.
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // Modifier'lar parametre alabilir:
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, destructible {
    mapping (address => bool) registeredAddresses;
    uint price;

    constructor(uint initialPrice) { price = initialPrice; }

    // Buradaki `payable` sözcüğü de oldukça önemlidir.
    // Eğer bu fonksiyon `payable` olmazsa kendisine gelen bütün
    // etherleri reddeder.
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    function changePrice(uint price_) public onlyOwner {
        price = price_;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(
            !locked,
            "Reentrant call."
        );
        locked = true;
        _;
        locked = false;
    }

    /// Bu fonksiyon bir mutex ile korunmaktadır.
    /// Yani, bu akıllı sözleşme re-entrancy çağrılarına karşı zaafiyetli değildir.
    /// `return 7` fonksiyonun bittiğini belirtse de henüz modifier'ımızın işi bitmedi.
    /// `locked = false;` satırı return ifademizden sonra çalışır.
    function f() public noReentrancy returns (uint) {
        (bool success,) = msg.sender.call("");
        require(success);
        return 7;
    }
}

Eğer C akıllı sözleşmesindeki m modifier’ına erişmek istiyorsanız, C.m şeklinde erişebilirsiniz. Modifier’lar sadece tanımlandıkları akıllı sözleşmede veya türetilen bir akıllı sözleşmede kullanılabilir. Modifier’lar kütüphanelerde de tanımlanabilir. Ancak kullanımları o kütüphanenin fonksiyonlarıyla kısıtlıdır. Yani tanımlandıkları kütüphane dışında kullanılamazlar.

Bir fonksiyona birden fazla modifier tanımlanabilir. Bunu gerçekleştirmek için her bir modifier isminden sonra bir boşluk bırakılmalıdır. Modifier’lar tanımlandıkları sıraya göre çalışacaktır.

Modifier’lar eklendikleri fonksiyonların parametrelerine veya return değerlerine kendi başlarına erişemezler. Eğer bir parametreyi bir modifier’da kullanmak istiyorsanız, o modifier’ı eklediğiniz yerde parametreyi de vermelisiniz. Fonksiyon çağırma yapısına benzer bir şekilde kullanılırlar.

Modifier’daki veya fonksiyon’daki return işlemi sadece o yazıldığı modifier’dan veya fonksiyon’dan çıkmaya yarar. Program akışı _ işaretinin olduğu yerden çalışmaya devam eder.

Uyarı

Daha önceki Solidity versiyonlarında modifier’a sahip fonksiyonlarda return ifadesi farklı bir şekilde davranış sergiler.

Açık bir şekilde return; ifadesinin yer aldığı bir modifier, fonksiyonun return edeceği değerle alakalı değildir. Modifier’lar fonksiyon içerisindeki kodları hiç çalıştırmamayı da tercih edebilirler. Bu durumda return değerleri default değerlerine eşitlenebilir. Böylelikle, fonksiyonun hiç bir kodu yokmuş gibi bir davranış sergilenir.

_ sembolü bir modifier’da birden fazla kez kullanılabilir. Her bir kullanım, fonksiyon içerisindeki kodla değiştirilecektir. Yani, _ gördüğünüz her yerde, eklenen fonksiyonun kodlarının bulunduğunu düşünebilirsiniz.

Modifier’lar parametre alabildiği için, bir fonksiyondaki bütün parametreler istenilen modifier’a gönderilebilir. Modifier’da tanımlanan semboller, fonksiyonlarda görülemez (override ile değiştirilebilir).

Constant ve Immutable State Değişkenleri

State değişkenleri constant veya immutable olarak tanımlanabilir. Her iki durumda da akıllı sözleşme kurulduktan sonra (constructor çalıştıktan sonra) bu tür değişkenler değiştirilemez. constant compile-time’da (kodun içerisinde) tanımlı olması gerekirken, immutable değişkenler constructor içerisinde de tanımlanabilir.

constant değişkenleri akıllı sözleşmelerin dışarısında (dosya seviyesinde) da tanımlayabiliriz.

Derleyici bu tür değişkenler için storage’de slot ayırmaz. Çünkü bu değişkenlerin kullanıldığı her yer, belirlenmiş değerle değiştirilir.

Normal state değişkenleriyle karşılaştırıldığında, constant ve immutable değişkenler çok daha az gaz harcar. Constant değişkenlerde, kullanıldıkları her yere karşılığında verilen değer kopyalanıp yapıştırılır. Bu, lokal optimizasyon olarak kullanılır. Immutable değişkenlerde ise, akıllı sözleşmenin kurulum anında (construction time) karşılık gelen değeri belirlenir ve kullanıldığı her yere kopyalanıp yapıştırılır. Bu değerler 32 byte’dan daha az yer kaplasa bile 32 byte’lık bir alanda muhafaza edilir. Bu sebepten ötürü, bazı durumlarda constant değerler kullanmak, immutable değerleri kullanmaktan daha ucuz olabilir.

Şu anda constant ve immutable bütün tipler için uygulanamamaktadır. Desteklenen tipler strings (sadece constant) ve değer tipleridir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.4;

uint constant X = 32**22 + 8;

contract C {
    string constant TEXT = "abc";
    bytes32 constant MY_HASH = keccak256("abc");
    uint immutable decimals;
    uint immutable maxBalance;
    address immutable owner = msg.sender;

    constructor(uint decimals_, address ref) {
        decimals = decimals_;
        // Immutable tanımlamalarında blok zincirinden veri de okunabilir.
        maxBalance = ref.balance;
    }

    function isBalanceTooHigh(address other) public view returns (bool) {
        return other.balance > maxBalance;
    }
}

Constant

constant değişkenlerin değerleri derleme anında (compile-time) sabit olmalı ve değişkenin tanımlandığı konumda belirtilmelidir. Herhangi bir storage’e erişim, blok zinciri verisi (örneğin, block.timestamp, address(this).balance veya block.number) veya çalıştırma verisi (msg.value veya gasleft()) veya başka akıllı sözleşmelere yapılan external çağrılara izin verilmez. Kullanılacak memory’i belirleme konusunda yan etki oluşturacak tanımalamalara izin verilirken, başka memory objeleri üzerinde yan etki oluşturan tanımlamalara izin verilmez. Built-in fonksiyonlarından keccak256, sha256, ripemd160, ecrecover, addmod ve mulmod fonksiyonlarının kullanımına izin verilmiştir (keccak256 başka akıllı sözleşmeleri çağırsa da, bir istisnadır).

Memory belirleyicisi üzerinde yan etkiye izin verilmesinin sebebi, karmaşık yapılarında kurulabilinmesi gereksinimidir (örneğin, lookup-table). Bu özellikler henüz tamamen kullanılabilir değildir.

Immutable

immutable olarak tanımlanan değişkenler constant olarak tanımlananlara göre biraz daha az kısıtlanmıştır: Immutable değişkenler akıllı sözleşmenin constructor fonksiyonunda keyfi bir değere atanabilir. Sadece bir kere tanımlanabilirler ve tanımlandıktan sonra istenilen anda sahip oldukları değer okunabilir.

Derleyici tarafından oluşturulmuş akıllı sözleşmenin creation code’u, runtime code’u return etmeden önce bütün immutable referanslarını tanımlanan değerle değiştirir. Bu yüzden immutable değişken kullandığınız bir akıllı sözleşme için, compiler’ın oluşturduğu runtime code ile blok zincirinde saklanan runtime code’u karşılaştırdığınızda farklı sonuçlar alırsınız.

Not

Tanımlandıkları satırda direkt olarak değerleri atanan immutable değişkenler akıllı sözleşmenin constructor fonksiyonu çalıştıktan sonra initialize edilmiş olarak düşünülür. Bu demek oluyor ki başka bir immutable değişkenin değerini kullanan bir immutable değişkenin değerini direkt olarak atayamazsınız. Bunu ancak constructor içerisinde yapabilirsiniz.

Bu state değişkenlerini ilk defa tanımlama sırasının farklı bir şekilde yorumlanmasını engellemek amacıyla konulmuş bir önleyicidir, özellikle de türetme (inheritance) konusunda.

Fonksiyonlar

Fonksiyonlar akıllı sözleşmelerin içerisinde veya dışarısında tanımlanabilir.

Akıllı sözleşmelerin dışarısında tanımlanan fonksiyonlara “özgür fonksiyonlar” denir ve her zaman internal görünürlüktedirler. Kodları, o fonksiyonları kullanan her bir akıllı sözleşmeye eklenir, tıpkı internal kütüphane fonksiyonları gibi.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}

contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        // Burada özgür bir fonksiyon internal olarak çağrılıyor.
        // Derleyici `sum` fonksiyonunu bu akıllı sözleşmenin kodları arasına ekleyecek.
        uint s = sum(arr);
        require(s >= 10);
        found = true;
    }
}

Not

Akıllı sözleşme dışında tanımlanan bir fonksiyon her zaman o akıllı sözleşmenin içeriği ile birlikte çalıştırılırlar. Hâlâ diğer akıllı sözleşmeleri çağırabilir, onlara Ether gönderebilir ve kendilerini çağıran akıllı sözleşmeleri yok edebilirler. Akıllı sözleşme içerisinde tanımlanan bir fonksiyon ile özgür bir fonksiyonun arasındaki en temel farklar özgür fonksiyonların this değişkenine erişimi olmaması, ve de kendi alanlarında (scope) bulunmayan storage değişkenlerine ve fonksiyonlara direkt erişime sahip olmamalarıdır.

Fonksiyon Parametreleri ve Return Parametreleri

Fonksiyonlar tipi belirtilmiş parametreler alabilir ve diğer birçok programlama dilinin aksine keyfi sayıda değişkeni return edebilirler.

Fonksiyon Parametreleri

Fonksiyon parametreleri değişkenlerle aynı şekilde tanımlanırlar. Ayrıca kullanılmayan parametreler gözardı edilebilirler.

Örneğin, eğer akıllı sözleşmenizdeki bir fonksiyonun iki adet integer değişkeni parametre olarak almasını isterseniz, aşağıdaki gibi bir yapı kullanabilirsiniz:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}

Fonksiyon parametreleri herhangi bir lokal değişken olarak kullanılaiblir ve ayrıca lokal değişkenlere atanabilirler.

Not

Bir external fonksiyon çok boyutlu bir diziyi parametre olarak alamazlar. Bu özelliği eğer ABI coder v2’yi kaynak kodunuzda pragma abicoder v2; bu şekilde aktifleştirdiyseniz kullanabilirsiniz.

Bir internal fonksiyon o özelliği aktifleştirmeden de çok boyutlu bir diziyi parametre olarak alabilir.

Return Değişkenleri

Fonksiyon return değişkenleri aynı şekilde returns sözcüğünden sonra tanımlanır.

Örneğin, iki adet sonucu return etmek istediğinizi düşünün: fonksiyon parametresi olarak verilmiş iki adet integer’ın toplamı ve çarpımı. Şu şekilde bir kod işinizi görecektir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}

Return değişkenlerinin tipleri gözardı edilebilirler. Return değişkenleri herhangi bir lokal değişken olarak kullanılabilirler. Bu değişkenler direkt olarak default değerine eşitlenir ve değiştirilene kadar bu değere eşit olurlar.

İsterseniz yukarıdaki gibi açık bir şekilde return değişkenlerinin değerlerini verebilir veya aşağıdaki gibi direkt olarak return ifadesini kullanabilirsiniz (ister tek, isterseniz de çoklu return):

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        return (a + b, a * b);
    }
}

Eğer fonksiyondan çıkmak için erkenden return kullanmanak istiyorsanız, bütün return değişkenlerini vermeniz gerekir.

Not

Bazı tipleri internal olmayan fonksiyonlardan return edemezsiniz, örneğin, çok boyutlu dinamik boyutlu diziler ve structlar. Eğer ABI coder v2’yi pragma abicoder v2; şeklinde kodunuza eklerseniz daha fazla tip kullanılabilir olacaktır, ancak mapping tipi hâlâ bir akıllı sözleşme içerisinde sınırlıdır ve onları transfer edemezsiniz.

Çoklu Değer Return Etme

Bir fonksiyonda birden fazla değişkeni return etmek istiyorsanız return (v0, v1, ..., vn) şeklinde bir ifade kullanabilirsiniz. Return değişkeni sayısı ve tipleri, bir implicit dönüşümden sonra belirtilen değerlerle eşleşmelidir.

State Değişkenliği

View Fonksiyonlar

view ile tanımlanan fonksiyonlar state’te herhangi bir değişikliği yapamaz, sadece state’deki değerleri okuyabilirler.

Not

Eğer derleyicinin EVM target kısmı Byzantium veya daha yenisi (default) ise view fonksiyonlar çağrıldığında STATICCALL opcode’u kullanılır ve bu opcode state’i değişmemeye zorlar. Kütüphanelerdeki view fonksiyonlarında ise DELEGATECALL kullanılır. Çünkü DELEGATECALL ve STATICCALL opcode’larından kombine edilmiş bir opcode bulunmamaktadır. Bu demek oluyor ki view fonksiyonlar state değişikliğini önlemek için run-time kontrollerine sahip değildirler. Bunun kötü bir güvenlik etkisi olmamalıdır. Çünkü kütüphane kodu genellikle derlenirken bilinir ve statik kontrol edici (static checker) compile-time kontrollerini gerçekleştirir.

Aşağıdaki ifadeler state değişikliğini temsil eder:

  1. State değişkenlerine yazmak.

  2. Event yayınlama.

  3. Başka akıllı sözleşmeler oluşturma.

  4. selfdestruct kullanmak.

  5. Ether göndermek.

  6. view veya pure olarak belirtilmeyen bir fonksiyon çağırmak.

  7. Low-level çağrılar kullanmak.

  8. Belirli opcode’ları kullanan inline assembly kullanmak.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}

Not

Versiyon 0.5.0 öncesinde fonksiyonlarda constant sözcüğü şu anki view için kullanılırdı, ancak artık kullanılmıyor.

Not

Getter fonksiyonlar otomatik olarak view görünürlüğüne sahip olur.

Not

Versiyon 0.5.0 öncesinde derleyici view için STATICCALL opcode’unu kullanmazdı. Bu, view fonksiyonlarda yanlış explicit tip dönüşümlerini kullanarak state değişikliği yapılmasına izin verdi. STATICCALL opcode’unu view fonksiyonlar için kullanarak EVM seviyesinde state değişikliklerinin yapılmasının önüne geçildi.

Pure Fonksiyonlar

Fonksiyonlar pure olarak tanımlanabilir ve bu şekilde tanımlanan fonksiyonlar state’i okuyamaz ve değişiklik yapamaz. Pure fonksiyonlar içerisinde immutable değişkenler okuyabilir durumdadır.

Not

Eğer derleyicinin EVM target kısmı Byzantium veya daha yeni (default) ise, STATICCALL opcode’u kullanılır. Bu opcode state’in okunmadığına dair garanti vermez ama en azından değiştirilmediğine dair bir garanti verir.

Yukarıda state’i değiştiren ifadeleri açıklamışken, state’i okuduğu düşünülen ifadeleri de aşağıda bulabilirsiniz:

  1. State değişkenlerini okumak.

  2. address(this).balance veya <address>.balance değişkenlerine erişmek.

  3. block, tx veya msg değişkenlerinin herhangi bir üyesine erişmek (msg.sig ve msg.data istisnadır).

  4. pure olmayan herhangi bir fonksiyonu çağırmak.

  5. Belirli opcode’ları kullanan inline assembly kullanmak.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

Pure fonksiyonlar revert() ve require() ifadelerini kullanarak hata oluşması durumunda potansiyel state değişikliğini engelleyebilirler.

State değişikliğini revert etmek bir “state değişikliği” olarak düşünülmez.

Bir state değişikliğini revert etmek bir “state değişikliği” olarak kabul edilmez, çünkü yalnızca daha önce kodda view veya pure kısıtlamaya sahip olmayan state’de yapılan değişiklikler revert edilir ve bu kodun revert’i yakalama ve aktarmama seçeneği vardır.

Bu davranış STATICCALL için de geçerlidir.

Uyarı

EVM seviyesinde fonksiyonların state’den okuma yapmasını engellemek mümkün değildir, sadece yazma engellenebilir (yani, EVM seviyesinde sadece view zorunlu kılınabilir, pure kılınamaz).

Not

Versiyon 0.5.0 öncesinde derleyici pure için STATICCALL opcode’unu kullanmazdı. Bu, pure fonksiyonlarda yanlış explicit tip dönüşümlerini kullanarak state değişikliği yapılmasına izin verdi. STATICCALL opcode’unu pure fonksiyonlar için kullanarak EVM seviyesinde state değişikliklerinin yapılmasının önüne geçildi.

Not

Versiyon 0.4.17 öncesinde derleyici pure fonksiyonların state’i okuması durumunda hata vermezdi. Bu, sözleşme türleri arasında geçersiz açık dönüşümler yaparak atlatılabilen ve bir tür denetim olan derleme zamanı yüzünden kaynaklanmaktaydı. Çünkü derleyici, sözleşme türünün durum değiştirme işlemleri yapmadığını doğrulayabilir, fakat çalışma zamanında çağrılacak olan sözleşmenin gerçekten bu türden olup olmadığını kontrol edemez.

Özel Fonksiyonlar

Receive Ether Fonksiyonu

Bİr akıllı sözleşme sadece bir adet receive fonksiyonuna sahip olabilir. Bu fonksiyon şu şekilde tanımlanır: receive() external payable { ... } (function sözcüğü olmadan). Bu fonksiyon parametre alamaz, hiçbir şey return edemez, görünürlüğü external olmalı ve ayrıca payable olarak tanımlanmalıdır. Bir receive fonksiyonu virtual olabilir, override edilebilir ve modifier’lara sahip olabilir.

Receive fonksiyonu akıllı sözleşmemize gelen boş bir calldata’sı bulunan çağrılarda çalıştırılır. Bu fonksiyon, akıllı sözleşmemize direkt Ether transferi gerçekleştirildiğinde (.send() veya .transfer() kullanılarak) çalıştırılır. Eğer bu fonksiyon tanımlı değil ama payable bir fallback fonksiyon tanımlı ise, direkt Ether transferlerinde bu fallback fonksiyonu çalıştırılır. Eğer akıllı sözleşme ne bir receive fonksiyonu, ne de bir payable fallback fonksiyonu tanımlamamışsa, akıllı sözleşmemiz direkt Ether transflerlerini kabul edemez, kendisine ether gönderildiğinde bir hata verir.

En kötü durumda receive fonksiyonu 2300 adet gazın mevcut olduğunu varsayabilir (örneğin send veya transfer kullanımında), geriye ise sadece log işlemleri gibi basit işlemler için gaz kalır. Aşağıdaki işlemler 2300 gazdan daha fazlasını harcar:

  • Storage’e yazmak

  • Akıllı sözleşme oluşturmak

  • Yüksek miktarda gaz harcayan bir external fonksiyonun çağrılması

  • Ether gönderimi

Uyarı

Bir akıllı sözleşmede direkt olarak Ether gönderirken (bir fonksiyon çağrısı olmadan, yani gönderenin send veya transfer kullandığı durumda) eğer akıllı sözleşme bir receive fonksiyonu veya bir payable fallback fonksiyonu tanımlamamışsa, bir hata oluşur ve Etherler gönderene iade edilir (bu durum Solidity 0.4.0 öncesinde farklıydı). Eğer akıllı sözleşmenizin direkt Ether transferlerini kabul etmesini istiyorsanız, bir receive fonksiyonu tanımlayın (Ether kabulu için payable fallback fonksiyonunun kullanımını tavsiye etmiyoruz, çünkü fallback fonksiyonu interface karışıklığı yaşandığında kullanıcıya hata vermeyecektir).

Uyarı

Bir akıllı sözleşme receive fonksiyonu olmadan da Ether kabul edebilir; coinbase transaction (diğer adıyla miner block reward) veya selfdestruct kullanılırken hedef adres olarak verilmesi halinde akıllı sözleşme Etherleri kabul etmek zorundadır.

Bir akıllı sözleşme bu gibi durumlardaki Ether transferlerine herhangi bir tepki veremez ve dolayısıyla bunları reddedemez. Bu EVM’in tasarım tercihlerinden biridir ve Solidity bunu es geçemez.

Bu ayrıca demek oluyor ki address(this).balance değişkenindeki değer sizin kendi hesaplamanızla (örneğin, receive fonksiyonunda her gelen miktarı hesaplamanız halinde) farklı olabilir.

Aşağıdaki Sink akıllı sözleşmesi receive kullanımına bir örnektir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// Bu akıllı sözleşmeye gönderilen Etherleri geri almanın hiçbir
// yolu yoktur.
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

Fallback Fonksiyonu

Bir akıllı sözleşme sadece bir adet fallback fonksiyonuna sahip olabilir. Bu fonksiyon şu iki şekilde tanımlanabilir: fallback () external [payable] veya fallback (bytes calldata input) external [payable] returns (bytes memory output) (ikisi de function sözcüğü olmadan kullanılıyor). Bu fonksiyon external görünürlüğe sahip olmalıdır. Bir fallback fonksiyonu virtual olabilir, override edilebilir ve modifier’lara sahip olabilir.

Fallback fonksiyonu bir çağrıda gönderilen fonksiyon imzasının (function signature) akıllı sözleşmedeki herhangi bir fonksiyon ile eşleşmediği durumda çalıştırılır, yani, eğer kullanıcının çalıştırmak istediği fonksiyon akıllı sözleşmede yoksa, fallback fonksiyonu çalıştırılır. Bir diğer kullanım alanı ise direkt Ether gönderimlerinde eğer akıllı sözleşmede receive Ether fonksiyonu yoksa ve fallback fonksiyonumuz payable ise, fallback fonksiyonu çalıştırılır.

Eğer yukarıda gösterdiğimiz iki kullanım şeklinden input kullanılanı kullanmak isterseniz, input akıllı sözleşmeye gönderilen tüm data, msg.data, olacaktır. Ayrıca output ile de data return edebilir. Return edilen data ABI-encoded olmayacaktır, onun yerine herhangi bir düzenleme olmadan (hatta padding bile olmadan) return edilecektir.

En kötü durumda, eğer bir payable fallback fonksiyonu receive fonksiyonun da yerine kullanıldıysa, sadece 2300 adet gaz ile işlemini tamamlayabilir (receive Ether fonksiyonu).

Diğer herhangi bir fonksiyon gibi fallback fonksiyonu da yeterli gaza sahip olduğu sürece çok karmaşık işlemleri yürütebilir.

Uyarı

Bir payable fallback fonksiyonu ayrıca direkt Ether transferlerinde de, eğer receive Ether fonksiyonu kullanılmadıysa, çalıştırılabilir. Eğer payable fallback fonksiyonuna spesifik bir kullanım için ihtiyacınız yoksa, receive fonksiyonunu kullanmanızı tavsiye ederiz.

Not

Eğer input verisini decode etmek istiyorsanız, ilk dört byte’ı fonksiyon imzası için kullanabilir ve kalan kısmı abi.decode kullanarak ABI-encoded veriyi decode edebilirsiniz: (c, d) = abi.decode(input[4:], (uint256, uint256)); Şunu unutmayın ki, bu bir son çaredir. Eğer yapabiliyorsanız daha uygun bir fonksiyon kullanmaya çalışın.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract Test {
    uint x;
    // Bu akıllı sözleşmeye gelen bütün mesaj çağrılarını
    // bu fonksiyon karşılar (akıllı sözleşmede başka bir
    // fonksiyon bulunmadığı için).
    // Fonksiyon payable olarak belirtilmediği için
    // Ether gönderimlerinde hata alınacaktır.
    fallback() external { x = 1; }
}

contract TestPayable {
    uint x;
    uint y;
    // Bu akıllı sözleşmeye gelen direkt Ether gönderimleri dışındaki bütün mesajları
    // bu fonksiyon karşılayacaktır (receive dışında başka bir fonksiyon
    // bulunmamakta). Calldatası boş olmayan bütün çağrıları bu fonksiyon
    // karşılar (çağrı ile birlikte Ether gönderilse bile).
    fallback() external payable { x = 1; y = msg.value; }

    // Bu fonksiyon sadece direkt Ether gönderimleri için kullanılır, yani,
    // boş bir calldata ve Ether gönderilen çağrıları bu fonksiyon karşılar.
    receive() external payable { x = 2; y = msg.value; }
}

contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.x'in == 1 olmasına neden olur.

        // address(test) direkt olarak ``send`` kullanımına izin vermez.
        // ``send`` fonksiyonunu çağırabilmek için bile ``address payable``
        // tipine dönüştürme gerekmektedir.
        address payable testPayable = payable(address(test));

        // Eğer biri burada da olduğu gibi payable fallback fonksiyonu olmayan bir
        // akıllı sözleşmeye ether göndermeye çalışırsa, hata alacaktır.
        // Dolayısıyla burada ``false`` return edilir.
        return testPayable.send(2 ether);
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.x == 1 olur ve test.y 0 olur.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // test.x == 1 olur ve test.y 1 olur.

        // Eğer biri aşağıdaki gibi TestPayable akıllı sözleşmesine Ether gönderirse, receive fonksiyonu çalışır.
        // Yukarıda tanımladığımız receive fonksiyonu storage'e yazdığı için 2300'den daha fazla
        // gaz harcanmasına sebep olur. O yüzden ``send`` ve ``transfer`` kullanılamaz.
        // Onların yerine low-level call kullanmalıyız.
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // test.x'in == 2 ve test.y'nin 2 Ether olmasıyla sonuçlanır.

        return true;
    }
}

Fonksiyon Overloading

Bir akıllı sözleşme aynı isimde fakat farklı parametre tiplerine sahip fonksiyonlara sahip olabilir. Bu işlem “overloading” olarak adlandırılır ve ayrıca türetilen fonksiyonlar için de geçerlidir. Aşağıdaki örnek A akıllı sözleşmesindeki f fonksiyonları ile overloading’i gösterir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint value) public pure returns (uint out) {
        out = value;
    }

    function f(uint value, bool really) public pure returns (uint out) {
        if (really)
            out = value;
    }
}

Overload edilmiş fonksiyonlar external interface’de de göründüğü için iki fonksiyonun aldığı parametreler external tiplerine göre karşılaştırılır. Yani, örneğin aşağıdaki fonksiyonlardan biri parametre olarak akıllı sözleşme aldığını belirtmiş. Ancak external interface’de bu, bir akıllı sözleşme değil, adres olarak görünür. O yüzden bu akıllı sözleşme compile edilemez.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

// Compile edilemez
contract A {
    function f(B value) public pure returns (B out) {
        out = value;
    }

    function f(address value) public pure returns (address out) {
        out = value;
    }
}

contract B {
}

Yukarıdaki iki f fonksiyonu da ABI’leri aracılığı ile address tipinden bir parametre kabul ediyor, her ne kadar Solidity içerisinde farklı tipler kabul etseler de.

Overload Ayrıştırma ve Parametre Eşleştirme

Overload edilmiş fonksiyonlar, geçerli kapsamdaki fonksiyon tanımlamalarını fonksiyon çağrısında sağlanan parametrelerle eşleştirerek seçilir. Tüm parametreler implicit olarak beklenen türlere dönüştürülebiliyorsa, fonksiyon overload adayı olarak seçilir. Tam olarak bir aday yoksa, çözümleme başarısız olur.

Not

Overload ayrıştırma için return parametreleri dikkate alınmaz.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint8 val) public pure returns (uint8 out) {
        out = val;
    }

    function f(uint256 val) public pure returns (uint256 out) {
        out = val;
    }
}

f(50) çağrısını yaptığımızda bir hata alırız. Bunun sebebi 50 sayısının hem uint8 hem de uint256 tipinde de kullanılabilmesidir. Ama eğer f(256) çağrısını gerçekleştirirsek 256 sayısı direkt olarak f(uint256) bu şekilde tanımlanan fonksiyona gönderilir. Çünkü 256 uint8 olarak gösterilemez.

Eventler

Solidity eventleri EVM’nin loglama işlevinin üzerine bir soyutlama verir. Uygulamalar Ethereum clientlarının RPC arayüzüne abone olarak bu eventleri dinleyebilirler.

Eventler akıllı sözleşmelerin türetilebilen üyeleridir. Çağrıldıklarında işlemlerin log kısmında - blok zincirindeki özel bir veri yapısı - depolanırlar. Bu eventler çağrıldıkları akıllı sözleşmenin adresi ile özdeşleştirilir ve işlemin bulunduğu blok erişilebilir olduğu sürece bu eventlere de erişilebilir (şu anda bu süre sonsuza kadardır ancak Serenity ile bu değişebilir). Log ve event verisi akıllı sözleşme tarafından erişilebilir değildir (eventi oluşturan akıllı sözleşme için bile bu geçerlidir).

Loglar için bir Merkle proof talep etmek mümkündür, bu nedenle external bir varlık böyle bir kanıtla bir akıllı sözleşme sağlarsa, logun blok zinciri içinde gerçekten var olup olmadığını kontrol edebilir. Sözleşme yalnızca son 256 blok hashini görebildiği için blok başlıkları sağlamanız gerekir.

Logun veri kısmı yerine “topics” olarak bilinen özel bir veri yapısına ekleyen en fazla üç parametreye indexed özniteliği ekleyebilirsiniz. Bir topic yalnızca tek bir kelimeyi (32 byte) tutabilir, bu nedenle indekslenmiş bir argüman için bir referans tipi kullanırsanız, bunun yerine değerin Keccak-256 hashi topic olarak saklanır.

indexed olmadan kullanılan bütün parametreler logun veri kısmına ABI-encoded olarak saklanır.

Topicler eventleri aramanıza izin verir, örneğin belirli eventler için bir blok dizisini filtrelerken. Ayrıca eventleri yayınlandıkları akıllı sözleşmede göre de filtreleyebilirsiniz.

Örneğin aşağıdaki kod web3.js’in subscribe("logs") methodunu kullanarak logları belirli bir adrese göre filtreleme işlemi yapmıştır:

var options = {
    fromBlock: 0,
    address: web3.eth.defaultAccount,
    topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
    if (!error)
        console.log(result);
})
    .on("data", function (log) {
        console.log(log);
    })
    .on("changed", function (log) {
});

Eventin imzasının hashi, etkinliği anonim belirteçle bildirmeniz dışında, topiclerden biridir. Bu, belirli anonim eventleri ada göre filtrelemenin mümkün olmadığı, yalnızca akıllı sözleşme adresine göre filtreleyebileceğiniz anlamına gelir. Anonim eventlerin avantajı, deploy etmenin ve çağırmanın daha ucuz olmasıdır. Ayrıca, üç yerine dört indexed değişken bildirmenize olanak tanır.

Not

İşlem logları değişken türünü değil, yalnızca olay verilerini sakladığından, verileri doğru bir şekilde yorumlamak için hangi parametrenin dizine eklendiği ve eventin anonim olup olmadığı dahil olmak üzere olayın türünü bilmeniz gerekir. Özellikle, anonim bir event kullanarak başka bir eventin imzasını “sahte” yapmak mümkündür.

Eventlerin Üyeleri

  • event.selector: Anonim olmayan eventlerde bytes32 tipindeki bir değerdir ve eventin imzasının hashini içerir keccak256.

Örnek

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;

contract ClientReceipt {
    event Deposit(
        address indexed from,
        bytes32 indexed id,
        uint value
    );

    function deposit(bytes32 id) public payable {
        // Eventler `emit` sözcüğü ve sonrasında
        // eventin ismi ve parametreleri (varsa) parantez
        // içerisine konularak yayınlanır.
        // Bu şekildeki herhangi bir çağırma işlemi
        // (iç içe olsa bile) `Deposit` ile filtreleme
        // yaparak JavaScript API tarafından yakalanabilir.
        emit Deposit(msg.sender, id, msg.value);
    }
}

JavaScript API kullanımı ise şu şekildedir:

var abi = /* derleyici tarafından üretilen ABI */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* adres */);

var depositEvent = clientReceipt.Deposit();

// değişiklikleri izle
depositEvent.watch(function(error, result){
    // sonuç, `Deposit` çağrısına verilen indekslenmemiş
    // argümanları ve topicleri içerir.
    if (!error)
        console.log(result);
});


// veya bir callback fonksiyonu ile direkt olarak dinlemeye başlayabilirsiniz
var depositEvent = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

Yukarıdaki kod şu şekilde bir çıktı verir (trim edilmiş hali ile):

{
   "returnValues": {
       "from": "0x1111…FFFFCCCC",
       "id": "0x50…sd5adb20",
       "value": "0x420042"
   },
   "raw": {
       "data": "0x7f…91385",
       "topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
   }
}

Eventleri Anlamak İçin Ekstra Kaynaklar

Hata ve Geri Alma Durumları

Solidity’de hatalar gaz-verimli ve kullanışlı bir şekilde kullanıcılara bir işlemin neden başarısız olduğunu söylemeyi sağlar. Akıllı sözleşmenin içerisinde veya dışarısında tanımlanabilirler (interface ve kütüphaneler de dahil).

Revert ifadesi ile kullanılmalıdır. Bu ifade anlık çağrıda yapılan bütün değişiklikleri geri alır ve işlemi çağıran kişiye bir hata gönderir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Transfer için yetersiz bakiye. `required` kadar bakiye
/// olmalıyken, `available` kadar bakiye mevcuttur.
/// @param available, kullanılabilir bakiye.
/// @param required, transfer edilmek istenen miktar.
error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    mapping(address => uint) balance;
    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });
        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
    // ...
}

Hatalar overload veya override edilemez ama türetilebilirler. Alanları farklı olduğu sürece aynı hata birden fazla kere tanımlanabilir. Hata örnekleri sadece revert ifadesi kullanılarak üretilebilir.

Hata, daha sonra zincir dışı bileşene geri dönmek veya onu try/catch ifadesiyle. yakalamak için geri alma işlemiyle işlemi çağırana veri iletir. Bir hatanın yalnızca harici bir aramadan geldiğinde yakalanabileceğini, dahili aramalarda veya aynı işlevin içinde gerçekleşen geri dönüşlerin yakalanamayacağını unutmayın.

Herhangi bir parametre sağlamazsanız, hata yalnızca dört bayt veriye ihtiyaç duyar ve zincirde depolanmayan hatanın ardındaki nedenleri daha fazla açıklamak için NatSpec’i yukarıdaki gibi kullanabilirsiniz. Bu, bunu aynı zamanda çok ucuz ve kullanışlı bir hata raporlama özelliği yapar.

Daha spesifik olarak, bir hata örneği, aynı ad ve türdeki bir işleve yapılan bir işlev çağrısıyla aynı şekilde ABI ile kodlanır ve daha sonra geri alma işlem kodunda dönüş verileri olarak kullanılır. Bu, verilerin 4 baytlık bir fonksiyon selector’ünün ve ardından ABI-encoded verilerden oluştuğu anlamına gelir. Selector, hata türünün imzasının keccak256 hash’inin ilk dört baytından oluşur.

Not

Bir sözleşmenin aynı adı taşıyan farklı hatalarla veya hatta işlemi çağıran tarafından ayırt edilemeyen farklı yerlerde tanımlanan hatalarla geri dönmesi mümkündür. Dışarıdan, yani ABI için, tanımlandığı sözleşme veya dosya değil, yalnızca hatanın adı önemlidir.

require(condition, "description"); ifadesi ile if (!condition) revert Error("description") ifadesi eğer hata error Error(string) bu şekilde tanımlanmışsa, aynı işi yapar. Error tipinin bir built-in tipi olduğunu ve kullanıcı tarafından tanımlanamayacağını unutmayın.

Benzer olarak bir assert ile tespit edilen bir başarısızlık, yine bir built-in tipi olan Panic(uint256) ile geri alınacaktır.

Not

Hata verileri sadece bir başarısızlığı işaret etmek için kullanılmalıdır, kontrol akışı için kullanılmamalıdır. Bunun nedeni, dahili çağrıların geri alınan verilerinin, varsayılan olarak harici çağrılar zinciri boyunca geri yayılmasıdır. Bu, bir iç çağrının, kendisini çağıran sözleşmeden gelmiş gibi görünen verileri “sahte” hale getirebileceği anlamına gelir.

Hataların Üyeleri

  • error.selector: Hatanın selector’ünü içeren bytes4 dört baytlık bir değer.

Kalıtım

Solidity polimorfizm dahil birçok kalıtım yöntemini destekler.

Polimorfizm, bir fonksiyon çağrısının (dahili ve harici), kalıtım hiyerarşisinde aynı fonksiyona sahip birden fazla akıllı sözleşmenin olması durumunda, ilk türetilen akıllı sözleşmenin fonksiyonunun çalıştırılmasına verilen isimdir. Bu, virtual ve override anahtar sözcükleri kullanılarak hiyerarşideki her işlevde açıkça etkinleştirilmelidir. Daha fazla ayrıntı için Function Overriding’e bakın.

Kalıtım hiyerarşisinden bir fonksiyonu çağırmak isterseniz; ContractName.functionName() bu şekilde çağırabilirsiniz. Veya kalıtım hiyerarşisinde bir üst akıllı sözleşmede bulunan bir fonksiyonu çağırmak isterseniz de; super.functionName() kullanabilirsiniz.

Bir akıllı sözleşme başka bir akıllı sözleşmeyi türettiğinde, blockchainde sadece bir adet akıllı sözleşme oluşturulur ve tüm ana akıllı sözleşmelerden gelen kodlar oluşturulan akıllı sözleşmeye eklenir. Bu demek oluyorki ana akıllı sözleşmelerin fonksiyonlarına yapılan bütün internal çağrılar sadece internal fonksiyon çağrılarını kullanırlar (super.f(..) sadece JUMP opcode’unu kullanacaktır, mesaj çağrısı yapmayacaktır).

Durum değişkeni gölgeleme bir hata olarak kabul edilir. Bir türetilen akıllı sözleşme sadece ve sadece eğer türettiği akıllı sözleşmelerden hiçbiri x isminde bir değişkeni kullanmıyorsa bu isimde bir değişken tanımlayabilir.

Genel kalıtım sistemi Python’a oldukça benzer, özellikle de çoklu kalıtım konusunda, fakat ayrıca bazı farklılıklar da bulunmaktadır

Aşağıdaki örnekte detaylar açıklanmıştır.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;


contract Owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}


// `is` kullanarak başka bir akıllı sözleşmeyi türetebiliriz.
// Türetilen akıllı sözleşmeler private olmayan bütün üyelere
// erişebilir, internal fonksiyonlar ve durum değişkenleri
// dahil. Bunlara external olarak `this` kullanılarak da erişilemez.
contract Destructible is Owned {
    // `virtual` sözcüğü bu fonksiyonun, türetilen
    // akıllı sözleşmelerde değiştirilebileceğini belirtir ("overriding").
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}


// Abstract akıllı sözleşmeler sadece derleyiciye interface'i
// bildirmek için kullanılır. Fonksiyonun kodlarının olmadığına
// dikkat edin. Eğer bir akıllı sözleşme bütün fonksiyonlarının içeriğini
// bulundurmazsa, sadece interface olarak da kullanılabilir.
abstract contract Config {
    function lookup(uint id) public virtual returns (address adr);
}


abstract contract NameReg {
    function register(bytes32 name) public virtual;
    function unregister() public virtual;
}


// Çoklu türetim de mümkündür. `Owned` akıllı sözleşmesinin
// ayrıca `Destructible` akıllı sözleşmesinin ana akıllı sözleşmelerinden
// biri olduğunu unutmayın. Ancak `Owned` akıllı sözleşmesinin
// sadece bir adet örneği vardır (C++'daki sanal kalıtım gibi).
contract Named is Owned, Destructible {
    constructor(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // Fonksiyonlar başka bir fonksiyon tarafından aynı isim ve aynı
    // sayıda/tipte girdi ile override edilebilir. Eğer override eden
    // fonksiyon farklı sayıda çıktı veriyorsa, bu ortaya bir hata çıkarır.
    // Hem yerel hem de mesaj-tabanlı fonksiyon çağrıları bu override işlemlerini
    // hesaba katar. Eğer bir fonksiyonu override etmek istiyorsanız
    // `override` sözcüğünü kullanmak zorundasınız. Ayrıca fonksiyonunuzun
    // tekrardan override edilebilir olmasını istiyorsanız, tekrardan
    // `virtual` olarak belirlemelisiniz.
    function destroy() public virtual override {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // Override edilmiş bir fonksiyonu spesifik olarak
            // çağırmak mümkündür.
            Destructible.destroy();
        }
    }
}


// Eğer bir constructor parametre alıyorsa, bu
// başlıkta veya değiştirici-çağırma-stili ile
// türetilen akıllı sözleşmesinin constructor'ında
// verilmelidir (aşağıya bakın).
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
    function updateInfo(uint newInfo) public {
        if (msg.sender == owner) info = newInfo;
    }

    // Burada sadece `override` yazıyoruz, `virtual` yazmıyoruz.
    // Bu, `PriceFeed` akıllı sözleşmesinden türetilen akıllı sözleşmelerin
    // artık `destroy` fonksiyonunu override edemeyecekleri anlamına geliyor.
    function destroy() public override(Destructible, Named) { Named.destroy(); }
    function get() public view returns(uint r) { return info; }

    uint info;
}

Yukarıdaki Destructible.destroy() fonksiyon çağrımızın bazı problemlere yol açtığını aşağıdaki örnekte görebilirsiniz.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}

contract Destructible is owned {
    function destroy() public virtual {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ Destructible.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 2 */ Destructible.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { Base2.destroy(); }
}

Final.destroy() çağrısı Base2.destroy fonksiyonunu çağıracak. Çünkü yaptığımız son override’da böyle belirtti. Ancak bu fonksiyon Base1.destroy fonksiyonunu bypass eder.

A call to Final.destroy() will call Base2.destroy because we specify it explicitly in the final override, but this function will bypass Base1.destroy. Bunu aşmanın yolu super kelimesini kullanmaktır:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}

contract Destructible is owned {
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}


contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { super.destroy(); }
}

Base2 , super işlevini çağırırsa, bu işlevi temel sözleşmelerinden birinde çağırmaz. Bunun yerine, son kalıtım grafiğindeki bir sonraki temel sözleşmede bu işlevi çağırır, bu nedenle Base1.destroy() u çağırır (son kalıtım dizisinin – en türetilmiş sözleşmeyle başlayarak şöyle olduğuna dikkat edin: Final, Base2, Base1, Destructible, owned). super kullanılırken çağrılan asıl işlev, türü bilinmesine rağmen kullanıldığı sınıf bağlamında bilinmemektedir. Bu, sıradan sanal yöntem araması için benzerdir.

Fonksiyon Override Etme

Temel fonksiyonlar virtual olarak işaretlenmişse, davranışlarını değiştirmek için override edilebilirler. Override eden fonksiyon override olarak belirlenmelidir. Override edilen fonksiyonun görünürlüğü external’dan public’e dönüştürülebilir. Değişebilirlik ise daha fazla kısıtlandırılmış bir yapıya dönüştürülebilir: nonpayable, view ve pure tarafından override edilebilir. view ise pure tarafından override edilebilir. payable bir istisna olarak diğer değişebilirlik türlerine dönüştürülemez.

Aşağıdaki örnek değişebilirliği ve görünürlüğü değiştirmeyi açıklıyor:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base
{
    function foo() virtual external view {}
}

contract Middle is Base {}

contract Inherited is Middle
{
    function foo() override public pure {}
}

Çoklu kalıtım için, aynı işlevi tanımlayan en çok türetilmiş temel sözleşmeler, override anahtar sözcüğünden sonra açıkça belirtilmelidir. Başka bir deyişle, aynı işlevi tanımlayan ve henüz başka bir temel sözleşme tarafından geçersiz kılınmamış tüm temel sözleşmeleri belirtmeniz gerekir (miras grafiği boyunca bir yolda). Ek olarak, bir sözleşme aynı işlevi birden çok (ilgisiz) temelden devralırsa, bunu açıkça geçersiz kılması gerekir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base1
{
    function foo() virtual public {}
}

contract Base2
{
    function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
    // foo() fonksiyonuna sahip birden fazla temel akıllı sözleşmesi türetir.
    // Bu yüzden override etmek için açıkça belirtmeliyiz.
    function foo() public override(Base1, Base2) {}
}

Fonksiyon, ortak bir temel sözleşmede tanımlanmışsa veya ortak bir temel sözleşmede diğer tüm işlevleri zaten override eden benzersiz bir işlev varsa, açık bir override belirteci gerekli değildir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// Açıkça override gerekmemektedir.
contract D is B, C {}

Daha resmi olarak, imza için tüm override etme yollarının parçası olan bir temel sözleşme varsa, birden çok tabandan devralınan bir fonksiyonu (doğrudan veya dolaylı olarak) override etme gerekli değildir ve (1) bu taban fonksiyonu uygular ve mevcut akıllı sözleşmeden tabana giden hiçbir yol bu imzaya sahip bir fonksiyondan bahsetmez veya (2) bu taban fonksiyonu yerine getirmiyor ve mevcut akıllı sözleşmeden o tabana kadar olan tüm yollarda fonksiyondan en fazla bir kez söz ediliyor.

Bu anlamda, bir imza için override etme yolu, söz konusu akıllı sözleşmede başlayan ve override etmeyen bu imzaya sahip bir işlevden bahseden bir akıllı sözleşmede sona eren miras grafiği boyunca bir yoldur.

Override eden bir fonksiyonu virtual olarak işaretlemezseniz, türetilmiş sözleşmeler artık bu fonksiyonun davranışını değiştiremez.

Not

private görünürlüğe sahip fonksiyonlar virtual olamaz.

Not

Interface dışında olup da kodu olmayan fonksiyonlar virtual olarak işaretlenmelidir. Interface içerisindeki bütün fonksiyonlar otomatikmen virtual olarak düşünülür.

Not

Solidity 0.8.8 itibari ile bir interface fonksiyonunu override ederken override sözcüğünü kullanmanıza gerek kalmıyor, birden fazla temel akıllı sözleşmede tanımlanan fonksiyonlar dışında.

Public durum değişkenleri parametre ve dönüş tipleri uyuştuğu zaman bir external fonksiyonu override edebilir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract A
{
    function f() external view virtual returns(uint) { return 5; }
}

contract B is A
{
    uint public override f;
}

Not

Public durum değişkenleri external fonksiyonları override edebilirken, kendileri override edilemez.

Modifier Override Etme

Fonksiyon modifier’ları birbirlerini override edebilirler. Bu aynı fonksiyon override etmedeki gibidir (modifierlarda overload etme olmamakla istisnası ile). virtual sözcüğü override edilecek modifier’da kullanılmalı ve override eden modifier’da ise override sözcüğü kullanılmalıdır.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base
{
    modifier foo() virtual {_;}
}

contract Inherited is Base
{
    modifier foo() override {_;}
}

Çoklu kalıtım durumumnda bütün temel akıllı sözleşmeler açıkça override edilme durumunu belirtmelidir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base1
{
    modifier foo() virtual {_;}
}

contract Base2
{
    modifier foo() virtual {_;}
}

contract Inherited is Base1, Base2
{
    modifier foo() override(Base1, Base2) {_;}
}

Constructor’lar

Constructor isteğe bağlı olarak tanımlanan özel fonksiyonlardan biridir ve constructor sözcüğü ile tanımlanır. Bu fonksiyon akıllı sözleşme oluşumu sırasında çalıştırılır ve akıllı sözleşme başlatma kodunuz burada bulunmaktadır.

Constructor kodu çalıştırılmadan önce durum değişkenleri eğer aynı satırda tanımladıysanız gerekli değer atamalarını veya tanımlamadıysanız default değerlerini alırlar.

Constructor çalıştırıldıktan sonra kodun son hali blockchain’e yüklenir. Bu işlemin ücreti ise lineer bir şekilde olup kodun uzunluğuna bağımlıdır. Bu kod dışarıdan erişilebilecek ve bir fonksiyon tarafından erişilen bütün fonksiyonları içerir. Constructor kodunu veya sadece constructor tarafından erişilen internal fonksiyonları içermez.

Eğer constructor yoksa, default constructor çalıştırılır constructor() {}. Örneğin:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

abstract contract A {
    uint public a;

    constructor(uint a_) {
        a = a_;
    }
}

contract B is A(1) {
    constructor() {}
}

Constructor’larda internal parametreleri kullanabilirsiniz (örneğin, storage pointer’ları). Bu durumda akıllı sözleşme abstract olarak işaretlenmelidir. Çünkü bu parametrelere dışarıdan geçerli değerler atanamaz, ancak yalnızca türetilmiş sözleşmelerin constructor’ları aracılığıyla atanır.

Uyarı

Versiyon 0.4.22 öncesinde constructor’lar akıllı sözleşme ile aynı isme sahip fonksiyonlar olarak kullanılırdı. Ancak bu yazılış biçiminin Versiyon 0.5.0 sonrasında kullanımına izin verilmemektedir.

Uyarı

Versiyon 0.7.0 öncesinde constructor’ların görünürlüğünü internal veya public olarak belirtmek zorundaydınız.

Temel Constructor’lar için Argümanlar

Tüm temel akıllı sözleşmelerin constructor’ları, aşağıda açıklanan doğrusallaştırma kurallarına göre çağrılacaktır. Temel akıllı sözleşmelerin argümanları varsa, türetilmiş akıllı sözleşmelerin hepsini belirtmesi gerekir. Bu iki şekilde yapılabilir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base {
    uint x;
    constructor(uint x_) { x = x_; }
}

// Direkt kalıtım listesinde belirtme...
contract Derived1 is Base(7) {
    constructor() {}
}

// veya "modifier" stilinde belirtme...
contract Derived2 is Base {
    constructor(uint y) Base(y * y) {}
}

// veya abstract olarak belirtin...
abstract contract Derived3 is Base {
}

// ve bir sonraki contractın onu başlatmasını sağlayın.
contract DerivedFromDerived is Derived3 {
    constructor() Base(10 + 10) {}
}

Bir yol doğrudan kalıtım listesindedir (is Base(7)). Diğeri, türetilmiş constructor’ın bir parçası olarak bir modifier’ın çağrılma biçimindedir (Base(y * y)). Bunu yapmanın ilk yolu, constructor argümanının sabit olması ve akıllı sözleşmenin davranışını tanımlaması veya tanımlaması durumunda daha uygundur. Temel constructor argümanları türetilmiş akıllı sözleşmenin argümanlarına bağlıysa, ikinci yol kullanılmalıdır. Argümanlar ya kalıtım listesinde ya da türetilmiş constructor’da değiştirici-tarzda verilmelidir. Argümanları her iki yerde de belirtmek bir hatadır.

Türetilmiş bir akıllı sözleşme, temel akıllı sözleşmelerin tüm constructorları için argümanları belirtmiyorsa, özet olarak bildirilmelidir. Bu durumda, ondan başka bir akıllı sözleşme türetildiğinde, diğer akıllı sözleşmenin miras listesi veya constructor’ı, parametreleri belirtilmemiş tüm temel sınıflar için gerekli parametreleri sağlamalıdır (aksi takdirde, diğer akıllı sözleşme da soyut olarak bildirilmelidir). Örneğin, yukarıdaki kod parçacığında, bkz. Derived3 ve DerivedFromDerived.

Çoklu Kalıtım ve Doğrusallaştırma

Çoklu kalıtıma izin veren diller birkaç problemle uğraşmak zorundadır. Bunlardan bir tanesi Elmas Problemi’dir. Solidity Python’a benzer olarak “C3 Linearization” kullanarak directed acyclic graph’da (DAG) spesifik bir sırayı zorlar. Bu, istenen monotonluk özelliği ile sonuçlanır, ancak bazı kalıtım grafiklerine izin vermez. Özellikle is yönergesinde temel sınıfların veriliş sırası önemlidir: Doğrudan temel sözleşmeleri “en temele benzeyen”den “en çok türetilene” doğru sıralamalısınız. Bu sıralamanın Python’da kullanılanın tersi olduğuna dikkat edin.

Bunu açıklamanın bir başka basitleştirici yolu, farklı akıllı sözleşmelerde birden çok kez tanımlanan bir fonksiyon çağrıldığında, verilen tabanların sağdan sola (Python’da soldan sağa) derinlemesine ilk olarak aranması ve ilk eşleşmede durdurulmasıdır. . Bir temel akıllı sözleşme zaten aranmışsa, atlanır.

Aşağıdaki kodda Solidity “Linearization of inheritance graph impossible” hatası verecektir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
// Bu derlenemez
contract C is A, X {}

Bunun sebebi C akıllı sözleşmesinin X akıllı sözleşmesinin A akıllı sözleşmesini override etmesini istemesidir (A, X sırası ile bunu belirtiyor), ancak A akıllı sözleşmesinin kendisi X akıllı sözleşmesini override etmeyi talep eder ki bu çözülemeyecek bir çelişkidir.

Benzersiz bir override olmadan birden çok tabandan devralınan bir fonksiyonu açıkça override etmek gerektiğinden, pratikte C3 doğrusallaştırması çok önemli değildir.

Kalıtım doğrusallaştırmasının özellikle önemli olduğu ve belki de o kadar net olmadığı bir alan, miras hiyerarşisinde birden çok constructor olduğu zamandır. Constructor’lar, argümanlarının devralınan akıllı sözleşmenin constructor’ında sağlandığı sıraya bakılmaksızın her zaman doğrusallaştırılmış sırada yürütülür. Örneğin:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

// Constructor'lar aşağıdaki sıra ile çalışır:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// Constructor'lar aşağıdaki sıra ile çalışır:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// Constructors are still executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

Farklı Türden Aynı İsme Sahip Üyeleri Türetme

Bir akıllı sözleşmede aşağıdaki çiftlerden herhangi birinin miras nedeniyle aynı ada sahip olması bir hatadır:
  • bir fonksiyon ve bir modifier

  • bir fonksiyon ve bir event

  • bir event ve bir modifier

İstisna olarak, bir durum değişkeninin getirici fonksiyonu bir external fonksiyonu override edebilir.

Abstract Akıllı Sözleşmeler

Sözleşmeler, işlevlerinden en az biri uygulanmadığında veya bütün temel sözleme yapıcılar için argüman sağlamadığında abstract olarak işaretlenmelidir. Bu durumlardan herhangi biri geçerli değilse bile bir akıllı sözleşme abstract olarak işaretlenebilir. Örneğin bir akıllı sözleşmenin direkt olarak oluşturulmasını istemediğiniz durumlarda bunu gerçekleştirebilirsiniz. Abstract akıllı sözleşmeler Interface’ler oldukça benzerdir ancak interface’ler çok daha kısıtlı bir yapıdadır.

Aşağıdaki örnekte belirtildiği gibi, Abstract akıllı sözleşmeler abstract olarak işaretlenerek belirtilir. Aşağıdaki akıllı sözleşmenin abstract olarak tanımlanması gerektiğine dikkat edin. Çünkü utterance() fonksiyonu tanımlanıp kodları yazılmamıştır ({ } arasında kod bulunmamakta).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

abstract contract Feline {
    function utterance() public virtual returns (bytes32);
}

Bu tip abstract akıllı sözleşmeler direkt olarak örneklendirilemez. Bu, abstract sözleşmenin kendisi tanımlanmış tüm işlevleri yerine getiriyorsa da geçerlidir. Abstract bir akıllı sözleşmenin temel sınıf olarak kullanımı aşağıda gösterilmiştir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

abstract contract Feline {
    function utterance() public pure virtual returns (bytes32);
}

contract Cat is Feline {
    function utterance() public pure override returns (bytes32) { return "miaow"; }
}

Bir akıllı sözleşme bir abstract akıllı sözleşmeden türetiliyorsa ve abstract akıllı sözleşmedeki bütün kodu yazılmamış fonksiyonların kodunu yazmıyorsa, o akıllı sözleşme da abstract olarak belirtilmelidir.

Kodu olmayan bir fonksiyonun Fonksiyon Tipinden farklı olduğuna dikkat edin, her ne kadar yazılışları oldukça benzer olsa da.

Kodu olmayan bir fonksiyona örnek olarak (fonksiyon tanımlaması):

function foo(address) external returns (address);

Türü bir fonksiyon türü olan bir değişken bildirimi örneği:

function(address) external returns (address) foo;

Abstract akıllı sözleşmeler, daha iyi genişletilebilirlik ve kendi kendine belgeleme sağlayarak ve Template yöntemi gibi kalıpları kolaylaştırarak ve kod tekrarını ortadan kaldırarak bir akıllı sözleşmenin tanımını uygulamasından ayırır. Abstract akıllı sözleşmeler, bir arabirimdeki yöntemleri tanımlamanın yararlı olduğu şekilde yararlıdır. Abstract akıllı sözleşmenin tasarımcısının “her çocuğum bu yöntemi uygulamalı” demesinin bir yoludur.

Not

Abstract akıllı sözleşmeler kodu yazılmış bir virtual fonksiyonu kodu yazılmamış bir fonksiyon ile override edemezler.

Interface’ler

Interface’ler abstract akıllı sözleşmelere benzerler ama onlardan farklı olarak hiçbir fonksiyonunun kodu yazılamaz. Daha fazla kısıtlama vardır:

  • Diğer akıllı sözleşmelerden miras alamazken, diğer interface’lerden alabilirler.

  • Interface’deki bütün fonksiyonlar external olmalıdır, akıllı sözleşmede public olsalar dahi.

  • Constructor tanımlayamazlar.

  • Durum değişkeni tanımlayamazlar.

  • Modifier tanımlayamazlar.

Bu kısıtlamalardan bazıları ilerleyen zamanlarda kaldırılabilir.

Interface’ler kabaca akıllı sözleşme ABI’sinin temsil edebileciği ile kısıtlıdır. Bu yüzden ABI ve interface arasındaki dönüşümler bilgi kaybı yaşanmadan gerçekleştirilebilmelidir.

Interface’ler kendi anahtar sözcükleri ile tanımlanırlar:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

interface Token {
    enum TokenType { Fungible, NonFungible }
    struct Coin { string obverse; string reverse; }
    function transfer(address recipient, uint amount) external;
}

Akıllı sözleşmeler diğer akıllı sözleşmelerden miras alabildikleri gibi diğer interface’lerden de alabilirler.

Interface’lerdeki bütün fonksiyonlar gizlici virtual olarak işaretlenmiş haldedir ve onları override ederken override kelimesine gerek yoktur. Bu, otomatik olarak override eden bir fonksiyonun yeniden override edilebileceği anlamına gelmez - bu yalnızca override eden fonksiyon virtual olarak işaretlenmişse mümkündür.

Interface’ler diğer interfacelerden miras alabilirler, normal kalıtım kuralında olduğu gibi.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

interface ParentA {
    function test() external returns (uint256);
}

interface ParentB {
    function test() external returns (uint256);
}

interface SubInterface is ParentA, ParentB {
    // Ebeveny anlamlarının uyumlu olduğunu iddia
    // etmek için test yeniden tanımlanmalıdır.
    function test() external override(ParentA, ParentB) returns (uint256);
}

Interface’lerde tanımlanan tiplere ve diğer akıllı sözleşme benzeri yapılara diğer akıllı sözleşmelerden erişilebilir: Token.TokenType veya Token.Coin.

Kütüphaneler

Kütüphaneler akıllı sözleşmelere benzerler, ama onların amacı sadece bir kere deploy edilip daha sonrasında ihtiyaç duyulması halinde DELEGATECALL ile çağrılmalarıdır (Homestead’a kadar CALLCODE kullanılırdı). Bu demek oluyor ki kütüphane fonksiyonları çağrıldığında, onların kodu çağıran akıllı sözleşmenin içeriği ile çalıştırılıyor, mesela this sözcüğü çağıran akıllı sözleşmeyi işaret eder ve özellikle storage olarak çağıran akıllı sözleşmenin storage kısmı kullanılır. Bir kütüphane izole edilmiş bir kaynak kodu parçası olduğundan, yalnızca açıkça sağlanmışlarsa çağrı sözleşmesinin durum değişkenlerine erişebilir (aksi takdirde bunları adlandırmanın hiçbir yolu yoktur). Kütüphane fonksiyonları yalnızca durumu değiştirmedikleri takdirde (yani view veya pure fonksiyonlarsa) doğrudan (yani DELEGATECALL kullanılmadan) çağrılabilir, çünkü kütüphanelerin durumsuz olduğu varsayılır. Özellikle, bir kütüphaneyi yok etmek mümkün değildir.

Not

0.4.20 sürümüne kadar, Solidity’nin tip sistemini atlayarak kütüphaneleri yok etmek mümkündü. Bu sürümden başlayarak, kütüphaneler, durumu değiştiren fonksiyonların doğrudan çağrılmasına izin vermeyen bir mekanizma içerir (yani DELEGATECALL olmadan).

Kütüphaneler, onları kullanan akıllı sözleşmelerin zımni temel akıllı sözleşmeleri olarak görülebilir. Miras hiyerarşisinde açıkça görünmezler, ancak kütüphane fonksiyonlarına yapılan çağrılar, açık temel akıllı sözleşmelerin fonksiyonlarına yapılan çağrılara benzer (L.f() gibi nitelikli erişim kullanarak). Tabii ki, dahili fonksiyonlara yapılan çağrılar dahili çağrı kuralını kullanır; bu, tüm dahili türlerin iletilebileceği ve bellekte depolanan türlerin kopyalanmadan referans olarak iletileceği anlamına gelir. Bunu EVM’de gerçekleştirmek için, bir akıllı sözleşmeden çağrılan dahili kütüphane fonksiyonlarının ve buradan çağrılan tüm fonksiyonların kodu derleme zamanında çağrı akıllı sözleşmesine dahil edilecek ve bir DELEGATECALL yerine normal bir JUMP çağrısı kullanılacaktır.

Not

Public fonksiyonlar söz konusu olduğunda miras analojisi bozulur. L.f() ile bir genel kütüphane fonksiyonunun çağrılması, harici bir çağrıyla sonuçlanır (kesin olarak DELEGATECALL). Buna karşılık, A mevcut akıllı sözleşmesinin temel akıllı sözleşmesi olduğunda, A.f() dahili bir çağrıdır.

Aşağıdaki örnek, kütüphanelerin nasıl kullanılacağını gösterir (ancak manuel bir yöntem kullanarak, bir kümeyi uygulamak için daha gelişmiş bir örnek için kullanmayı kontrol ettiğinizden emin olun).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;


// Çağrı akıllı sözleşmesinde verilerini tutmak
// için kullanılacak yeni bir struct veri türü tanımlıyoruz.
struct Data {
    mapping(uint => bool) flags;
}

library Set {
    // İlk parametrenin "depolama referansı" türünde
    // olduğunu ve bu nedenle çağrının bir parçası
    // olarak içeriğinin değil, yalnızca depolama
    // adresinin iletildiğini unutmayın.
    // Bu, kütüphane fonksiyonlarının özel bir özelliğidir.
    // Fonksiyon, o nesnenin bir yöntemi olarak görülebiliyorsa,
    // ilk parametreyi 'self' olarak adlandırmak deyimseldir.
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // zaten orada
        self.flags[value] = true;
        return true;
    }

    function remove(Data storage self, uint value)
        public
        returns (bool)
    {
        if (!self.flags[value])
            return false; // orada değil
        self.flags[value] = false;
        return true;
    }

    function contains(Data storage self, uint value)
        public
        view
        returns (bool)
    {
        return self.flags[value];
    }
}


contract C {
    Data knownValues;

    function register(uint value) public {
        // "Instance" geçerli akıllı sözleşme olacağından,
        // kütüphane fonksiyonları kütüphanenin belirli
        // bir örneği olmadan çağrılabilir.
        require(Set.insert(knownValues, value));
    }
    // Bu sözleşmede ayrıca direkt olarak knownValues.flags değişkenine de erişebiliriz.
}

Elbette kütüphaneleri kullanmak için bu yolu izlemeniz gerekmez: struct veri türleri tanımlamadan da kullanılabilirler. Fonksiyonlar ayrıca herhangi bir depolama referans parametresi olmadan da çalışırlar ve herhangi bir pozisyonda birden fazla depolama referans parametresine sahip olabilirler.

Set.contains, Set.insert ve Set.remove çağrılarının hepsi harici çağrı olarak derlenir (DELEGATECALL). Eğer kütüphaneleri kullanacaksanız gerçekten bir harici fonksiyon çağrısı yaptığınızı unutmayın. msg.sender, msg.value ve this çağrı boyunca kendi değerlerini koruyacaktır (Homestead öncesi CALLCODE yüzünden msg.sender ve msg.value değişiyordu).

Aşağıdaki örnek, harici fonksiyon çağrılarının ek yükü olmadan özel türleri uygulamak için bellekte depolanan türlerin ve kütüphanelerdeki dahili fonksiyonların nasıl kullanılacağını gösterir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

struct bigint {
    uint[] limbs;
}

library BigInt {
    function fromUint(uint x) internal pure returns (bigint memory r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
        r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint limbA = limb(a, i);
            uint limbB = limb(b, i);
            unchecked {
                r.limbs[i] = limbA + limbB + carry;

                if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
                    carry = 1;
                else
                    carry = 0;
            }
        }
        if (carry > 0) {
            // çok kötü, bir limb eklemeliyiz
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            uint i;
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint memory a, uint index) internal pure returns (uint) {
        return index < a.limbs.length ? a.limbs[index] : 0;
    }

    function max(uint a, uint b) private pure returns (uint) {
        return a > b ? a : b;
    }
}

contract C {
    using BigInt for bigint;

    function f() public pure {
        bigint memory x = BigInt.fromUint(7);
        bigint memory y = BigInt.fromUint(type(uint).max);
        bigint memory z = x.add(y);
        assert(z.limb(1) > 0);
    }
}

Bir kütüphanenin adresini, kütüphane tipini address tipine çevirerek, yani address(LibraryName) kullanarak elde etmek mümkündür.

Derleyici kütüphanenin konuşlandırılacağı adresi bilmediğinden, derlenmiş onaltılık kod __$30bbc0abd4d6364515865950d3e0d10953$__ biçiminde yer tutucular içerecektir. Yer tutucu, tam nitelikli kütüphane adının keccak256 hashinin hex kodlamasının 34 karakterlik bir önekidir; bu, örneğin kütüphane bigint.sol isimli bir dosyada ve libraries/ isimli bir dizinde bulunuyorsa şu şekilde gösterilir libraries/bigint.sol:BigInt. Bu tür bayt kodu eksiktir ve dağıtılmamalıdır. Yer tutucuların gerçek adreslerle değiştirilmesi gerekir. Bunu, kütüphane derlenirken bunları derleyiciye ileterek veya önceden derlenmiş bir ikili dosyayı güncellemek için bağlayıcıyı kullanarak yapabilirsiniz. Bağlama için komut satırı derleyicisinin nasıl kullanılacağı hakkında bilgi için Kütüphane Bağlantıları (Library Linking) konusuna bakın.

Akıllı sözleşmelerle kıyaslandığında, kütüphaneler aşağıdaki şekillerde kısıtlanmışlardır:

  • durum değişkenleri olamaz

  • miras veremezler veya alamazlar

  • Ether kabul edemezler

  • yok edilemezler

(Bunlar ilerleyen zamanlarda kaldırılabilirler.)

Function Signatures and Selectors in Libraries

Public veya external kütüphane fonksiyonlarına harici çağrılar mümkün olsa da, bu tür çağrılar için çağrı kuralının Solidity’nin içinde olduğu ve normal contract ABI için belirtilenle aynı olmadığı kabul edilir. External kütüphane fonksiyonları, örneğin özyinelemeli yapılar ve depolama işaretçileri gibi external kütüphane fonksiyonlarından daha fazla bağımsız değişken türünü destekler. Bu nedenle, 4 baytlık seçiciyi hesaplamak için kullanılan fonksiyon imzaları, bir internal adlandırma şemasının ardından hesaplanır ve ABI akıllı sözleşmesinde desteklenmeyen türdeki bağımsız değişkenler bir dahili kodlama kullanır.

İmzalardaki türler için aşağıdaki tanımlayıcılar kullanılır:

  • Değer tipleri, storage olmayan string ve storage olmayan bytes tipleri akıllı sözleşme ABI’sinde aynı tanımlayıcıları kullanır.

  • Storage olmayan array tipleri de akıllı sözleşme ABI’sindeki genel görüşü kabul eder, yani dinamik arrayler için <type>[] ve fixed-size arrayler için <type>[M] kullanılır.

  • Storage olmayan structlar tam isimleri ile referans edilir, yani contract C { struct S { ... } } için C.S.

  • Storage pointer mappingleri de mapping(<keyType> => <valueType>) storage kullanır. Burada <keyType> ve <valueType> sırasıyla mappingdeki anahtar ve değer tipleridir.

  • Diğer storage pointer tipleri de kendi storage olmayan tiplerinin tanımlayıcılarını kullanırlar, ama bir boşluk ile storage eklenmiş halleri ile.

Argüman encode’lama da sıradan akıllı sözleşme ABI’si gibidir, storage pointerları hariç, işaret ettikleri storage slotuna atıfta bulunan bir uint256 değeri olarak kodlanmıştır.

Akıllı sözleşme ABI’sine benzer bir şekilde, selector, imzanın Keccak256-hashinin ilk dört baytından oluşur. Değeri, .selector üyesi kullanılarak Solidity’den şu şekilde elde edilebilir:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;

library L {
    function f(uint256) external {}
}

contract C {
    function g() public pure returns (bytes4) {
        return L.f.selector;
    }
}

Kütüphaneler İçin Çağrı Koruması

Girişte belirtildiği gibi, bir kütüphanenin kodu DELEGATECALL veya CALLCODE yerine bir CALL kullanılarak yürütülürse, bir view veya pure fonksiyon çağrılmadığı sürece geri dönecektir.

EVM, bir akıllı sözleşmenin CALL kullanılarak çağrılıp çağrılmadığını tespit etmek için doğrudan bir yol sağlamaz, ancak bir sözleşme, “nerede” çalıştığını bulmak için ADDRESS işlem kodunu kullanabilir. Oluşturulan kod, arama modunu belirlemek için bu adresi yapım sırasında kullanılan adresle karşılaştırır.

Daha spesifik olarak, bir kütüphanenin çalışma zamanı kodu her zaman derleme zamanında 20 bayt sıfır olan bir push komutuyla başlar. Dağıtım kodu çalıştığında, bu sabit bellekte geçerli adresle değiştirilir ve bu değiştirilmiş kod sözleşmede saklanır. Çalışma zamanında, bu, dağıtım zamanı adresinin yığına gönderilecek ilk sabit olmasına neden olur ve dağıtıcı kodu, herhangi bir görünüm olmayan ve saf olmayan işlev için geçerli adresi bu sabitle karşılaştırır.

Bu, bir kitaplık için zincirde depolanan gerçek kodun derleyici tarafından bildirilen koddan farklıdır. deployedBytecode.

Using For

using A for B; yönergesi, (A) fonksiyonlarını herhangi bir türe (B) üye fonksiyonlar olarak eklemek için kullanılabilir. Bu fonksiyonlar, çağrıldıkları nesneyi ilk parametreleri olarak alırlar (Python’daki self değişkeni gibi).

Dosya seviyesinde veya bir akıllı sözleşme içerisinde, akıllı sözleşme seviyesinde, geçerlidir.

İlk kısım, A, aşağıdakilerden biri olabilir:

  • dosya seviyesindeki fonksiyonların bir listesi veya kütüphane fonksiyonları (using {f, g, h, L.t} for uint;) - sadece o fonksiyonlar eklenecektir.

  • kütüphanenin adı (using L for uint;) - bütün fonksiyonlar (public ve internallerin hepsi) tipe eklenir.

Dosya seviyesinde, ikinci kısım, B, açık bir tip olmalıdır (veri konumu belirtici olmadan). Akıllı sözleşmenin içerisinde, ayrıca şu ifadeyi de kullanabilirsiniz using L for *;, böylece L kütüphanesinin bütün fonksiyonları bütün tiplere eklenmiş olur.

Bir kütüphane belirtirseniz, kütüphanedeki tüm fonksiyonlar, ilk parametrenin türü nesnenin türüyle eşleşmese bile eklenir. Fonksiyonun çağrıldığı noktada tip kontrol edilir ve fonksiyon aşırı yük çözünürlüğü gerçekleştirilir.

Eğer bir fonksiyon listesi kullanırsanız (using {f, g, h, L.t} for uint;), ardından gelen tip (uint) o kütüphanedeki bütün fonksiyonların ilk parametrelerine gizlice dönüştürülebilir olmalıdır. Bu kontrol, fonksiyonların hiçbiri çağrılmasa bile gerçekleştirilir.

using A for B; direktifi, tüm fonksiyonları dahil olmak üzere yalnızca mevcut kapsamda (sözleşme veya mevcut modül/kaynak birim) etkindir ve kullanıldığı sözleşme veya modül dışında hiçbir etkisi yoktur.

Yönerge dosya düzeyinde kullanıldığında ve aynı dosyada dosya düzeyinde tanımlanmış kullanıcı tanımlı bir türe uygulandığında, sonuna global sözcüğü eklenebilir. Bu, yalnızca using ifadesinin kapsamında değil, türün kullanılabilir olduğu her yerde (diğer dosyalar dahil) işlevlerin türe eklenmesi etkisine sahip olacaktır.

Kütüphaneler bölümünde yazdığımız bir örneği dosya seviyesindeki fonksiyonlarla yeniden yazalım:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

struct Data { mapping(uint => bool) flags; }
// Şimdi örneğe fonksiyonları ekliyoruz.
// Eklenen fonksiyonlar modül boyuna kullanılabilir.
// Eğer modülü başka bir dosyadan eklerseniz
// using yönergesini orada yeniden kullanmalısınız:
//   import "flags.sol" as Flags;
//   using {Flags.insert, Flags.remove, Flags.contains}
//     for Flags.Data;
using {insert, remove, contains} for Data;

function insert(Data storage self, uint value)
    returns (bool)
{
    if (self.flags[value])
        return false; // already there
    self.flags[value] = true;
    return true;
}

function remove(Data storage self, uint value)
    returns (bool)
{
    if (!self.flags[value])
        return false; // not there
    self.flags[value] = false;
    return true;
}

function contains(Data storage self, uint value)
    view
    returns (bool)
{
    return self.flags[value];
}


contract C {
    Data knownValues;

    function register(uint value) public {
        // Burada, Data türündeki tüm değişkenlerin karşılık
        // gelen üye işlevleri vardır. Aşağıdaki işlev çağrısı,
        // `Set.insert(knownValues, value)` ile aynıdır.
        require(knownValues.insert(value));
    }
}

Yerleşik türleri bu şekilde genişletmek de mümkündür. Bu örnekte bir kütüphane kullanacağız.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;

library Search {
    function indexOf(uint[] storage self, uint value)
        public
        view
        returns (uint)
    {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return type(uint).max;
    }
}
using Search for uint[];

contract C {
    uint[] data;

    function append(uint value) public {
        data.push(value);
    }

    function replace(uint from, uint to) public {
        // Bu, kütüphane işlev çağrısını gerçekleştirir
        uint index = data.indexOf(from);
        if (index == type(uint).max)
            data.push(to);
        else
            data[index] = to;
    }
}

Tüm harici kütüphane çağrılarının gerçek EVM fonksiyon çağrıları olduğunu unutmayın. Bu, bellek veya değer türlerini geçerseniz, self değişken durumunda bile bir kopyanın gerçekleştirileceği anlamına gelir. Kopyalama yapılmayacak tek durum, depolama referans değişkenlerinin kullanıldığı veya dahili kütüphane fonksiyonlarının çağrıldığı durumlardır.