Ana içeriğe geç

ERC-20 Sözleşmesine Genel Bakış

solidity
erc-20
Acemi
Ori Pomerantz
9 Mart 2021
22 dakikalık okuma

Giriş

Ethereum'un en yaygın kullanımlarından biri, bir grubun bir anlamda kendi para birimi olan ticareti yapılabilen bir token oluşturmasıdır. Bu jetonlar genellikle bir standardı takip eder, ERC-20. Bu standart, likidite havuzları ve cüzdanlar gibi tüm ERC-20 jetonlarıyla çalışan araçlar yazmayı mümkün kılar. Bu makalede OpenZeppelin Solidity ERC20 uygulamasını (opens in a new tab) ve arayüz tanımını (opens in a new tab) analiz edeceğiz.

Bu, açıklamalı bir kaynak kodudur. ERC-20'yi uygulamak istiyorsanız bu eğitimi okuyun (opens in a new tab).

Arayüz

ERC-20 gibi bir standardın amacı, cüzdanlar ve merkeziyetsiz borsalar gibi uygulamalar arasında birlikte çalışabilen birçok jeton uygulamasını mümkün kılmaktır. Bunu başarmak için bir arayüz (opens in a new tab) oluştururuz. Jeton sözleşmesini kullanması gereken herhangi bir kod, arayüzdeki aynı tanımları kullanabilir ve onu kullanan tüm jeton sözleşmeleriyle uyumlu olabilir; bu MetaMask gibi bir cüzdan, etherscan.io gibi bir dapp veya bir likidite havuzu gibi farklı bir sözleşme olabilir.

ERC-20 arayüzünün çizimi

Deneyimli bir programcıysanız, Java (opens in a new tab) veya hatta C başlık dosyalarında (opens in a new tab) benzer yapılar gördüğünüzü muhtemelen hatırlarsınız.

Bu, OpenZeppelin'den alınan ERC-20 Arayüzünün (opens in a new tab) bir tanımıdır. İnsan tarafından okunabilir standardın (opens in a new tab) Solidity koduna çevirisidir. Elbette arayüzün kendisi, herhangi bir şeyin nasıl yapılacağını tanımlamaz. Bu, aşağıdaki sözleşme kaynak kodunda açıklanmıştır.

 

1// SPDX-License-Identifier: MIT

Solidity dosyalarının bir lisans tanımlayıcısı içermesi gerekir. Lisansların listesini burada görebilirsiniz (opens in a new tab). Farklı bir lisansa ihtiyacınız varsa bunu yorumlarda açıklamanız yeterlidir.

 

1pragma solidity >=0.6.0 <0.8.0;

Solidity dili hâlâ hızla gelişiyor ve yeni sürümler eski kodlarla uyumlu olmayabilir (buraya bakın (opens in a new tab)). Bu nedenle, yalnızca dilin minimum sürümünü değil, aynı zamanda kodu test ettiğiniz en son sürüm olan maksimum sürümünü de belirtmek iyi bir fikirdir.

 

1/**
2 * @dev EIP'de tanımlandığı şekliyle ERC20 standardının arayüzü.
3 */

Yorumdaki @dev, kaynak kodundan doküman üretmek için kullanılan NatSpec formatının (opens in a new tab) bir parçasıdır.

 

