Solidity 4 — Contract ve Function

Engin UNAL
10 min readOct 13, 2022

Solidity serisinin bu yazısında temel bileşenlerle devam ediyoruz. Önceki yazıların linkleri: Temel Bilgiler, İlk Akıllı Kontrat ve Remix, Tipler. Bu yazıda ise contract ve function konularına gireceğiz. Öncelikle contract ile başlayalım.

Contract

Adı üstünde akıllı kontrat yapılarının omurgasını oluşturur. Diğer programlama dillerindeki class yapılarına benzer. Blockchain üzerine kaydedilecek verileri taşıyan değişkenler(state variables) kontrat içerisinde tanımlanırlar. Yine aynı şekilde Blockchain üzerinde kaydettiğiniz verilerin değiştirilmesi işlemleri de kontrat içerisindeki fonksiyonlar yardımıyla yapılır. Bunları kodlarken daha net göreceğiz.

Not: Contract’ın Türkçe karşılığı olan kontrat ve smart contract karşılığı olan akıllı kontrat yerine kavramın orjinal halini kullanabilirim. Gözden kaçabiliyor bu nedenle kusura bakmayın.

Bir kontrat kendi kendine çalışmaz mutlaka bir tetikleyicisi olmalıdır. Yani bir kontrat ya bir hesap tarafından veya başka bir kontrat tarafından çağırılabilir.

Constructor

Akıllı kontrat yaratıldığında bir kez çalıştırılan fonksiyonlardır. Mecburi değillerdir, ihtiyaca göre kullanılırlar. Bir kontratın başlangıç state’ini ayarlamak için kullanılır. Bir kere çalıştırılan kod bölümü olduğundan blockchain üzerine kaydedilmez, çalıştıktan sonra yaptığı değişiklikler blockchain’e kaydedilir. Örnek:

contract Cekilis {
address owner;

constructor() {
owner = msg.sender;
}
}

Cekilis ismindeki kontrat içinde constructor() tanımı bulunmakta. Yaptığı işlem ise bu kontratı yükleyen hesap adresini alıp owner olarak set etmektir. Böylece daha sonra bu kontrat sahibine özel yetki gerektiren işlemler yapılmak istendiğinde, owner değişkenine erişip işlem yapmak isteyenin gerçekten bu kontratın sahibi mi kontrol edilebilir.

Değişken Erişim Tanımları (State Variable Visibility)

Akıllı kontrat içindeki değişken tanımlarının görünürlüğü veya erişimini belirlemek için private, internal ve public anahtar kelimeleri kullanılır. Bu kelimeler kullanılmaz ise standart olarak internal kullanılmış gibi çalışmaktadır.

  • private : Erişimi en kısıtlı seviyede uygulayan erişim tanımlayıcısıdır. Dışarıdan erişime izin vermez ve türetilmiş contract’lardan da erişime izin vermemektedir.
  • internal : Sadece contract içinden ve türeyen contract içinden erişime izin verir. Eğer herhangi bir anahtar kelime verilmezse standart olarak bu tanımlayıcı uygulanır.
  • public : Dışarıdan veya türeyen bir kontrat içinden erişime imkan tanır. public kullanılan değişkenler için compiler, otomatik olarak bir getter üretir fakat setter kod bloğu üretmez. Bu demektir ki kontrat dışından(türeyen kontrat veya dış çağrı olabilir) doğrudan değişkenin değerini alabilirsiniz fakat değerini set edemezsiniz. Değer ataması yapmak için değişkenin tanımlı olduğu kontrat içinde setter işi görecek bir fonksiyon yazılmalıdır.

Bir örnek üzerinden gidelim.

Örnekte görüldüğü üzere number isimli değişken public tanımlı yani onun için bir getter fonksiyon yazılmasına gerek yok. Fakat name ve pass internal ve private tanımlı olduğundan değişken değerlerinin dışarıdan okunabilmesi ve isteniliyorsa güncellenebilmesi için getter ve setter fonksiyonlar eklenerek dışarıdan kontrollü erişimi sağlanabilir.

Constant ve Immutable Tanımları

Tanımlanan değişken eğer daha sonradan değiştirilmeyecek ise veya kontrat yaratılırken bir kere set edilecek ve sonrasında değişmesi istenmiyorsa (veya gerekmiyorsa) constant veya immutable olarak tanımlanmaları önerilmektedir. Bu şekildeki tanımlamalar kontrat gas tüketiminin azalmasına ve daha verimli kullanılmasına imkan sağlar.

