Ana içeriğe geç

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

solidityerc-20
Acemi
Ori Pomerantz
9 Mart 2021
22 dakikalık okuma minute read

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 token'lar genelde bir standarda, yani ERC-20'ye uyumludur. Bu standart, tüm ERC-20 token'larıyla çalışan likidite havuzları ve cüzdanlar gibi 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çıklanmış kaynak koddur. Eğer ERC-20 kullanmak isterseniz, bu öğreticiyi 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 token uygulamasına izin vermektir. Bunu sağlamak için, bir arayüz(opens in a new tab) oluştururuz. Token sözleşmesini kullanması gereken herhangi bir kod, arayüzde aynı tanımları kullanabilir ve onu kullanan tüm token sözleşmeleriyle uyumlu olarak, MetaMask gibi bir cüzdan, etherscan.io gibi bir dapp veya likidite havuzu gibi farklı bir sözleşme olabilir.

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

Deneyimli bir programcıysanız, muhtemelen Java(opens in a new tab)'da ve hatta C header dosyalarında(opens in a new tab) benzer yapılar gördüğünüzü hatırlıyorsunuzdur.

Bu, OpenZeppelin'in yaptığı bir ERC-20 Arayüzü(opens in a new tab) 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 şeyi nasıl yapacağını tanımlamaz. Bu, aşağıdaki sözleşme kaynak kodunda açıklanmıştır.

1// SPDX-License-Identifier: MIT
Kopyala

Solidity dosyalarının bir lisans tanımlayıcısı içermesi gerekir. Burada lisansların bir listesini 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;
Kopyala

Solidity dili hâlâ hızla gelişiyor ve yeni sürümler eski kodla uyumlu olmayabilir (buraya bakın(opens in a new tab)). Bu nedenle, dilin yalnızca 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 ERC20 standardının EIP'de tanımlandığı gibi arayüzü.
3 */
Kopyala

Yorumdaki @dev, kaynak kodundan belge oluşturmak için kullanılan NatSpec formatının(opens in a new tab) bir parçasıdır.