1interface IERC20 {

Kural gereği, arayüz adları I ile başlar.

 

1 /**
2 * @dev Mevcut jeton miktarını döndürür.
3 */
4 function totalSupply() external view returns (uint256);

Bu fonksiyon external'dır, yani yalnızca sözleşmenin dışından çağrılabilir (opens in a new tab). Sözleşmedeki toplam jeton arzını döndürür. Bu değer, Ethereum'daki en yaygın tür olan işaretsiz 256 bit kullanılarak döndürülür (256 bit, EVM'nin yerel kelime boyutudur). Bu fonksiyon aynı zamanda bir view'dır, yani durumu değiştirmez, bu nedenle blokzincirindeki her düğümün çalıştırması yerine tek bir düğümde yürütülebilir. Bu tür bir fonksiyon bir işlem oluşturmaz ve gaz maliyeti yoktur.

Not: Teoride, bir sözleşmeyi oluşturan kişinin, gerçek değerden daha küçük bir toplam arz döndürerek her bir jetonun gerçekte olduğundan daha değerli görünmesini sağlayarak hile yapabileceği düşünülebilir. Ancak bu korku, blokzincirin gerçek doğasını göz ardı eder. Blokzincirinde olan her şey her düğüm tarafından doğrulanabilir. Bunu başarmak için, her sözleşmenin makine dili kodu ve depolaması her düğümde mevcuttur. Sözleşmeniz için Solidity kodunu yayınlamanız gerekmese de, sağladığınız makine dili koduna karşı doğrulanabilmesi için kaynak kodunu ve derlendiği Solidity sürümünü yayınlamadığınız sürece kimse sizi ciddiye almaz. Örneğin, bu sözleşmeye (opens in a new tab) bakın.

 

1 /**
2 * @dev `account`un sahip olduğu jeton miktarını döndürür.
3 */
4 function balanceOf(address account) external view returns (uint256);

Adından da anlaşılacağı gibi, balanceOf bir hesabın bakiyesini döndürür. Ethereum hesapları, 160 bit tutan address türü kullanılarak Solidity'de tanımlanır. Ayrıca external ve view'dur.

 

1 /**
2 * @dev Arayan kişinin hesabından `recipient`a `amount` kadar jeton taşır.
3 *
4 * İşlemin başarılı olup olmadığını gösteren bir boolean değeri döndürür.
5 *
6 * Bir {Transfer} olayı yayar.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);

transfer fonksiyonu, jetonları arayandan farklı bir adrese aktarır. Bu bir durum değişikliği içerir, dolayısıyla bir view değildir. Bir kullanıcı bu fonksiyonu çağırdığında bir işlem oluşturur ve gaz harcar. Ayrıca, blokzincirindeki herkese olay hakkında bilgi vermek için Transfer adlı bir olay yayar.

Fonksiyonun, iki farklı türde arayan için iki tür çıktısı vardır:

  • Fonksiyonu doğrudan bir kullanıcı arayüzünden çağıran kullanıcılar. Genellikle kullanıcı bir işlem gönderir ve süresiz olarak sürebilecek bir yanıt beklemez. Kullanıcı, işlem makbuzunu (işlem karması ile tanımlanır) arayarak veya Transfer olayını arayarak ne olduğunu görebilir.
  • Fonksiyonu genel bir işlemin parçası olarak çağıran diğer sözleşmeler. Bu sözleşmeler, aynı işlemde çalıştıkları için sonucu hemen alır, böylece fonksiyon dönüş değerini kullanabilirler.

Aynı tür çıktı, sözleşmenin durumunu değiştiren diğer fonksiyonlar tarafından oluşturulur.

 

Ödenekler, bir hesabın farklı bir sahibine ait olan bazı jetonları harcamasına izin verir. Bu, örneğin satıcı olarak hareket eden sözleşmeler için kullanışlıdır. Sözleşmeler olayları izleyemez, bu nedenle bir alıcı jetonları doğrudan satıcı sözleşmesine aktarırsa bu sözleşme ödendiğini bilemez. Bunun yerine alıcı, satıcı sözleşmesinin belirli bir miktarı harcamasına izin verir ve satıcı bu tutarı transfer eder. Bu, satıcı sözleşmesinin çağırdığı bir fonksiyon aracılığıyla yapılır, böylece satıcı sözleşmesi başarılı olup olmadığını anlayabilir.

1 /**
2 * @dev `spender`ın {transferFrom} aracılığıyla `owner` adına harcamasına
3 * izin verilecek kalan jeton sayısını döndürür. Bu varsayılan
4 * olarak sıfırdır.
5 *
6 * Bu değer, {approve} veya {transferFrom} çağrıldığında değişir.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);

allowance fonksiyonu, herkesin bir adresin (owner) başka bir adresin (spender) harcamasına izin verdiği ödeneği sorgulamasına olanak tanır.

 

1 /**
2 * @dev Arayanın jetonları üzerinde `spender`ın ödeneği olarak `amount`u ayarlar.
3 *
4 * İşlemin başarılı olup olmadığını gösteren bir boole değeri döndürür.
5 *
6 * ÖNEMLİ: Bu yöntemle bir ödeneği değiştirmenin,
7 * birisinin şanssız bir işlem sıralamasıyla hem eski hem de yeni ödeneği
8 * kullanma riski taşıdığına dikkat edin. Bu yarış
9 * durumunu azaltmak için olası bir çözüm, önce harcama yapanın ödeneğini 0'a
10 * indirmek ve ardından istenen değeri ayarlamaktır:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Bir {Approval} olayı yayar.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Tümünü göster

approve fonksiyonu bir ödenek oluşturur. Nasıl kötüye kullanılabileceğiyle ilgili mesajı okuduğunuzdan emin olun. Ethereum'da kendi işlemlerinizin sırasını kontrol edersiniz, ancak diğer tarafın işleminin gerçekleştiğini görene kadar kendi işleminizi göndermezseniz, diğer kişilerin işlemlerinin yürütüleceği sırayı kontrol edemezsiniz.

 

1 /**
2 * @dev Ödenek mekanizmasını kullanarak `sender`dan `recipient`a `amount`
3 * kadar jeton taşır. `amount` daha sonra arayanın
4 * ödeneğinden düşülür.
5 *
6 * İşlemin başarılı olup olmadığını gösteren bir boole değeri döndürür.
7 *
8 * Bir {Transfer} olayı yayar.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Tümünü göster

Son olarak, transferFrom harcayan tarafından ödeneği gerçekten harcamak için kullanılır.

 

1
2 /**
3 * @dev Bir hesaptan (`from`) diğerine (`to`)
4 * `value` kadar jeton taşındığında yayılır.
5 *
6 * `value`un sıfır olabileceğini unutmayın.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Bir `owner` için bir `spender` ödeneği, {approve} çağrısıyla
12 * ayarlandığında yayılır. `value` yeni ödenektir.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Tümünü göster

Bu olaylar, ERC-20 sözleşmesinin durumu değiştiğinde yayılır.

Asıl Sözleşme

Bu, buradan alınan (opens in a new tab) ERC-20 standardını uygulayan asıl sözleşmedir. Olduğu gibi kullanılması amaçlanmamıştır, ancak onu kullanılabilir bir şeye genişletmek için ondan kalıtım alabilirsiniz (opens in a new tab).

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.6.0 <0.8.0;

 

İçe Aktarma İfadeleri

Yukarıdaki arayüz tanımlarına ek olarak, sözleşme tanımı diğer iki dosyayı içe aktarır:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
  • GSN/Context.sol, ether'i olmayan kullanıcıların blokzincirini kullanmasına olanak tanıyan bir sistem olan OpenGSN (opens in a new tab)'yi kullanmak için gereken tanımlardır. Bunun eski bir sürüm olduğunu unutmayın, OpenGSN ile entegre olmak istiyorsanız bu eğitimi kullanın (opens in a new tab).
  • Solidity'nin <0.8.0 sürümleri için aritmetik taşmaları/eksik kalmaları önleyen SafeMath kütüphanesi (opens in a new tab). Solidity ≥0.8.0'da, aritmetik işlemler taşma/eksik kalma durumunda otomatik olarak geri döner, bu da SafeMath'i gereksiz kılar. Bu sözleşme, eski derleyici sürümleriyle geriye dönük uyumluluk için SafeMath kullanır.

 

Bu yorum, sözleşmenin amacını açıklar.

1/**
2 * @dev {IERC20} arayüzünün uygulanması.
3 *
4 * Bu uygulama, jetonların oluşturulma şeklinden bağımsızdır. Bu, {_mint} kullanılarak
5 * türetilmiş bir sözleşmeye bir arz mekanizmasının eklenmesi gerektiği anlamına gelir.
6 * Genel bir mekanizma için bkz. {ERC20PresetMinterPauser}.
7 *
8 * İPUCU: Ayrıntılı bir yazı için rehberimize bakın
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Arz
10 * mekanizmaları nasıl uygulanır].
11 *
12 * Genel OpenZeppelin yönergelerini takip ettik: fonksiyonlar başarısız olduğunda
13 * `false` döndürmek yerine geri döner. Bu davranış yine de gelenekseldir
14 * ve ERC20 uygulamalarının beklentileriyle çelişmez.
15 *
16 * Ek olarak, {transferFrom} çağrılarında bir {Approval} olayı yayılır.
17 * Bu, uygulamaların sadece bu olayları dinleyerek tüm hesaplar için
18 * ödeneği yeniden oluşturmasına olanak tanır. EIP'nin diğer uygulamaları, spesifikasyonda
19 * gerekli olmadığı için bu olayları yaymayabilir.
20 *
21 * Son olarak, standart dışı {decreaseAllowance} ve {increaseAllowance}
22 * fonksiyonları, ödenekleri ayarlamayla ilgili bilinen sorunları azaltmak
23 * için eklenmiştir. Bkz. {IERC20-approve}.
24 */
25
Tümünü göster

Sözleşme Tanımı

1contract ERC20 is Context, IERC20 {

Bu satır, bu durumda OpenGSN için yukarıdaki IERC20'den ve Context'ten kalıtımı belirtir.

 

1
2 using SafeMath for uint256;
3

Bu satır, SafeMath kütüphanesini uint256 türüne bağlar. Bu kütüphaneyi burada (opens in a new tab) bulabilirsiniz.

Değişken Tanımları

Bu tanımlar, sözleşmenin durum değişkenlerini belirtir. Bu değişkenler private olarak bildirilmiştir, ancak bu yalnızca blokzincirindeki diğer sözleşmelerin onları okuyamayacağı anlamına gelir. Blokzincirinde sır yoktur, her düğümdeki yazılım, her bloktaki her sözleşmenin durumuna sahiptir. Kural olarak, durum değişkenleri _<bir şey> olarak adlandırılır.

İlk iki değişken eşlemelerdir (opens in a new tab), yani anahtarların sayısal değerler olması dışında kabaca ilişkisel dizilerle (opens in a new tab) aynı şekilde davranırlar. Depolama, yalnızca varsayılandan (sıfır) farklı değerlere sahip girdiler için tahsis edilir.

1 mapping (address => uint256) private _balances;

İlk eşleme olan _balances, adresleri ve bu jetonun ilgili bakiyelerini içerir. Bakiyeye erişmek için şu söz dizimini kullanın: _balances[<adres>].

 

1 mapping (address => mapping (address => uint256)) private _allowances;

Bu değişken, _allowances, daha önce açıklanan ödenekleri saklar. İlk dizin jetonların sahibidir ve ikincisi ödeneğe sahip olan sözleşmedir. A adresinin B adresinin hesabından harcayabileceği miktara erişmek için _allowances[B][A] kullanın.

 

1 uint256 private _totalSupply;

Adından da anlaşılacağı gibi, bu değişken toplam jeton arzını takip eder.

 

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;

Bu üç değişken okunabilirliği artırmak için kullanılır. İlk ikisi kendini açıklayıcıdır, ancak _decimals farklıdır.

Bir yandan, Ethereum'da kayan noktalı veya kesirli değişkenler yoktur. Diğer yandan, insanlar jetonları bölebilmeyi sever. İnsanların para birimi olarak altını seçmesinin bir nedeni, birisi bir ördeğin değerinde inek almak istediğinde para üstü vermenin zor olmasıydı.

Çözüm, tam sayıları takip etmek, ancak gerçek jeton yerine neredeyse değersiz olan kesirli bir jetonu saymaktır. Ether durumunda, kesirli jetona wei denir ve 10^18 wei bir ETH'ye eşittir. Bu yazı yazıldığı sırada, 10.000.000.000.000 wei yaklaşık bir ABD veya Euro sentine eşittir.

Uygulamaların jeton bakiyesini nasıl göstereceklerini bilmeleri gerekir. Bir kullanıcının 3.141.000.000.000.000.000 wei'si varsa, bu 3,14 ETH midir? 31,41 ETH mi? 3.141 ETH mi? Ether durumunda, ETH'ye 10^18 wei olarak tanımlanır ancak kendi jetonunuz için farklı bir değer seçebilirsiniz. Jetonu bölmek mantıklı değilse, sıfır değerinde bir _decimals kullanabilirsiniz. ETH ile aynı standardı kullanmak istiyorsanız, 18 değerini kullanın.

Oluşturucu

1 /**
2 * @dev {name} ve {symbol} için değerleri ayarlar, {decimals}'i
3 * varsayılan 18 değeriyle başlatır.
4 *
5 * {decimals} için farklı bir değer seçmek için {_setupDecimals} kullanın.
6 *
7 * Bu üç değerin tümü sabittir: yalnızca oluşturma sırasında
8 * bir kez ayarlanabilirler.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 // Solidity ≥0.7.0'da, 'public' örtüktür ve atlanabilir.
12
13 _name = name_;
14 _symbol = symbol_;
15 _decimals = 18;
16 }
Tümünü göster

Oluşturucu, sözleşme ilk oluşturulduğunda çağrılır. Kural gereği, fonksiyon parametreleri <bir şey>_ olarak adlandırılır.

Kullanıcı Arayüzü Fonksiyonları

1 /**
2 * @dev Jetonun adını döndürür.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Genellikle adın daha kısa bir versiyonu olan jetonun sembolünü
10 * döndürür.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Kullanıcı gösterimini elde etmek için kullanılan ondalık sayısını döndürür.
18 * Örneğin, `decimals` `2`'ye eşitse, `505` jetonluk bir bakiye
19 * kullanıcıya `5,05` (`505 / 10 ** 2`) olarak gösterilmelidir.
20 *
21 * Jetonlar genellikle ether ve wei arasındaki ilişkiyi taklit ederek 18 değerini
22 * tercih ederler. Bu, {_setupDecimals} çağrılmadığı sürece {ERC20}'nin
23 * kullandığı değerdir.
24 *
25 * NOT: Bu bilgi yalnızca _gösterim_ amacıyla kullanılır: hiçbir
26 * şekilde {IERC20-balanceOf} ve {IERC20-transfer} dahil olmak üzere
27 * sözleşmenin aritmetiğini etkilemez.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Tümünü göster

Bu fonksiyonlar, name, symbol ve decimals, kullanıcı arayüzlerinin sözleşmeniz hakkında bilgi sahibi olmalarına yardımcı olur, böylece sözleşmenizi düzgün bir şekilde görüntüleyebilirler.

Dönüş türü string memory'dir, yani bellekte depolanan bir dize döndürür. Dizeler gibi değişkenler üç konumda saklanabilir:

Geçerlilik SüresiSözleşme ErişimiGaz Bedeli
BellekFonksiyon çağrısıOkunur/YazılırOnlarca veya yüzlerce (daha yüksek konumlar için daha yüksek)
CalldataFonksiyon çağrısıSalt OkunurDönüş türü olarak kullanılamaz, yalnızca bir fonksiyon parametre türü olarak kullanılabilir
DepolamaDeğişene kadarOkunur/YazılırYüksek (Okuma için 800, yazma için 20 bin)

Bu durumda memory en iyi seçenektir.

Jeton Bilgilerini Oku

Bunlar, toplam arz veya bir hesabın bakiyesi gibi jeton hakkında bilgi sağlayan fonksiyonlardır.

1 /**
2 * @dev Bkz. {IERC20-totalSupply}.
3 */
4 function totalSupply() public view override returns (uint256) {
5 return _totalSupply;
6 }

totalSupply fonksiyonu, toplam jeton arzını döndürür.

 

1 /**
2 * @dev Bkz. {IERC20-balanceOf}.
3 */
4 function balanceOf(address account) public view override returns (uint256) {
5 return _balances[account];
6 }

Bir hesabın bakiyesini okuyun. Herkesin başka birinin hesap bakiyesini almasına izin verildiğini unutmayın. Zaten her düğümde mevcut olduğu için bu bilgiyi saklamaya çalışmanın bir anlamı yoktur. Blokzincirinde sır yoktur.

Jetonları Aktar

1 /**
2 * @dev Bkz. {IERC20-transfer}.
3 *
4 * Gereklilikler:
5 *
6 * - `recipient` sıfır adres olamaz.
7 * - arayan en az `amount` bakiyeye sahip olmalıdır.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Tümünü göster

transfer fonksiyonu, jetonları gönderenin hesabından farklı bir hesaba aktarmak için çağrılır. Bir boole değeri döndürmesine rağmen, bu değerin her zaman doğru olduğunu unutmayın. Aktarım başarısız olursa sözleşme çağrıyı geri alır.

 

1 _transfer(_msgSender(), recipient, amount);
2 return true;
3 }

_transfer fonksiyonu asıl işi yapar. Yalnızca diğer sözleşme fonksiyonları tarafından çağrılabilen özel bir fonksiyondur. Kural olarak özel fonksiyonlar, durum değişkenleriyle aynı şekilde _<bir şey> olarak adlandırılır.

Normalde Solidity'de mesajı gönderen için msg.sender kullanırız. Ancak bu, OpenGSN (opens in a new tab)'yi bozar. Jetonumuzla ether'sız işlemlere izin vermek istiyorsak, _msgSender() kullanmalıyız. Normal işlemler için msg.sender döndürür, ancak ether'sız işlemler için mesajı ileten sözleşmeyi değil, orijinal imzalayanı döndürür.

Ödenek Fonksiyonları

Bunlar, ödenek işlevselliğini uygulayan fonksiyonlardır: allowance, approve, transferFrom ve _approve. Ek olarak, OpenZeppelin uygulaması, güvenliği artıran bazı özellikleri içerecek şekilde temel standardın ötesine geçer: increaseAllowance ve decreaseAllowance.

Ödenek fonksiyonu

1 /**
2 * @dev Bkz. {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }

allowance fonksiyonu herkesin herhangi bir ödeneği kontrol etmesini sağlar.

approve fonksiyonu

1 /**
2 * @dev Bkz. {IERC20-approve}.
3 *
4 * Gereklilikler:
5 *
6 * - `spender` sıfır adres olamaz.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {

Bu işlev, bir ödenek oluşturmak için çağrılır. Yukarıdaki transfer fonksiyonuna benzer:

  • Fonksiyon yalnızca, gerçek işi yapan dahili bir fonksiyonu (bu durumda _approve) çağırır.
  • Fonksiyon ya true döndürür (başarılı ise) ya da hata verir (değilse).

 

1 _approve(_msgSender(), spender, amount);
2 return true;
3 }

Durum değişikliklerinin meydana geldiği yerlerin sayısını en aza indirmek için dahili fonksiyonları kullanıyoruz. Durumu değiştiren herhangi bir fonksiyon, güvenlik için denetlenmesi gereken potansiyel bir güvenlik riskidir. Bu şekilde daha az hata yapma ihtimalimiz olur.

transferFrom fonksiyonu

Bu, bir harcama yapanın bir ödenek harcamak için çağırdığı fonksiyondur. Bunun için iki işlem gerekir: harcanan tutarı transfer edin ve ödeneği bu tutar kadar azaltın.

1 /**
2 * @dev Bkz. {IERC20-transferFrom}.
3 *
4 * Güncellenmiş ödeneği gösteren bir {Approval} olayı yayar. Bu,
5 * EIP tarafından gerekli değildir. {ERC20}'nin başındaki nota bakın.
6 *
7 * Gereklilikler:
8 *
9 * - `sender` ve `recipient` sıfır adres olamaz.
10 * - `sender` en az `amount` bakiyeye sahip olmalıdır.
11 * - arayan, ``sender``ın jetonları için en az
12 * `amount` kadar ödeneğe sahip olmalıdır.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Tümünü göster

 

a.sub(b, "message") fonksiyon çağrısı iki şey yapar. İlk olarak, yeni ödenek olan a-b hesabını yapar. İkincisi, bu sonucun negatif olmadığını kontrol eder. Negatifse, verilen mesajla çağrı geri döner. Bir çağrı geri döndüğünde, o çağrı sırasında daha önce yapılmış herhangi bir işlemin yok sayıldığını ve bu nedenle _transfer işlemini geri almamız gerekmediğini unutmayın.

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: transfer amount exceeds allowance"));
3 return true;
4 }

OpenZeppelin güvenlik eklemeleri

Sıfırdan farklı bir ödeneği başka bir sıfırdan farklı değere ayarlamak tehlikelidir çünkü yalnızca kendi işlemlerinizin sırasını kontrol edersiniz, başkalarınınkini değil. Saf olan Alice ve dürüst olmayan Bill olmak üzere iki kullanıcınız olduğunu hayal edin. Alice, Bill'den beş jetona mal olduğunu düşündüğü bir hizmet istiyor, bu yüzden Bill'e beş jetonluk bir ödenek veriyor.

Sonra bir şeyler değişir ve Bill'in fiyatı on jetona yükselir. Hâlâ hizmeti isteyen Alice, Bill'in ödeneğini ona ayarlayan bir işlem gönderir. Bill, işlem havuzunda bu yeni işlemi gördüğü anda, Alice'in beş jetonunu harcayan ve çok daha yüksek bir gaz fiyatına sahip olan bir işlem gönderir, böylece işlem daha hızlı kazılır. Bu şekilde Bill, ilk beş jetonu harcayabilir ve ardından, Alice'in yeni ödeneği çıkarıldığında, on beş jetonluk toplam fiyat için, Alice'in yetkilendirmek istediğinden daha fazla olacak şekilde on tane daha harcayabilir. Bu tekniğe önden çalıştırma (opens in a new tab) denir

Alice'in İşlemiAlice NonceBill'in İşlemiBill NonceBill'in ÖdeneğiBill'in Alice'ten Toplam Geliri
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10.12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10.124015

Bu sorunu önlemek için, bu iki fonksiyon (increaseAllowance ve decreaseAllowance), ödeneği belirli bir miktarda değiştirmenize olanak tanır. Yani Bill zaten beş jeton harcamışsa, sadece beş tane daha harcayabilecektir. Zamanlamaya bağlı olarak, bunun iki sonucu olabilir ve her ikisinde de Bill yalnızca on jeton alabilir:

A:

Alice'in İşlemiAlice NonceBill'in İşlemiBill NonceBill'in ÖdeneğiBill'in Alice'ten Toplam Geliri
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10.12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10.124010

B:

Alice'in İşlemiAlice NonceBill'in İşlemiBill NonceBill'in ÖdeneğiBill'in Alice'ten Toplam Geliri
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10.124010
1 /**
2 * @dev Arayan tarafından `spender`'a verilen ödeneği atomik olarak artırır.
3 *
4 * Bu, {IERC20-approve}'da açıklanan sorunlar için bir azaltma olarak kullanılabilecek
5 * {approve}'a bir alternatiftir.
6 *
7 * Güncellenmiş ödeneği belirten bir {Approval} olayı yayar.
8 *
9 * Gereklilikler:
10 *
11 * - `spender` sıfır adres olamaz.
12 */
13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
15 return true;
16 }
Tümünü göster

a.add(b) fonksiyonu güvenli bir toplamadır. a+b>=2^256 olması gibi düşük bir ihtimalde, normal toplamanın yaptığı gibi başa dönmez.

1
2 /**
3 * @dev Arayan tarafından `spender`'a verilen ödeneği atomik olarak azaltır.
4 *
5 * Bu, {IERC20-approve}'da açıklanan sorunlar için bir azaltma olarak kullanılabilecek
6 * {approve}'a bir alternatiftir.
7 *
8 * Güncellenmiş ödeneği belirten bir {Approval} olayı yayar.
9 *
10 * Gereklilikler:
11 *
12 * - `spender` sıfır adres olamaz.
13 * - `spender`'ın arayan için en az
14 * `subtractedValue` kadar ödeneği olmalıdır.
15 */
16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
18 "ERC20: decreased allowance below zero"));
19 return true;
20 }
Tümünü göster

