pure: Fonksiyonun blokchain’den hem veri okumayacağını hem de değişiklik yapmayacağını bildirir. State içerisinde herhangi bir şey okumayacak ve yazmayacak, sadece Contract içinde çalışacak bir işlev.
constant: Değeri değiştirilemeyen değişkenlerdir. Atanan değer kontrat deploy edildikten sonra bir daha değiştirilemez. Gaz maaliyetinden tasarruf sağlayabilir.
immutable: Değeri değiştirilemeyen değişkenlerdir. Constant’tan farkı immutable ile işaretlenmiş değişkenin değerinin constructor ile başlangıçta değiştirilebiliyor oluşudur.
// For - While Döngüleri Kontrat Örneği
//SPDX-License-Identifier: Unlicensed
pragma solidity^0.8.0;
contract Loops {
uint256[15] public numbers0;
uint256[15] public numbers1;
boolpublic state =true;
int256public num =0;
functionlistByFor() public {
uint256[15] memory nums = numbers0;
for (uint256 i =0; i < nums.length; i++) {
//if(i == 13) continue;
//if(i == 14) break;
nums[i] = i;
}
numbers0 = nums;
}
functiongetArr0() publicviewreturns (uint256[15] memory) {
return numbers0;
}
functionlistByWhile() public {
uint256 i =0;
while (i < numbers1.length) {
numbers1[i] = i;
i++;
}
}
functiongetArr1() publicviewreturns (uint256[15] memory) {
return numbers1;
}
functioncrashByWhile() public {
while (state) {
num++;
num--;
}
}
}
Mapping
Mapping, Solidity’e özel bir veri tipidir. Diğer dillerdeki Dictionary, Map, HashTable veri tiplerine benzetebiliriz.
Eğer doğrudan iterate etmemiz gereken durumlar yoksa veya array’de ardışık şekilde sıralamamız gerekmiyorsa Mapping kullanmalıyız. Mapping kullanmak büyük gas tasarrufları yapmamıza ve kolayca verilere erişmemize yarıyor.
// Mapping
//SPDX-License-Identifier: Unlicensed
pragma solidity^0.8.0;
contract Mapping {
mapping(address=>bool) public registered;
mapping(address=>int256) public favNums;
functionregister(int256 _favNum) public {
//require(!registered[msg.sender], "Kullanıcınız daha önce kayıt yaptı.");
require(!isRegistered(), "Kullaniciniz daha once kayit yapti.");
registered[msg.sender] =true;
favNums[msg.sender] = _favNum;
}
functionisRegistered() publicviewreturns (bool) {
return registered[msg.sender];
}
functiondeleteRegistered() public {
require(isRegistered(), "Kullaniciniz kayitli degil.");
delete (registered[msg.sender]);
delete (favNums[msg.sender]);
}
}
contract NestedMapping {
mapping(address=>mapping(address=>uint256)) public debts;
functionincDebt(address _borrower, uint256 _amount) public {
debts[msg.sender][_borrower] += _amount;
}
functiondecDebt(address _borrower, uint256 _amount) public {
require(debts[msg.sender][_borrower] >= _amount, "Not enough debt.");
debts[msg.sender][_borrower] -= _amount;
}
functiongetDebt(address _borrower) publicviewreturns (uint256) {
return debts[msg.sender][_borrower];
}
}
Struct and Enum
Struct ve Enum veri tipleri.
memory, is a temporary place to store data. memory is used to make a transient reference to something in storage. Veriyi saklamak için kullanılan geçiçi bir yerdir. Sadece function scope içerisinde yer alır.
storage, holds data between function calls. Fonksiyon bittikten sonra da contract içerisinde yaşamaya devam eder. Global değişkenleri tanımlarken kullanılır. Diğer programlama dillerindeki pointer kavramına benzetebiliriz.
// Struct and Enum
//SPDX-License-Identifier: Unlicensed
pragma solidity^0.8.0;
contract StructEnum {
enum Status {
Taken, // 0
Preparing, // 1
Boxed, // 2
Shipped // 3
}
struct Order {
address customer;
string zipCode;
uint256[] products;
Status status;
}
Order[] public orders;
addresspublic owner;
constructor() {
owner =msg.sender;
}
functioncreateOrder(stringmemory _zipCode, uint256[] memory _products) externalreturns(uint256) {
require(_products.length >0, "No products.");
Order memory order;
order.customer =msg.sender;
order.zipCode = _zipCode;
order.products = _products;
order.status = Status.Taken;
orders.push(order);
// orders.push(
// Order({
// customer: msg.sender,
// zipCode: _zipCode,
// products: _products,
// status: Status.Taken
// })
// );
// orders.push(Order(msg.sender, _zipCode, _products, Status.Taken));
return orders.length -1; // 0 1 2 3
}
functionadvanceOrder(uint256 _orderId) external {
require(owner ==msg.sender, "You are not authorized.");
require(_orderId < orders.length, "Not a valid order id.");
Order storage order = orders[_orderId];
require(order.status != Status.Shipped, "Order is already shipped.");
if (order.status == Status.Taken) {
order.status = Status.Preparing;
} elseif (order.status == Status.Preparing) {
order.status = Status.Boxed;
} elseif (order.status == Status.Boxed) {
order.status = Status.Shipped;
}
}
functiongetOrder(uint256 _orderId) externalviewreturns (Order memory) {
require(_orderId < orders.length, "Not a valid order id.");
/*
Order memory order = orders[_orderId];
return order;
*/return orders[_orderId];
}
functionupdateZip(uint256 _orderId, stringmemory _zip) external {
require(_orderId < orders.length, "Not a valid order id.");
Order storage order = orders[_orderId];
require(order.customer ==msg.sender, "You are not the owner of the order.");
order.zipCode = _zip;
}
}
Modifiers
A modifier aims to change the behaviour of the function to which it is attached. Modifiers are code that can be run before and / or after a function call.
Modifiers can be used to:
Restrict access
Validate inputs
Guard against reentrancy hack
Modifiers genelde contract sonuna yazılır (not necessary).
// Modifiers
//SPDX-License-Identifier: Unlicensed
//@notice it's advanced version of previous parts example
pragma solidity^0.8.0;
contract Modifier {
enum Status {
Taken,
Preparing,
Boxed,
Shipped
}
struct Order {
address customer;
string zipCode;
uint256[] products;
Status status;
}
Order[] public orders;
addresspublic owner;
uint256public txCount;
constructor() {
owner =msg.sender;
}
functioncreateOrder(stringmemory _zipCode, uint256[] memory _products) checkProducts(_products) incTx externalreturns(uint256) {
// require(_products.length > 0, "No products.");
Order memory order;
order.customer =msg.sender;
order.zipCode = _zipCode;
order.products = _products;
order.status = Status.Taken;
orders.push(order);
return orders.length -1;
}
functionadvanceOrder(uint256 _orderId) checkOrderId(_orderId) onlyOwner external {
// require(owner == msg.sender, "You are not authorized.");
// require(_orderId < orders.length, "Not a valid order id.");
Order storage order = orders[_orderId];
require(order.status != Status.Shipped, "Order is already shipped.");
if (order.status == Status.Taken) {
order.status = Status.Preparing;
} elseif (order.status == Status.Preparing) {
order.status = Status.Boxed;
} elseif (order.status == Status.Boxed) {
order.status = Status.Shipped;
}
}
functiongetOrder(uint256 _orderId) checkOrderId(_orderId) externalviewreturns (Order memory) {
// require(_orderId < orders.length, "Not a valid order id.");
return orders[_orderId];
}
functionupdateZip(uint256 _orderId, stringmemory _zip) checkOrderId(_orderId) onlyCustomer(_orderId) incTx external {
// require(_orderId < orders.length, "Not a valid order id.");
Order storage order = orders[_orderId];
// require(order.customer == msg.sender, "You are not the owner of the order.");
order.zipCode = _zip;
}
modifiercheckProducts(uint256[] memory _products) {
require(_products.length >0, "No products.");
_;
}
modifiercheckOrderId(uint256 _orderId) {
require(_orderId < orders.length, "Not a valid order id.");
_;
}
modifierincTx {
_;
txCount++;
}
modifieronlyOwner {
require(owner ==msg.sender, "You are not authorized.");
_;
}
modifieronlyCustomer(uint256 _orderId) {
require(orders[_orderId].customer ==msg.sender, "You are not the owner of the order.");
_;
}
}
Events
Standalone çalışan contract içerisinde önemli olmasa da DApp’ lerde önemli bir işlevi vardır. Akıllı sözleşmelerdeki işlemlerin sonuçlarını ve dışarı çıkarmak istedeğimiz bilgileri events yardımı ile yapabiliyoruz.
indexed eğer yayınladığımız bir event’de bir değişkeni indexed olarak belirlersek sonrasında blockchain’den bu indexed değerlerini sorgulayabiliyoruz.
// Events
//SPDX-License-Identifier: Unlicensed
//@notice it's advanced version of previous parts example
pragma solidity^0.8.0;
contract Events {
enum Status {
Taken,
Preparing,
Boxed,
Shipped
}
struct Order {
address customer;
string zipCode;
uint256[] products;
Status status;
}
Order[] public orders;
addresspublic owner;
uint256public txCount;
event OrderCreated(uint256 _orderId, addressindexed _consumer);
event ZipChanged(uint256 _orderId, string _zipCode);
constructor() {
owner =msg.sender;
}
functioncreateOrder(stringmemory _zipCode, uint256[] memory _products) checkProducts(_products) incTx externalreturns(uint256) {
Order memory order;
order.customer =msg.sender;
order.zipCode = _zipCode;
order.products = _products;
order.status = Status.Taken;
orders.push(order);
emit OrderCreated(orders.length -1, msg.sender);
return orders.length -1;
}
functionadvanceOrder(uint256 _orderId) checkOrderId(_orderId) onlyOwner external {
Order storage order = orders[_orderId];
require(order.status != Status.Shipped, "Order is already shipped.");
if (order.status == Status.Taken) {
order.status = Status.Preparing;
} elseif (order.status == Status.Preparing) {
order.status = Status.Boxed;
} elseif (order.status == Status.Boxed) {
order.status = Status.Shipped;
}
}
functiongetOrder(uint256 _orderId) checkOrderId(_orderId) externalviewreturns (Order memory) {
return orders[_orderId];
}
functionupdateZip(uint256 _orderId, stringmemory _zip) checkOrderId(_orderId) onlyCustomer(_orderId) incTx external {
Order storage order = orders[_orderId];
order.zipCode = _zip;
emit ZipChanged(_orderId, _zip);
}
modifiercheckProducts(uint256[] memory _products) {
require(_products.length >0, "No products.");
_;
}
modifiercheckOrderId(uint256 _orderId) {
require(_orderId < orders.length, "Not a valid order id.");
_;
}
modifierincTx {
_;
txCount++;
}
modifieronlyOwner {
require(owner ==msg.sender, "You are not authorized.");
_;
}
modifieronlyCustomer(uint256 _orderId) {
require(orders[_orderId].customer ==msg.sender, "You are not the owner of the order.");
_;
}
}
Sending Ethers
payable: Ether transfer edebileceğimiz fonksiyon ve adresleri bildirir. payable kullanmaz ve fonksiyon içerisinde msg.value kullanmaya çalışırsak hata alırız.
Bir fonksiyona ether göndermek için fonksiyonun payable olarak nitelenmiş olması gerekir
require function should be used to check return values from calls to external contracts or to guarantee that valid conditions, such as inputs or contract state variables, are satisfied. Kullanıcı sebebi ile oluşacak hataları engellemek için kullanılır.
assert function should only be used to examine invariants and test for internal problems.
Use require() to:
Validate user inputs ie. require(input<20);
Validate the response from an external contract ie. require(external.send(amount));
Validate state conditions prior to execution, ie. require(block.number > SOME_BLOCK_NUMBER) or require(balance[msg.sender]>=amount)
Generally, you should use require most often
Generally, it will be used towards the beginning of a function
Use revert() to:
Handle the same type of situations as require(), but with more complex logic.
Use assert() to:
Check for overflow/underflow, ie. c = a+b; assert(c > b)
// Errors.sol
//SPDX-License-Identifier: Unlicensed
pragma solidity^0.8.0;
contract Errors {
uint256public totalBalance;
mapping(address=>uint256) public userBalances;
error ExceedingAmount(address user, uint256 exceedingAmount);
error Deny(string reason);
receive() externalpayable {
revert Deny("No direct payments.");
}
fallback() externalpayable {
revert Deny("No direct payments.");
}
functionpay() noZero(msg.value) externalpayable {
require(msg.value==1ether, "Only payments in 1 ether");
totalBalance +=1ether; // 1e18
userBalances[msg.sender] +=1ether; // 10000...0000
}
functionwithdraw(uint256 _amount) noZero(_amount) external {
uint256 initalBalance = totalBalance;
//require(userBalances[msg.sender] >= _amount, "Insufficient balance.");
if(userBalances[msg.sender] < _amount) {
//revert("Insufficient balance.");
revert ExceedingAmount(msg.sender, _amount - userBalances[msg.sender]);
}
totalBalance -= _amount;
userBalances[msg.sender] -= _amount;
// address => address payable
payable(msg.sender).transfer(_amount); // Doğrudan transfer methodunu çağrılırsa bir Re-Entrancy güvenlik açığı doğar. Bir fonksiyonu dışarıdan çağırmadan önce kendi kontratımızda yapılması gereken tüm değişikliklerin yapılması gerekiyor. Bu nedenle burada balance değişiklikleri yapılıyor.
assert(totalBalance < initalBalance);
}
modifiernoZero(uint256 _amount) {
require(_amount !=0);
_;
}
}
Library
library: Kontratlara benzer fakat ether alamaması ve durum değişkeni tutamaması ile farklılaşır. Kontratların içerisine gömülü fonksiyonların eklenmesine benzetilebilir.
using <libraryName> for <type> tanımlaması ile de kullanılabilir.
EVM (Ethereum Virtual Machine) ‘de 3 çeşit hafıza alanı (data location) bulunur.
storage: Bu veriler blokzincirde tutulur.
memory : Bu veriler fonksiyon çağrıldıktan itibaren EVM tarafından ayrılan özel bir bölgede tutulur ve fonksiyon bittiğinde silinir.
calldata: Bu veriler fonksiyon çağrılırken, çağrının (transaction) içerisinde tutulur (msg.data). Bu veriler sadece okunabilir (read-only).
bytes, string, uint256[], struct gibi referans tipleri fonksiyonlarda
kullanılırken bu verilerin hangi hafıza alanından alınacağı belirtilmelidir.
calldata sadece fonksiyon parametreleri için kullanılabilir. Eğer fonksiyon içerisinde verilen değerleri doğrudan okumak isterseniz bunu kullanın. memory yi fonksiyon içerisinde eğer bir değişiklik yapmayacaksanız, sadece okuma yapacaksaksanız kullanın. storage sadece fonksiyon gövdesinde kullanılabilir. Eğer fonksiyon içerisinde bir değişiklik yapacaksaksanız bunu kullanın.
Bunu yapmanız hem daha okunabilir, anlaşılabilir kodlar yazmanızı hem de gas tasarrufu yapmanızı sağlar. storage üzerinde yapılan işlemler daha pahalı iken memory üzerinde yapılan daha ucuzdur, calldata daha da ucuzdur.
// DataLocations.sol
// SPDX-License-Identifier: MIT
pragma solidity^0.8.0;
/*
Kontrat <---- Kontrata yapılan çağrı
------- -------------
Kontrat depolama alanı Fonksiyon için ayrılan hafıza ve çağrıdaki data alanı
memory: Geçici hafıza
storage: Kalıcı hafıza
calldata: Çağrıdaki argümanlar
bytes, string, array, struct
* Değer tipleri (uint, int, bool, bytes32) kontrat üzerinde storage,
fonksiyon içinde memory'dir
* mapping'ler her zaman kontrat üzerinde tanımlanır ve storage'dadır.
*/struct Student {
uint8 age;
uint16 score;
stringname;
}
contract School {
uint256 totalStudents =0; // storage
mapping(uint256=> Student) students; // storage
functionaddStudent(string calldata name, uint8 age, uint16 score) external {
uint256 currentId = totalStudents++;
students[currentId] = Student(age, score, name);
}
functionchangeStudentInfoStorage(
uint256 id, // memory
string calldata newName, // calldata
uint8 newAge, // memory
uint16 newScore // memory
) external {
Student storage currentStudent = students[id];
currentStudent.name= newName;
currentStudent.age = newAge;
currentStudent.score = newScore;
}
/**
@dev Bu işe yaramayacaktır, çünkü oluşturulan currentStudent ömrü
fonksiyonun bitişine kadar olan bir değişken ve fonksiyon tamamlandığında
silinecektir
*/functionchangeStudentInfoMemory(
uint256 id, // memory
string calldata newName, // calldata
uint8 newAge, // memory
uint16 newScore // memory
) external {
Student memory currentStudent = students[id];
currentStudent.name= newName;
currentStudent.age = newAge;
currentStudent.score = newScore;
}
functiongetStudentName(uint256 id) externalviewreturns(stringmemory) {
return students[id].name;
}
}
Inheritance
is: Solidity supports both single as well as multiple inheritance. Solidity çoklu kalıtımı destekleyen bir programlama dilidir. Kontratlar is anahtar sözcüğü ile diğer kontratları miras olarak alabilir.
virtual: Bir alt kontrat tarafından fonksiyonun geçersiz kılınabileceğini bildiren niteleyicidir. (Bir kontratı miras olarak aldığımızda virtual işaretli fonksiyonu tekrar düzenleyebilir ve içeriğini değiştirebiliriz.)
override: Bir üst kontratta bulunan virtual ile işaretlenmiş fonksiyonları geçersiz kılmamızı ve tekrardan tanımladığımızı bildiren niteleyicidir. (Miras aldığımız kontrakt içerisindeki özelliğini değiştirmek istediğimiz fonksiyon override olarak işaretlenmeli.)
super: Miras sırası önemlidir. C3-linearization kurallarına göre super anahtar sözcüğü ile miras alınan kontrata erişebiliriz.
Interface’ler (Arayüzler) çalışma mantığı farklı olan ama yaptığı iş aynı olan kontratların (örneğin token kontratları) ortak bir standarda sahip olmasını böylece bu kontratlarla çalışmak isteyen birinin her bir kontrata özgü kod yazmak yerine bu standarda uygun tek bir kod yazmasını sağlar.
ERC20, ERC721, ERC1155 gibi standartlar aslında bir interface şeklinde tanımlanmıştır.
call methodu aracılığıyla low-level çağrılar yapılıyor. Diğer akıllı kontratlar ile etkileşmemize yarar. Diğer akıllı kontratların fallback methodlarını tetiklemek veya doğrudan sadece bir ETH değeri göndermek için kullanılması öneriliyor. call methodu aracılığı ile diğer akıllı kontratlardaki fonksiyonu, eğer adını ve parametrelerini doğru şekilde biliyorsak çağırabiliyoruz. Bu da kontratın ABI bilgisine ve kontratın kendisine ihtiyacımız olmadan akıllı kontratları çağırmaya yarıyor.
call methoduna parametreleri doğrudan giremeyiz. abi.encodeWithSignature() methodu yardımı ile hash’leyerek girebiliyoruz. abi.encodeWithSignature() methodunun ilk parametresi çalıştırmak istediğimiz fonksiyon ve fonksiyona ait parametrelerin tipleri, diğer parametreler ise fonksiyona gönderilecek değerlerdir. Eğer ilk parametre olan fonksiyonu bilmiyorsak veya yanlış fonksiyon çağırırsak, bu fallback methodunu tetikler. Bu doğru bir kullanım değildir.
call methodu iki değer döndürüyor. Değerlerden birisi bool, diğeri bytes. İkinci değeri doğrudan değil hash’leyerek döndürüyor. abi.decode() ile bu değere erişebiliyoruz.
call methodu ile bir kontratın fallback methodunu tetiklemek için süslü parantez kullanarak msg.value değerini veya herhangi bir değer (1 ether vs.) gönderebiliyoruz. Eğer bu boş bir çağrıysa, yani herhangi bir fonksiyonu tetiklemek istemiyorsak parantez içerisine boş tırnak işareti koymamız gerekiyor.
call methodunun döndürdüğü ikinci değişkeni kullanmak istemiyorsak, ilk değerden sonra virgül yazıp gerisini boş bırakıyoruz.
Akıllı kontratlar aracılığıyla farklı akıllı kontratlar deploy edilmesi. Bunlara Factory Contract adı veriliyor.
Yeni bir kontrat deploy etme işlemi başka bir akıllı kontrat ile yapılsa bile çok gas harcayan maliyetli bir iştir. Ne kadar maliyetli olduğu kontratın ne kadar karmaşık olduğu ile ilgili. O yüzden bu tarz örnekleri mümkün olduğunca kullanmaktan kaçınmalıyız.