1interface IERC20 {
Kopyala

Kural olarak, arayüz isimleri I ile başlar.

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

Bu fonksiyon external'dır (harici), yani sadece sözleşmenin dışından çağrılabilir(opens in a new tab). Sözleşmedeki toplam token arzını döndürür. Bu değer, Ethereum'daki en yaygın tür olan imzasız 256 bit kullanılarak döndürülür (256 bit, EVM'nin yerel kelime boyutudur). Bu fonksiyon aynı zamanda bir view'dur, yani durumu değiştirmez, bu nedenle blok zincirindeki 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 fonksiyonun 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 token'ın gerçekte olduğundan daha değerli görünmesini sağlayarak hile yapabileceği görünebilir. Ancak, bu korku blok zincirinin gerçek doğasını görmezden geliyor. Blok zincirinde 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şmenizin Solidity kodunu yayınlamanız gerekmese de, sağladığınız makina dili koduna karşı doğrulanabilmesi için kaynak kodunu ve derlendiği Solidity versiyonunu paylaşana kadar kimse sizi ciddiye almaz. Örnek olarak, bu sözleşmeye(opens in a new tab) bakın.

1 /**
2 * @dev `account` tarafından sahip olunan token miktarını döndürür.
3 */
4 function balanceOf(address account) external view returns (uint256);
Kopyala

Adından da anlaşılacağı üzere, balanceOf (bakiyesi) 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 `amount` tokeni çağıranın hesabından `recipient` hesabına hareket ettirir.
3 *
4 * İşlemin başarılı olup olmadığını gösteren bir boole değeri döndürür.
5 *
6 * Bir {Transfer} olayı yayar.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
Kopyala

transfer fonksiyonu çağırandan farklı bir adrese token'ları aktarır. Bu bir durum değişikliği içerir, yani view değildir. Bir kullanıcı bu fonksiyonu çağırdığında bir işlem oluşturur ve gaz harcar. Ayrıca, blok zincirindeki herkese olay hakkında bilgi vermek için Transfer adlı bir olay yayar.

Fonksiyon, iki farklı türde çağıran için iki tür çıktıya sahiptir:

  • Fonksiyonu doğrudan bir kullanıcı arabiriminden çağıran kullanıcılar. Tipik olarak, kullanıcı bir işlem gönderir ve ne zaman geleceği belli olmayan yanıtın gelmesini beklemez. Kullanıcı, işlem makbuzunu (işlem hash değeri ile tanımlanır) arayarak veya Transfer olayını arayarak ne olduğunu görebilir.
  • Genel bir işlemin parçası olarak fonksiyonu çağıran diğer sözleşmeler. Bu sözleşmeler, aynı işlemde çalıştıkları için sonucu hemen alırlar, 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ı token'ları 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ı token'ları 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şmesinin başarılı olup olmadığını anlayabilir.

1 /**
2 * @dev, `spender` adresinin `owner` adına {transferFrom}
3 * aracılığıyla harcayabileceği kalan token miktarını döndürür. Bu
4 * varsayılan 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);
Kopyala

allowance fonksiyonu, herkesin bir adresin (owner) başka bir adresin (spender) harcamasına izin verdiği ödeneği görmek için sorgulama yapmasına olanak tanır.

1 /**
2 * @dev Çağıranın token'ları üzerinde `spender` ödeneğini `amount` olarak belirler.
3 *
4 * İşlemin başarılı olup olmadığını gösteren bir boolean değeri döndürür.
5 *
6 * ÖNEMLİ: Bu yöntemle bir ödeneği değiştirmenin, talihsiz işlem sıralaması ile
7 * birinin hem eski hem de yeni ödeneği kullanması riskini
8 * taşıdığına dikkat edin. Bu yarış koşulunun etkisini azaltmanın muhtemel bir yolu,
9 * ilk olarak harcayanın ödeneğini 0'a ayarlayıp arzulanan değeri
10 * daha sonra belirlemektir:
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
Kopyala

approve fonksiyonu bir ödenek oluşturur. Nasıl kötüye kullanılabileceğine dair 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öndermediğiniz sürece diğer kişilerin işlemlerinin yürütüleceği sırayı kontrol edemezsiniz.

1 /**
2 * @dev Ödenek mekanizmasını kullanarak `amount` token'ı `sender` adresinden `recipient`
3 * adresine aktarır. `amount` bunun sonrasında çağıranın ödeneğinden
4 * kesilir.
5 *
6 * İşlemin başarılı olup olmadığını gösteren bir boolean 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
Kopyala

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

1
2 /**
3 * @dev `value` token bir hesaptan (`from`) diğerine (`to`) hareket
4 * ettirildiğinde yayılır.
5 *
6 * `value` sıfır olabilir, bunu unutmayın.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. `value` yeni ödenektir.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Tümünü göster
Kopyala

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ı için yapılmamıştır, ancak onu kullanılabilir bir hâle getirmek için kalıtım(opens in a new tab) şeklinde alabilirsiniz.

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

İfadeleri İçe Aktarın

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";
Kopyala

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

1/**
2 * @dev {IERC20} arayüzünün uygulanması.
3 *
4 * Bu uygulama, token'ların oluşturulma şekline karşı agnostiktir. Bu,
5 * {_mint} kullanılarak türetilmiş bir sözleşmeye bir tedarik mekanizmasının eklenmesi gerektiği anlamına gelir.
6 * Kapsamlı 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[Tedarik
10 * mekanizmaları nasıl uygulanır].
11 *
12 * Genel OpenZeppelin talimatlarını izledik: fonksiyonlar, başarısızlık durumunda
13 * `false` döndürmek yerine geri alınırlar. 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, söz konusu olayları dinleyerek uygulamaların tüm hesaplar için
18 * ödeneği yeniden yapılandırmasına izin verir. EIP'nin diğer uygulamaları, şartname gerektirmediği için
19 * bu olayları yaymayabilir.
20 *
21 * Son olarak, ödenek ayarlama ile ilgili bilinen sorunları azaltmak için
22 * standart olmayan {decreaseAllowance} ve {increaseAllowance} fonksiyonları
23 * eklenmiştir. Bakınız {IERC20-approve}.
24 */
25
Tümünü göster
Kopyala

Sözleşme Tanımı

1contract ERC20 is Context, IERC20 {
Kopyala

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
Kopyala

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. Değişkenler private olarak bildirilir, ancak bu yalnızca blok zincirindeki diğer sözleşmelerin onları okuyamayacağı anlamına gelir. Blok zinciri üzerinde sır yoktur, her düğümdeki yazılım her bloktaki her sözleşmenin durumunu bulundurur. Kural olarak, durum değişkenleri _<something> olarak isimlendirilir.

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

1 mapping (address => uint256) private _balances;
Kopyala

İlk eşleme, _balances, adresler ve bu token'ın ilgili bakiyeleridir. Bakiyeye erişmek için, bu söz dizimini kullanın: _balances[<address>].

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

Bu değişken, _allowances, daha önce açıklanan ödenekleri saklar. İlk endeks, token'ların sahibidir ve ikincisi, ödeneğin olduğu 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;
Kopyala

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

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

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'un kayan nokta veya kesirli değişkenleri yoktur. Diğer taraftan, insanlar token'ları bölebilmeyi sever. İnsanların para birimi olarak altını seçmesinin bir nedeni, birisi bir ördeğin değerinde inek almak istediğinde değişiklik yapmanın zor olmasıydı.

Çözüm, tam sayıları takip etmektir ancak gerçek token yerine neredeyse değersiz olan kesirli bir token saymaktır. Ether durumunda, kesirli token wei olarak adlandırılır ve 10^18 wei bir ETH'ye eşittir. Yazarken, 10.000.000.000.000.000 wei yaklaşık olarak bir ABD veya Euro sentidir.

Uygulamalar token bakiyesini nasıl göstereceklerini bilmelidir. Bir kullanıcının 3.141.000.000.000.000.000 wei'si varsa, bu 3,14 ETH midir? 31.41 ETH? 3,141 ETH? Ether durumunda, ETH'ye 10^18 wei olarak tanımlanır ancak kendi token'ınız için farklı bir değer seçebilirsiniz. Eğer token'ı bölmek mantıklı gelmiyorsa sıfır değerinde bir _decimals kullanabilirsiniz. ETH ile aynı standardı kullanmak istiyorsanız, 18 değerini kullanın.

Yapıcı

1 /**
2 * @dev {name} ve {symbol} için değerleri belirler, varsayılan 18 değeriyle
3 * {decimals} oluşturur.
4 *
5 * {decimals} için farklı bir değer seçmek için, {_setupDecimals} kullanın.
6 *
7 * Bu değerlerin üçü de değişmezdir: yalnızca oluşturma sırasında bir kez
8 * ayarlanabilirler.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
Tümünü göster
Kopyala

Yapıcı, sözleşme ilk oluşturulduğunda çağrılır. Kural olarak, fonksiyon parametreleri <something>_ olarak isimlendirilir.

Kullanıcı Arayüzü Fonksiyonları

1 /**
2 * @dev Token'ın adını döndürür.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Returns the number of decimals used to get its user representation.
18 * Örnek olarak, eğer `decimals` eşittir `2` ise, `505` token'lık bakiye
19 * kullanıcıya `5,05` olarak gösterilmelidir (`505 / 10 ** 2`).
20 *
21 * Token'lar genellikle ether ve wei arasındaki ilişkiyi taklit ederek
22 * 18 değerini seçer. Bu, {_setupDecimals} çağrılmadıysa {ERC20} tarafından kullanılan
23 * değerdir.
24 *
25 * NOT: Bu bilgi yalnızca _görüntüleme_ amacıyla kullanılır:
26 * {IERC20-balanceOf} ve {IERC20-transfer} dahil olmak üzere hiçbir şekilde sözleşmenin
27 * aritmetiğini etkilemez.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Tümünü göster
Kopyala

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)
Çağrı VerisiFonksiyon ç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.

Token Bilgisini Okuyun

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

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

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

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

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. Blok zincirinde sır yoktur.

Token Transfer Edin

1 /**
2 * @dev See {IERC20-transfer}.
3 *
4 * Gereksinimler:
5 *
6 * - `recipient` sıfır adresi olamaz.
7 * - arayan kişinin en az `amount bakiyesi olmalıdır.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Tümünü göster
Kopyala

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

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

_transfer fonksiyonu asıl işi yapar. Yalnızca diğer sözleşme fonksiyonları tarafından çağrılabilen özel bir fonksiyondur. Konvansiyonel olarak özel fonksiyonlar, durum değişkenleriyle aynı şekilde _<something> 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)'i bozar. Eğer token'ımızla 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 fonksiyonlarını 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 See {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }
Kopyala

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

Onaylama fonksiyonu

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

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 }
Kopyala

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 See {IERC20-transferFrom}.
3 *
4 * Güncellenmiş ödeneği gösteren bir {Approval} olayı yayar. Bu
5 * EIP için gerekmez. {ERC20} başlangıcındaki nota bakınız.
6 *
7 * Gereksinimler:
8 *
9 * - `sender` ve `recipient` sıfır adresi olamaz.
10 * - `sender` en az `amount` miktarda bakiyeye sahip olmalıdır.
11 * - çağıranın ``sender`` token'ları için en az `amount`
12 * ödeneği 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
Kopyala

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 arama 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 }
Kopyala

OpenZeppelin güvenlik eklemeleri

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

Sonra bir şeyler değişir ve Bill'in fiyatı on token'a yükselir. Hâlâ hizmeti isteyen Alice, Bill'in ödeneğini 10'a ayarlayan bir işlem gönderir. Bill, işlem havuzunda bu yeni işlemi gördüğü anda, Alice'in beş token'ını 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ş token'ı harcayabilir ve ardından, Alice'in yeni ödeneği çıkarıldığında, on beş token'lık toplam fiyat için, Alice'in yetkilendirmek istediğinden daha fazla olacak şekilde on tane daha harcayabilir. Bu tekniğe front-running(opens in a new tab) denir

Alice'in İşlemiAlice'in Nonce DeğeriBill'in İşlemiBill'in Nonce DeğeriBill'in ÖdeneğiBill'in Alice'den 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ş token harcamışsa, sadece beş tane daha harcayabilecektir. Zamanlamaya bağlı olarak, bunun iki sonucu olabilir ve her ikisinde de Bill yalnızca on token alabilir:

A:

Alice'in İşlemiAlice'in Nonce DeğeriBill'in İşlemiBill'in Nonce DeğeriBill'in ÖdeneğiBill'in Alice'den 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'in Nonce DeğeriBill'in İşlemiBill'in Nonce DeğeriBill'in ÖdeneğiBill'in Alice'den Toplam Geliri
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Çağıran tarafından `spender` için sağlanan ödeneği atomik derecede artırır.
3 *
4 * Bu, {IERC20-approve} bölümünde açıklanan sorunlar için hafifletme olarak kullanılabilecek
5 * {approve} seçeneğine bir alternatiftir.
6 *
7 * Güncellenmiş ödeneği gösteren bir {Approval} olayı yayar.
8 *
9 * Gereksinimler:
10 *
11 * - `spender` sıfır adresi 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
Kopyala

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

1
2 /**
3 * @dev Çağıran tarafından `spender` için sağlanan ödeneği atomik derecede azaltır.
4 *
5 * Bu, {IERC20-approve} bölümünde açıklanan sorunlar için hafifletme olarak kullanılabilecek
6 * {approve} seçeneğine bir alternatiftir.
7 *
8 * Güncellenmiş ödeneği gösteren bir {Approval} olayı yayar.
9 *
10 * Gereksinimler:
11 *
12 * - `spender` sıfır adresi olamaz.
13 * - `spender` çağıran için en az `subtractedValue` kadar
14 * ödeneğe sahip olmalı.
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
Kopyala

Token Bilgilerini Değiştiren Fonksiyonlar

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

_transfer fonksiyonu {#_transfer}

1 /**
2 * @dev `amount` token'ı `sender`'dan `recipient`'a hareket ettirir.
3 *
4 * Bu dahili fonksiyon {transfer} ile eş değerdir ve şu amaçlarla kullanılabilir:
5 * örn. otomatik token ücretlerini, kesme mekanizmalarını vb. uygulama.
6 *
7 * Bir {Transfer} olayı yayar.
8 *
9 * Gereksinimler:
10 *
11 * - `sender` sıfır adresi olamaz.
12 * - `recipient` sıfır adresi olamaz.
13 * - `sender` en az `amount` miktarda bakiyeye sahip olmalıdır.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Tümünü göster
Kopyala

Bu fonksiyon, _transfer, token'ları 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 izinleri 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");
Kopyala

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
Kopyala

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 yoluyla 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.

Token'lar her el değiştirdiğinde bir fonksiyon gerçekleştirmek genellikle yararlıdır. Ancak,_transfer çok önemli bir fonksiyondur ve güvenli olmayan bir şekilde yazmak mümkündür (aşağıya bakın), bu nedenle geçersiz kılmamak en iyisidir. Çözüm, bir kanca fonksiyonu(opens in a new tab) olan _beforeTokenTransfer fonksiyonudur. 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);
Kopyala

Bunlar aslında aktarımı yapan hatlardı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, ortada farklı bir sözleşmeye çağrı olsaydı, bu sözleşmeyi aldatmak için kullanılmış olabileceği için önemlidir. Bu şekilde aktarım atomiktir, ortasında hiçbir şey olamaz.

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

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

_mint ve _burn fonksiyonları {#_mint-and-_burn}

Bu iki fonksiyon (_mint and _burn) toplam token arzını düzenler. Bunlar dahilidir ve bu sözleşmede onları çağıran bir fonksiyon yoktur, bu nedenle yalnızca sözleşmeden devralırsanız ve hangi koşullar altında yeni token'lar basacağınıza veya mevcut token'ları yakacağınıza karar vermek için kendi mantığınızı eklerseniz kullanışlıdırlar.

NOT: Her ERC-20 token'ının, token yönetimini belirleyen kendi çalışma mantığı vardır. Örneğin, sabit bir arz sözleşmesi, yapıcıda yalnızca _mint öğesini çağırabilir ve hiçbir zaman _burn öğesini çağıramaz. Token satan bir sözleşme, ödeme yapıldığında _mint'i ve kaçak enflasyonu önlemek için bir noktada muhtemelen _burn'u arayacaktır.

1 /** @dev `amount` token yaratır ve onları `account`'a atarak toplam arzı
2 * artırır.
3 *
4 * `from` sıfır adresine ayarlı olacak şekilde bir {Transfer} olayı yayar.
5 *
6 * Gereksinimler:
7 *
8 * - `to` sıfır adresi 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
Kopyala

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

1 /**
2 * @dev `amount` token'ı `account`'tan yok ederek toplam arzı
3 * azaltır.
4 *
5 * `to` sıfır adresine ayarlı olacak şekilde bir {Transfer} olayı yayar.
6 *
7 * Gereksinimler:
8 *
9 * - `account` sıfır adresi olamaz.
10 * - `account` en az `amount` miktarda token'a sahip olmalı.
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, diğer yöne gitmesi dışında _mint ile hemen hemen aynıdır.

_approve fonksiyonu {#_approve}

Bu aslında ödenekleri belirten fonksiyondur. Sahibin, kendi mevcut bakiyesinden daha yüksek bir ödenek belirlemesine izin verdiğini unutmayın. Bakiye, ödenek oluşturulduğundaki bakiyeden farklı olabileceği transfer sırasında kontrol edildiği için bu sorun yaratmaz.

1 /**
2 * @dev `owner` token'ları üzerinde `spender` ödeneğini `amount` olarak belirler.
3 *
4 * Bu dahili işlev `approve` ile eş değerdir ve şu amaçlarla kullanılabilir:
5 * örn. belirli alt sistemler için otomatik izinler ayarlama vb.
6 *
7 * Bir {Approval} olayı yayar.
8 *
9 * Gereksinimler:
10 *
11 * - `owner` sıfır adresi olamaz.
12 * - `spender` sıfır adresi 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
Kopyala

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

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

Ondalık Değişkenini Düzenleyin

1
2
3 /**
4 * @dev {decimals} değerini varsayılan olan 18 harici bir değere ayarlar.
5 *
6 * UYARI: Bu fonksiyon sadece yapıcıdan çağrılmalıdır. Çoğu
7 * Token sözleşmeleriyle etkileşime giren uygulama,
8 * {decimals} değerinin 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
Kopyala

Bu fonksiyon, kullanıcı arabirimlerine miktarın nasıl yorumlanacağını söylemek için kullanılan _decimals değişkenini değiştirir. Yapıcıdan ç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 token'ın aktarımı öncesi çağrılan kanca. Buna
4 * basım ve yakım dahildir.
5 *
6 * Çağrı koşulları:
7 *
8 * - `from` ve `to` sıfır olmadığında, `from`'un token `amount` değeri
9 * `to`'ya aktarılır.
10 * - `from` sıfırken, `amount` token `to` için basılır.
11 * - `to` sıfırken, ``from``'un `amount` kadar token'ı yakılır.
12 * - `from` ve `to` asla ikisi birden sıfır olmaz.
13 *
14 * Kancalar hakkında daha fazla bilgi edinmek için xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks] sayfasına gidin.
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Tümünü göster
Kopyala

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 (bence sizinki muhtemelen değişebilir):

  • Blok zincirinde sır yoktur.. Akıllı bir sözleşmenin erişebileceği herhangi bir bilgi tüm dünya tarafından kullanılabilir.
  • Başkalarının işlemleri gerçekleştiğinde anlar hariç kendi işlemlerinizin sırasını kontrol edebilirsiniz. Bu, bir ödeneği değiştirmenin tehlikeli olabilmesinin nedenidir, çünkü harcama yapanın her iki ödeneğin toplamını harcamasına izin verir.
  • uint256 türünde değerler döner. Başka bir deyişle, 0-1=2^256-1. Bu istenen davranış değilse, kontrol etmeniz (veya sizin için yapan SafeMath kütüphanesini kullanmanız) gerekir. Bunun Solidity 0.8.0(opens in a new tab) sürümünde değiştiğini unutmayın.
  • Denetimi kolaylaştırdığından, belirli bir türdeki tüm durum değişikliklerini belirli bir yerde yapın. Bu, örnek olarak approve, transferFrom, increaseAllowance ve decreaseAllowance tarafından çağrılan _approve fonksiyonuna sahip olmamızın sebebidir
  • Durum değişiklikleri, aralarında başka bir işlem 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üştüğünüz süre ile alıcının bakiyesine eklediğiniz zaman arasında olması gerekenden daha az token vardır. 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üz, kendi güvenli sözleşmelerinizi ve uygulamalarınızı yazabilirsiniz.

Son düzenleme: @nhsz(opens in a new tab), 18 Şubat 2024

Bu rehber yararlı oldu mu?