Jeton Bilgilerini Değiştiren Fonksiyonlar

Bunlar asıl işi yapan dört fonksiyondur: _transfer, _mint, _burn ve _approve.

_transfer fonksiyonu

1 /**
2 * @dev `sender`'dan `recipient`'a `amount` kadar jeton taşır.
3 *
4 * Bu dahili fonksiyon {transfer}'a eşdeğerdir ve örneğin
5 * otomatik jeton ücretlerini, kesme mekanizmalarını vb. uygulamak için kullanılabilir.
6 *
7 * Bir {Transfer} olayı yayar.
8 *
9 * Gereklilikler:
10 *
11 * - `sender` sıfır adres olamaz.
12 * - `recipient` sıfır adres olamaz.
13 * - `sender` en az `amount` bakiyeye sahip olmalıdır.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Tümünü göster

Bu fonksiyon, _transfer, jetonları bir hesaptan diğerine aktarır. Hem transfer (gönderenin kendi hesabından yapılan transferler için) hem de transferFrom (başka birinin hesabından transfer için ödenekleri kullanmak için) tarafından çağrılır.

 

1 require(sender != address(0), "ERC20: transfer from the zero address");
2 require(recipient != address(0), "ERC20: transfer to the zero address");

Ethereum'da hiç kimse aslında sıfır adresine sahip değildir (yani, eşleşen ortak anahtarı sıfır adresine dönüştürülen özel bir anahtarı kimse bilmez). İnsanlar bu adresi kullandığında, bu genellikle bir yazılım hatasıdır. Bu nedenle gönderen veya alıcı olarak sıfır adres kullanılırsa başarısız oluruz.

 

1 _beforeTokenTransfer(sender, recipient, amount);
2

Bu sözleşmeyi kullanmanın iki yolu vardır:

  1. Kendi kodunuz için bir şablon olarak kullanın
  2. Ondan kalıtım alın (opens in a new tab) ve yalnızca değiştirmeniz gereken fonksiyonları geçersiz kılın

İkinci yöntem çok daha iyidir çünkü OpenZeppelin ERC-20 kodu zaten denetlenmiş ve güvenli olduğu gösterilmiştir. Kalıtım kullandığınızda, değiştirdiğiniz fonksiyonların ne olduğu açıktır ve sözleşmenize güvenmek için kişilerin yalnızca bu belirli fonksiyonları denetlemesi gerekir.

Jetonlar her el değiştirdiğinde bir fonksiyon gerçekleştirmek genellikle yararlıdır. Ancak,_transfer çok önemli bir fonksiyondur ve güvensiz bir şekilde yazılması mümkündür (aşağıya bakın), bu nedenle onu geçersiz kılmamak en iyisidir. Çözüm, bir kanca fonksiyonu (opens in a new tab) olan _beforeTokenTransfer'dır. Bu fonksiyonu geçersiz kılabilirsiniz ve her aktarımda çağrılacaktır.

 

1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
2 _balances[recipient] = _balances[recipient].add(amount);

Bunlar aslında aktarımı yapan satırlardır. Aralarında hiçbir şey olmadığını ve aktarılan tutarı alıcıya eklemeden önce göndericiden çıkardığımızı unutmayın. Bu önemlidir çünkü ortada farklı bir sözleşmeye çağrı olsaydı, bu sözleşmeyi aldatmak için kullanılabilirdi. Bu şekilde aktarım atomiktir, ortasında hiçbir şey olamaz.

 

1 emit Transfer(sender, recipient, amount);
2 }

Son olarak, bir Transfer olayı yayınlayın. Olaylara akıllı sözleşmelerle erişilemez, ancak blokzincirinin dışında çalışan kod, olayları dinleyebilir ve bunlara tepki verebilir. Örneğin bir cüzdan, sahibinin ne zaman daha fazla jeton aldığını takip edebilir.

_mint ve _burn fonksiyonları

Bu iki fonksiyon (_mint ve _burn), toplam jeton arzını değiştirir. Bunlar dahili fonksiyonlardır ve bu sözleşmede onları çağıran bir fonksiyon yoktur, bu nedenle yalnızca sözleşmeden kalıtım alırsanız ve hangi koşullar altında yeni jetonlar basacağınıza veya mevcut olanları yakacağınıza karar vermek için kendi mantığınızı eklerseniz kullanışlıdırlar.

NOT: Her ERC-20 jetonunun, jeton yönetimini belirleyen kendi iş mantığı vardır. Örneğin, sabit arza sahip bir sözleşme oluşturucusunda yalnızca _mint'i çağırabilir ve _burn'ü asla çağırmayabilir. Jeton satan bir sözleşme, ödeme yapıldığında _mint'i çağırır ve muhtemelen bir noktada enflasyonun kontrolden çıkmasını önlemek için _burn'ü çağırır.

1 /** @dev `amount` kadar jeton oluşturur ve bunları `account`a atayarak
2 * toplam arzı artırır.
3 *
4 * `from` sıfır adrese ayarlanmış bir {Transfer} olayı yayar.
5 *
6 * Gereklilikler:
7 *
8 * - `to` sıfır adres olamaz.
9 */
10 function _mint(address account, uint256 amount) internal virtual {
11 require(account != address(0), "ERC20: mint to the zero address");
12 _beforeTokenTransfer(address(0), account, amount);
13 _totalSupply = _totalSupply.add(amount);
14 _balances[account] = _balances[account].add(amount);
15 emit Transfer(address(0), account, amount);
16 }
Tümünü göster