Constant tanımı, bir değişkenin değerinin derleme anından alması ve fikslenmesi yani daha sonra değiştirilmemek üzere kaydedilmesi anlamına gelir. Immutable ise değişkenin değerini constructor çalıştığı anda alır ve daha sonra değişmemek üzere sabitler. İkisinin farkı, birinin değerinin derleme anında fikslenmesi diğerinde ise constructor çalışma anında değerinin fikslenmesi konusudur. Örnekte görelim.

contract ConstantAndImmutable {
int public constant val1 = 123;
int public immutable val2;
constructor() {
val2 = 456;
}
}

Inheritance (Kalıtım)

Smart kontrat nesneleri miras yoluyla değişken ve fonksiyonların türeyen kontratlara aktarımına imkan tanımaktadır. Ayrıca OOP desteği olan dillerde olan polimorfizm yani aynı isimdeki fonksiyonun türeyen nesnede olan tanımını çalıştırmaya da imkan tanımaktadır. Buna da değinip netleştireceğiz. Öncelikle kontrat nasıl türetilir? Nasıl çalışır? Kod üzerinden inceleyerek görelim.

Bir kontrattan türeyen yeni bir kontrat oluşturmak istiyorsak “is” anahtar kelimesi kullanırız.

Yukarıdaki örnekte Parent isimli kontrat içerisinde state değişkeni ve iki fonksiyon bulunmakta. Parent kontratından türeyen Child isimli yeni bir kontrat oluşturduk ve “is” anahtar kelimesi ile türettik. Böylece Child isimli kontrat, Parent’taki state değişkenleri ve fonksiyonları miras olarak alır. Sanki kendi içinde tanımlılar gibi kullanabilir. Bu şekilde bazı ortak fonksiyon, tanım ve değişkenleri içeren bir kontrat yazarak yeni kontratları bundan türetip daha esnek ve modüler bir yapı kurabiliriz.

Dikkat edilmesi gereken bir nokta ise çalıştığımız ortam blockchain olduğundan türetilen kontrat, blockchain üzerine yayınlanırken bir kontrat olarak yayınlanır. Yani türetilen kontrat blockchain’e kaydedilirken miras aldığı kontrat içindeki fonksiyon ve tanımlarla birlikte derlenip tek bir kontrat olarak yazılır.

Solidity akıllı kontratları çoklu miras(multiple inheritance) desteği vermektedir.

contract Child is Parent, ParentDiger { }

Şimdi aşağıdaki örneği inceleyelim. Burada çoklu miras(multiple inheritance) kullanıldı, bununla birlikte Parent1 ve Parent2 kontratlarında constructor tanımları da mevcut.

Parent1 constructor int tipinde year alır, Parent2 constructor ise string tipinde name alır. Child kontrat türemesi yapılırken bu constructor’lara ilk değerler kod örneğinde olduğu gibi verilebilmektedir. Önce Parent1 constructor çalışır ardından Parent2 constructor çalışacaktır.

Constructor’lara parametre geçilmesi işlemi kontrat yaratılırken de dinamik olarak yapılabilmektedir. Bunun örneği ise aşağıda Child2 ismiyle oluşturulan kontrat örneğinden incelenebilir.