Toplam jeton sayısı değiştiğinde _totalSupply'ı güncellediğinizden emin olun.

 

1 /**
2 * @dev `account`tan `amount` kadar jetonu yok ederek toplam
3 * arzı azaltır.
4 *
5 * `to` sıfır adrese ayarlanmış bir {Transfer} olayı yayar.
6 *
7 * Gereklilikler:
8 *
9 * - `account` sıfır adres olamaz.
10 * - `account` en az `amount` kadar jetona sahip olmalıdır.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: burn from the zero address");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
18 _totalSupply = _totalSupply.sub(amount);
19 emit Transfer(account, address(0), amount);
20 }
Tümünü göster

_burn fonksiyonu, ters yönde çalışması dışında _mint ile neredeyse aynıdır.

_approve fonksiyonu

Bu, ödenekleri gerçekten belirten fonksiyondur. Sahibinin, kendi mevcut bakiyesinden daha yüksek bir ödenek belirlemesine izin verdiğini unutmayın. Bu sorun değil çünkü bakiye, ödenek oluşturulduğundaki bakiyeden farklı olabileceği için transfer sırasında kontrol edilir.

1 /**
2 * @dev `owner`ın jetonları üzerinde `spender`ın ödeneği olarak `amount`u ayarlar.
3 *
4 * Bu dahili fonksiyon `approve`a eşdeğerdir ve örneğin
5 * belirli alt sistemler için otomatik ödenekler ayarlamak için kullanılabilir.
6 *
7 * Bir {Approval} olayı yayar.
8 *
9 * Gereklilikler:
10 *
11 * - `owner` sıfır adres olamaz.
12 * - `spender` sıfır adres olamaz.
13 */
14 function _approve(address owner, address spender, uint256 amount) internal virtual {
15 require(owner != address(0), "ERC20: approve from the zero address");
16 require(spender != address(0), "ERC20: approve to the zero address");
17
18 _allowances[owner][spender] = amount;
Tümünü göster

 

Bir Approval olayı yayınlayın. Uygulamanın nasıl yazıldığına bağlı olarak, harcayan sözleşmeye onay hakkında ya sahibi tarafından ya da bu olayları dinleyen bir sunucu tarafından bilgi verilebilir.

1 emit Approval(owner, spender, amount);
2 }
3