contract Child2 is Parent1, Parent2 {
constructor(int _year, string memory _name) Parent1(_year) Parent2(_name) {
}

Bu örnekte Child2 constructor’a kontrat oluşturulurken verilen _year ve _name parametreleri Parent1 ve Parent2 constructor’lara parametre olarak geçilmektedir.

Function

Akıllı kontrat içinde yapılacak işlemlerin kodlamalarını içeren ve kendi içinde gruplanmış kod bloklarıdır. Erişim seviyesine göre dışarıdan çağırılabilir, değişkenlik tanımlarına göre state üzerinde değişiklikler yapabilir, token alabilir veya transfer işlemleri yapabilirler. Diğer programlama dillerindeki aynı isimli yapılarla benzerdir. Akıllı kontrat konusunun başında değindiğim constructor da özel tip bir fonksiyondur. Özel fonksiyonlara başka örnekler receive, fallback, modifier verilebilir. Konunun devamında bunlara da değinilecektir. Öncelikle erişim seviyesi tanımlarıyla başlayalım.

Fonksiyon Erişim Tanımları (Function Visibility)

  • private : Erişimi en kısıtlı seviyede uygulayan erişim tanımlayıcısıdır. Dışarıdan erişime izin vermez ve türetilmiş kontratlardan da erişime izin vermemektedir.
  • internal : Sadece kontrat içinden ve türeyen kontrat içinden erişime izin verir.
  • public : Dışarıdan veya türeyen bir kontrat içinden erişime imkan tanır. Bu şekilde tanımlanan fonksiyonlar, diğer kontrat veya hesaplardan çağırılabilirler.
  • external : Sadece kontrat dışından yapılan çağrıları karşılayan fonksiyonlar için kullanılır, public’ten farkı sadece dışarıdan çağrılmasıdır. Bu şekilde tanımlanan fonksiyonların içeriden çağrılması önerilmez ama zorunlu durumda this.[fonksiyon adı] ile çağrılabilirler.

Kod üzerinden inceleyelim. Aşağıda function visibility örneklerini derleyip deploy ettiğinizde private ve internal olanların remix üzerinde görünmediğini, public ve external olanların listelendiğini göreceksiniz. Dışarıdan erişime kapatmak veya kısıtlamak istediğimiz fonksiyonlarımızı bu şekilde tanımlayabiliriz.

Bunun yanında state değişkenleri üzerindeki etkisiyle de tanımlama yapılır. Örneğin bir fonksiyon state değişkenini değiştiriyorsa veya değiştirmiyor sadece okuyorsa tanımının baştan belirtilmesi gerekir.

State Üzerindeki Etkisine Göre Fonksiyon Tanımları

  • view : Bu anahtar kelime ile tanımlanan fonksiyonlar state okuması yapmakta fakat state üzerinde herhangi bir değişiklik yapmamaktadır. Tek başına çağırıldığında bir gas bedeline neden olmazlar.
  • pure : State okuması veya değişimi yapmayan, sadece kendisine verilen parametreler üzerinden işlemler yapan fonksiyonlar için kullanılır. Tek başına çağırıldığında bir gas bedeline neden olmazlar.
  • payable : Ödeme alabilen fonksiyonlar bu anahtar kelime ile tanımlanmaktadır.

Özetlersek, view ve pure fonksiyonlar state üzerinde herhangi bir değişiklik yapamazlar. Pure olarak tanımlanan fonksiyonların farkı bu fonksiyonlar state verisini okuyamazlar. Tek başlarına çağırıldıklarında gas ücreti yoktur fakat state değiştiren bir fonksiyon içinden çağırıldıklarında az da olsa bir gas ücretine neden olurlar.

Yukarıdaki tanımları örnek üzerinde daha net görelim.

payableTest fonksiyonumuz ödeme alabilen bir fonksiyon ve getBalance ile de alınan ödemelerin toplamını öğrenebiliriz. Remix ile deneme yaparken bir fonksiyona ödeme yapabilmek için Value alanına istenen miktar girilip fonksiyon butonu tıklandığında girilen değeri o fonksiyona ödeme olarak geçecektir.

Receive ve Fallback Fonksiyonları

Akıllı kontrata yani kontrat adresine doğrudan ETH gönderimi yapılabilir. Önceki bölümde payable fonksiyonun ETH almak için kullanıldığını yazmıştım. Fakat bu durumda yazdığınız fonksiyonu çağırmak ve o fonksiyona value olarak ETH gönderimi yapmak gerekiyordu. Fakat kontrat içindeki bir fonksiyonu çağırmaya gerek kalmadan doğrudan kontrat adresine de ETH gönderimi yapılabilmektedir. Bu işlemler için özel iki fonksiyon bulunmaktadır. Receive ve Fallback fonksiyonları.

Receive

Akıllı kontrata Ether gönderimi yapılıyorsa ve herhangi bir fonksiyon çağrımı yapılmadıysa bu fonksiyon çağırılır. Parametre almaz, dönüş değeri yoktur ve external olarak tanımlanmalıdır.

receive() external payable {    }

Bu fonksiyonu kontrat içine koyduğunuzda doğrudan kontrat adresine yapılan ödemelerde receive fonksiyonu çağırılır. Örneğin bir kontrat adresine 1 ETH gönderilecek diyelim, bu gönderim yapıldığında kontrat içindeki receive fonksiyonu çağırılır. Gönderilen miktardan bağımsız olarak receive fonksiyonu çalışmaktadır, yani 0 Wei de gönderseniz receive fonksiyonu çağırılacaktır, çünkü bir transaction vardır ve value değeri sıfır olsa bile işlem gerçekleştiğinden receive çağırılır. Kod örneğiyle görelim.

Bu örnekte receive fonksiyonu çağırıldığında counter değeri bir artacaktır. Akıllı kontratımızı Remix üzerinde deploy ettikten sonra doğrudan kontrat adresine ödeme yapalım. Bunun için Remix’deki VALUE alanına istediğimiz bir değeri verip Low level interactions daki Transact düğmesi ile kontrata ödeme yapabiliriz. CallData kısmını boş bırakıyoruz, bu boş ise kontrat içindeki bir fonksiyon çağırılmamış anlamına gelir.

Her Transact işleminde kontrat içindeki receive fonksiyonu çağrılır ve receiveCallCounter bir artar, value değerini sıfır göndersek de bir artarak devam edecektir. Örnek olarak 2 Wei gönderimi yapalım sonrasında 0 Wei ile Transact çalıştıralım. Contract balance değeri 2 Wei olacaktır ve receiveCallCounter da 2 değerini alacaktır.

CALLDATA değerini boş bırakarak çağırdık ve VALUE değerine 2 Wei verdik. Transaction detayına bakarsak contract içindeki receive fonksiyonunun çağrıldığı görülecektir.

from 0x5B38Da6a701c568545dCfcB03FcB8...
to ReceiveFunction.(receive)0xddaAd340b0f...

Tekrar vurgulamak gerekirse, CallData boş olarak gönderildiğinde yani sadece ödeme işlemlerinde receive fonksiyonu çağırılacaktır.

Fallback

Akıllı kontrat içindeki bir fonksiyon çağrılmak istenildiğinde, çağırılan fonksiyon kontrat içindeki fonksiyonlardan biriyle eşleşmiyorsa fallback fonksiyonu çağırılır.

Fallback fonksiyon çağrımı Receive fonksiyonun varlığına göre değişir. Eğer Receive fonksiyonu tanımlı değil ise, CallData boş ise ve ödeme işlemi ise onun yerine Fallback fonksiyonu çağırılır. Fallback konusunun sonunda bunu bir şema ile daha net göreceğiz. Fallback fonksiyonu geniş bir amaç için kullanılabilir aşağıda tanım örneklerinden görülmektedir. Temel olarak bir fonksiyon çağrısında çağırılan fonksiyon kontrat içindekilerle eşleşmiyorsa fallback fonksiyonuna düşmektedir.

//Sadece fonksiyon çağırımları için
fallback () external
//Fonksiyon çağırımı ve Ether transferleri için
fallback () external payable
//Sadece fonksiyon çağırımları için, gelen data input içinde
fallback (bytes calldata input) external returns (bytes memory output)
//Fonksiyon çağırımı ve Ether transferleri için,
//gelen data input içinde
fallback (bytes calldata input) external payable returns (bytes memory output)

Şimdi kısa kodlama ile örneklendirelim.

İki adet counter ekledik birisi receive diğeri de fallback çağırıldığında artacak. Fallback çağırıldığında counter ikişer ikişer artarak devam edecek. Bunu deploy edip Transact CallData kısmına 0x1234 yani contract içinde olmayan bir fonksiyon adresi ile çağıralım.

Transaction incelendinde to kısmında fallback çağırıldığı görülmektedir. fallbackCallCounter değerimiz de iki artarak 2 olmaktadır.

to  FallbackFunction.(fallback) 0x93f8dddd876c7dBE3323...

Eğer calldata içerisini boşaltıp Transact ile işlemi başlatırsak bu sefer receive fonksiyonu çağırılır ve receiveCallCounter bir artarak 1 olur.

Genelde önerilen, bu iki fonksiyonun da kontrat içerisine eklenmesidir. Receive fonksiyonunun olmadığı durumlarda da receive fonksiyonun görevini fallback almaktadır. Yani ödeme işleminde calldata boş geliyorsa ve receive fonksiyonu yoksa fallback fonksiyonu çağırılır. Bunu aşağıdaki şemadan daha net görebiliriz.

Modifier

Bir diğer özel tip fonksiyon ise modifier fonksiyonlarıdır. Modifier fonksiyonları tanımlandıktan sonra kontrol edilmesi istenen fonksiyonlara uygulanarak kullanılır. Genelde erişim kontrolü veya parametre koşul kontrolleri gibi işlemlerde büyük kolaylık sağlar. Modifier içerisinden state değişkenlerine veya parametrelere erişebilir bunların kontrollerini yapabilirsiniz. Böylece bir fonksiyon çalışmadan o fonksiyon ile ilgili bazı kontrolleri yapabilmek mümkün olmaktadır. Aşağıda bununla ilgili kısa ve sade bir örnek vermeye çalıştım.

ModifierDemo isimli contract’ta yapılan; Gönderilen ödemeler sonrasında kontrat sahibinin withdraw fonksiyonu ile kendi hesabına transfer işlemidir.

Bu kontrat içinde onlyOwner isimli bir modifier bulunmakta, bunun görevi kontrat sahibi(owner) olmayan hesabın withdraw fonksiyonunu çağıramamasını sağlamaktır. Kontrat sahibi hesap tanımı(owner değişkeninde tutulur) constructor içerisinde yapılmaktadır. Bu kontratı deploy eden hesap owner state değişkenine atanır. withdraw fonksiyonundan önce onlyOwner çalışır ve eğer işlemi yapmak isteyen hesap owner ile aynı değil ise hata vererek işlemi iptal eder.

Denemek için kontrat hesabına bir miktar ETH gönderip daha sonra başka bir hesaptan withdraw çağırmak isterseniz aşağıdaki hatayı alırsınız.

The transaction has been reverted to the initial state.
Reason provided by the contract: "only owner can call..".

Eğer kontrat sahibi hesaptan withdraw fonksiyonunu çağırırsanız parametre olarak geçeceğiniz miktarda ödeme kontrat sahibi hesabına transfer edilmiş olacaktır. Başka bir örnekle modifier konusuna devam edelim.

Bu örnekte val değişkenini set eden SetVal fonksiyonu bulunmakta. Bu fonksiyona parametre olarak geçilen _val değeri rangeControl modifier ile kontrol edilmekte ve eğer 1–10 arası bir değer değil ise işlem iptale düşmektedir. Dikkat ederseniz setVal fonksiyonu argümanı olan _val değerini rangeControl modifier’a parametre olarak geçtik. Bu şekilde fonksiyon parametre kontrolünü de gerçekleştirebilmekteyiz.

Yukarıdaki örneklerde fonksiyon çalışmadan bazı işlemler yaptık. Fakat modifier fonksiyon çalışmasını bitirdikten sonra da uygulanabilir. Aşağıda buna örnek verilecektir. Bunu yapabilmek için _; karakterlerinden sonra istenen kodlama eklenebilir. Bu şekilde eklendiğinde fonksiyon çalıştıktan sonra örneğin bir state değişkeninin içeriği değiştirilebilir. Bunu görelim.

Bu örnekte locked değişkeni fonksiyon çalışmadan önce true verliyor ve çalışması bittikten sonra false olarak değiştiriliyor. Böylece fonksiyon çalışırken içerisine girilmesine izin vermiyor yani kilit konulmuş oluyor. Bunun gibi güvenlik önlemlerini ileriki yazılarda detaylı anlatmaya çalışacağım. Örnekte verilmek istenen ana konu, modifier’ın her iki şekilde yani fonksiyon öncesi ve sonrasında da kullanılabileceğidir.

Function Override

Eğer bir kontrat miras aldığı kontrattaki fonksiyonu geçersiz kılmak istiyorsa bunu override anahtar kelimesi ile yapabilir. Türetilen(base) kontrattaki override edilebilecek fonksiyon virtual anahtar kelimesi ile tanımlanmalıdır. Örnekle inceleyelim.

Örnek kodlamadaki Parent kontrat içinde tanımlı test fonksiyonu virtual olarak tanımlanmış yani override edilebilir. Child kontrat ise Parent’tan türetilmiş ve test fonksiyonu için overrride tanımı yapılmış. Böylece Child kontrat içindeki test fonksiyonu çağırıldığında doğrudan bu kontrat içindeki test fonksiyonu çalıştırılmış olur, Parent içindeki test fonksiyonu geçersiz kılınmış olmaktadır. Yani Child içindeki test fonksiyonu çalıştırıldığında 2 değeri dönecektir.

Diğer fonksiyon olan test2 çağırıldığında ise super.test() çağrımı yapmakta. Bu şekilde miras alınan kontrata erişmek mümkündür. Böylece super.test() çağrısı Parent içindeki test fonksiyonunu çağırır ve dönüş değeri olarak 1 dönecektir.

Serideki diğer yazılar:

Temel Bilgiler

İlk Akıllı Kontrat ve Remix

Tipler

Engin Ünal

--

--