Ondalık Değişkenini Değiştir

1
2
3 /**
4 * @dev {decimals}'i varsayılan 18 değerinden başka bir değere ayarlar.
5 *
6 * UYARI: Bu fonksiyon yalnızca oluşturucudan çağrılmalıdır. Jeton
7 * sözleşmeleriyle etkileşimde bulunan çoğu uygulama
8 * {decimals}'in hiç değişmesini beklemez ve değişirse yanlış çalışabilir.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Tümünü göster

Bu fonksiyon, kullanıcı arayüzlerine miktarın nasıl yorumlanacağını söylemek için kullanılan _decimals değişkenini değiştirir. Bunu oluşturucudan çağırmalısınız. Daha sonraki herhangi bir noktada onu çağırmak sahtekârlık olur ve uygulamalar bununla başa çıkmak için tasarlanmamıştır.

Kancalar

1
2 /**
3 * @dev Herhangi bir jeton transferinden önce çağrılan kanca. Bu,
4 * basmayı ve yakmayı içerir.
5 *
6 * Çağırma koşulları:
7 *
8 * - `from` ve `to` ikisi de sıfır değilken, ``from``'un jetonlarından `amount`
9 * kadarı `to`'ya transfer edilir.
10 * - `from` sıfırken, `to` için `amount` kadar jeton basılır.
11 * - `to` sıfırken, ``from``'un jetonlarından `amount` kadarı yakılır.
12 * - `from` ve `to` ikisi de asla sıfır olmaz.
13 *
14 * Kancalar hakkında daha fazla bilgi edinmek için şuraya gidin: xref:ROOT:extending-contracts.adoc#using-hooks[Kancaları Kullanma].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Tümünü göster

Bu, aktarımlar sırasında çağrılacak kanca fonksiyonudur. Bu örnekte kanca fonksiyonu boş ancak ihtiyaç duyarsanız fonksiyon içeriğini doldurabilirsiniz.

Sonuç

İnceleme için, bu sözleşmedeki en önemli fikirlerden bazıları şunlardır (bana göre, sizinki farklılık gösterebilir):

  • Blokzincirde sır yoktur. Bir akıllı sözleşmenin erişebileceği herhangi bir bilgi tüm dünya tarafından kullanılabilir.
  • Kendi işlemlerinizin sırasını kontrol edebilirsiniz, ancak diğer kişilerin işlemlerinin ne zaman gerçekleşeceğini kontrol edemezsiniz. Bu, bir ödeneği değiştirmenin tehlikeli olabilmesinin nedenidir, çünkü harcayanın her iki ödeneğin toplamını harcamasına izin verir.
  • uint256 türündeki değerler başa döner. Başka bir deyişle, 0-1=2^256-1. Bu istenen davranış değilse, bunu kontrol etmeniz (veya sizin için yapan SafeMath kütüphanesini kullanmanız) gerekir. Bunun Solidity 0.8.0 (opens in a new tab)'da değiştiğini unutmayın.
  • Belirli bir türdeki tüm durum değişikliklerini belirli bir yerde yapın, çünkü bu denetimi kolaylaştırır. Örneğin, approve, transferFrom, increaseAllowance ve decreaseAllowance tarafından çağrılan _approve fonksiyonuna sahip olmamızın nedeni budur.
  • Durum değişiklikleri, aralarında başka bir eylem olmaksızın atomik olmalıdır (_transfer'da görebileceğiniz gibi). Bunun nedeni, durum değişikliği sırasında tutarsız bir duruma sahip olmanızdır. Örneğin, gönderenin bakiyesinden düşüldüğü zaman ile alıcının bakiyesine eklendiği zaman arasında, var olması gerekenden daha az jeton bulunur. Aralarında işlemler, özellikle farklı bir sözleşmeye yapılan çağrılar varsa, bu potansiyel olarak kötüye kullanılabilir.

Artık OpenZeppelin ERC-20 sözleşmesinin nasıl yazıldığını ve özellikle nasıl daha güvenli hâle getirildiğini gördüğünüze göre, gidin ve kendi güvenli sözleşmelerinizi ve uygulamalarınızı yazın.

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

Sayfanın son güncellenmesi: 22 Ekim 2025

Bu rehber yararlı oldu mu?