Güvenlikle İlgili Değerlendirmeler
Genellikle öngörüldüğü gibi çalışan bir yazılım oluşturmak oldukça kolay olsa da, kimsenin bu yazılımı öngörülmeyen bir şekilde kullanamayacağını kontrol etmek oldukça zordur.
Solidity’de durum daha da önemlidir çünkü akıllı sözleşmeleri tokenları ya da muhtemelen daha değerli şeyleri yönetmek için kullanabilirsiniz. Dahası, bir akıllı sözleşme her yürütüldüğünde herkese görünür bir şekilde gerçekleşir ve buna ek olarak kaynak kodu da genellikle erişilebilirdir.
Elbette her zaman ne kadar tehlikede olduğunu göz önünde bulundurmanız gerekir: Bir akıllı sözleşmeyi halka (ve dolayısıyla kötü niyetli kişilere) açık ve hatta belki de açık kaynaklı bir web hizmeti ile karşılaştırabilirsiniz. Bu web hizmetinde yalnızca alışveriş listenizi saklıyorsanız, çok fazla dikkat etmeniz gerekmeyebilir, ancak banka hesabınızı bu web hizmetini kullanarak yönetiyorsanız, daha dikkatli olmalısınız.
Bu bölüm bazı tuzakları ve genel güvenlik önerilerini listeleyecektir, ancak elbette asla eksiksiz olamaz. Ayrıca, akıllı sözleşme kodunuz hatasız olsa bile derleyicide ya da platformun kendisinde bir hata bulunabileceğini unutmayın. Derleyicinin herkes tarafından bilinen güvenlikle ilgili bazı hatalarının bir listesi, makine tarafından da okunabilen :ref: bilinen hataların listesi<known_bugs> bölümünde bulunabilir. Solidity derleyicisinin kod oluşturucusunu kapsayan bir hata ödül programı olduğunu unutmayın.
Her zaman olduğu gibi, açık kaynak belgelerinde, lütfen bu bölümü genişletmemize yardımcı olun (özellikle, bazı örneklerin hiç kimseye zararı dokunmaz)!
NOT: Aşağıdaki listeye ek olarak, Guy Lando’nun bilgi listesinde ve Consensys GitHub reposunda daha fazla güvenlik önerisi ve en iyi uygulamaları bulabilirsiniz.
Tuzaklar
Özel(Private) Bilgiler ve Rastgelelik
Bir akıllı sözleşmede kullandığınız her şey, yerel değişkenler ve private
olarak
işaretlenmiş durum değişkenleri de dahil olmak üzere herkes tarafından görülebilir.
Madencilerin hile yapabilmesini istemiyorsanız, akıllı sözleşmelerde rastgele sayılar kullanmak oldukça zordur.
Yeniden Giriş (Re-Entrancy)
Bir sözleşmeden (A) başka bir sözleşmeye (B) herhangi bir etkileşim ve herhangi bir Ether transferi, kontrolü o sözleşmeye (B) devreder. Bu, B’nin bu etkileşim tamamlanmadan önce A’yı geri çağırmasını mümkün kılar. Bir örnek vermek gerekirse, aşağıdaki kod bir hata içermektedir (bu sadece bir kod parçacığıdır ve tam bir sözleşme değildir):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// BU SÖZLEŞME BUG İÇERİR: KULLANMAYIN
contract Fund {
/// @dev Sözleşmenin ether paylarının eşleştirilmesi.
mapping(address => uint) shares;
/// Payınızı geri çekin.
function withdraw() public {
if (payable(msg.sender).send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
Burada sorun, send``in bir parçası olarak sınırlı gas miktarı nedeniyle çok ciddi
değildir, ancak yine de bir zafiyet ortaya çıkarmaktadır: Ether transferi her zaman
kod yürütmeyi içerebilir, bu nedenle alıcı ``withdraw``a geri çağıran bir sözleşme
olabilir. Bu, birden fazla geri ödeme almasına ve temelde sözleşmedeki tüm Ether'i geri
almasına izin verecektir. Özellikle, aşağıdaki sözleşme, varsayılan olarak kalan tüm
gazı ileten ``call
kullandığı için bir saldırganın birden fazla kez geri ödeme
yapmasına izin verecektir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
// BU SÖZLEŞME BUG İÇERİR: KULLANMAYIN
contract Fund {
/// @dev Sözleşmenin ether paylarının eşleştirilmesi.
mapping(address => uint) shares;
/// Payınızı geri çekin.
function withdraw() public {
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");
if (success)
shares[msg.sender] = 0;
}
}
Yeniden Giriş’den(Re-entrancy) kaçınmak için, aşağıda daha ayrıntılı olarak açıklandığı gibi Checks-Effects-Interactions kalıbını kullanabilirsiniz:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Fund {
/// @dev Sözleşmenin ether paylarının eşleştirilmesi.
mapping(address => uint) shares;
/// Payınızı geri çekin.
function withdraw() public {
uint share = shares[msg.sender];
shares[msg.sender] = 0;
payable(msg.sender).transfer(share);
}
}
Yeniden girişin yalnızca Ether aktarımının değil, başka bir sözleşmedeki herhangi bir fonksiyon çağrısının da bir etkisi olduğunu unutmayın. Ayrıca, çoklu sözleşme içeren durumları da hesaba katmanız gerekmektedir. Çağrılan bir sözleşme, bağımlı olduğunuz başka bir sözleşmenin yapısını değiştirebilir.
Gas Limiti ve Döngüler
Sabit sayıda iterasyona sahip olmayan döngüler, örneğin depolama değerine bağlı döngüler,
dikkatli bir şekilde kullanılmalıdır: Blok gas limiti nedeniyle, işlemler yalnızca belirli
bir miktarda gas tüketebilir. Ya açıkça ya da sadece normal çalışma nedeniyle, bir döngüdeki
yineleme sayısı blok gas limitinin ötesine geçebilir ve bu da tüm sözleşmenin belirli bir
noktada durmasına neden olabilir. Bu durum, yalnızca blok zincirinden veri okumak için
çalıştırılan view
fonksiyonları için geçerli olmayabilir. Yine de, bu tür fonksiyonlar
zincir üzerindeki işlemlerin bir parçası olarak diğer sözleşmeler tarafından çağrılabilir
ve bunları durdurabilir. Lütfen sözleşmelerinizin dokümantasyonunda bu tür durumlar hakkında
açıkça bilgi verin.
Ether Gönderme ve Alma
Ne sözleşmeler ne de “harici hesaplar” şu anda birinin onlara Ether göndermesini engelleyememektedir. Sözleşmeler normal bir transfere yanıt verebilir ve reddedebilir, ancak bir mesaj çağrısı oluşturmadan Ether’i taşımanın yolları vardır. Bir yol basitçe sözleşme adresine “mine to” yapmak, ikinci yol ise
selfdestruct(x)
kullanmaktır.Bir sözleşme Ether alırsa (bir fonksiyon çağrılmadan), ya receive Ether ya da fallback fonksiyonu çalıştırılır. Eğer bir receive ya da fallback fonksiyonu yoksa, Ether reddedilir (bir istisna gönderilerek). Bu fonksiyonlardan birinin yürütülmesi sırasında, sözleşme yalnızca o anda kendisine aktarılan “gas stipend “in (2300 gas) kullanılabilir olmasına güvenebilir. Ancak bu miktarı depolamayı değiştirmek için yeterli değildir (bunu kesin olarak kabul etmeyin, gelecekteki hard fork’larla miktar değişebilir). Sözleşmenizin bu şekilde Ether alabileceğinden emin olmak için, receive ve fallback fonksiyonlarının gas gereksinimlerini kontrol etmeyi unutmayın (örneğin Remix’teki “ayrıntılar” bölümünde).
Daha fazla gas’ı
addr.call{value: x}("")
kullanarak alıcı sözleşmeye iletmenin bir yolu vardır. Bu aslındaaddr.transfer(x)
ile aynıdır, sadece kalan tüm gas miktarını iletir ve alıcının daha pahalı eylemler gerçekleştirmesine olanak sağlar (ve hatayı otomatik olarak iletmek yerine bir hata kodu döndürür). Bu, gönderici sözleşmeyi geri çağırmayı veya aklınıza gelmemiş olabilecek diğer durum değişikliklerini içerebilir. Dolayısıyla güvenilir kullanıcılar için olduğu kadar kötü niyetli kullanıcılar için de büyük esneklik sağlar.Wei miktarını temsil etmek için mümkün olan en kesin birimleri kullanın, çünkü kesinlik eksikliği nedeniyle yuvarlanan her şeyi kaybedersiniz.
Eğer
address.transfer
kullanarak Ether göndermek istiyorsanız, dikkat etmeniz gereken bazı detaylar var:Alıcı bir sözleşme ise, alıcı veya fallback fonksiyonunun yürütülmesine neden olur ve bu da gönderen sözleşmeyi geri çağırabilir.
Ether gönderimi, çağrı derinliğinin 1024’ün üzerine çıkması nedeniyle başarısız olabilir. Çağrı derinliği tamamen çağıranın kontrolünde olduğundan, aktarımı başarısız olmaya zorlayabilirler; bu olasılığı göz önünde bulundurun veya
send
kullanın ve dönüş değerini her zaman kontrol ettiğinizden emin olun. Daha da iyisi, sözleşmenizi alıcının Ether çekebileceği bir model kullanarak yazın.Ether göndermek, alıcı sözleşmenin yürütülmesi için tahsis edilen gas miktarından daha fazlası gerektiği için de başarısız olabilir (açıkça require, assert, revert kullanarak veya işlem çok pahalı olduğu için) - “gas biter” (OOG). Dönüş değeri kontrolü ile
transfer
veyasend
kullanırsanız, bu, alıcının gönderim sözleşmesindeki ilerlemeyi engellemesi için bir yöntem sağlayabilir. Burada da en iyi uygulama “send” pattern yerine bir “withdraw” pattern kullanmaktır.
Çağrı Yığını Derinliği
External fonksiyon çağrıları, 1024 olan maksimum çağrı yığını boyutu sınırını aştıkları için her an başarısız olabilirler. Bu gibi durumlarda Solidity bir istisna gönderir. Kötü niyetli kişiler, sözleşmenizle etkileşime girmeden önce çağrı yığınını yüksek bir değere zorlayabilir. Tangerine Whistle <https://eips.ethereum.org/EIPS/eip-608>`_ hardfork olduğundan, 63/64 kuralı çağrı yığını derinliği saldırısını kullanışsız hale getirir. Ayrıca, her ikisinin de 1024 yığın yuvası boyut sınırına sahip olmasına rağmen, çağrı yığını ve ifade yığınının birbiriyle alakasız olduğunu unutmayın.
Eğer çağrı yığını tükenirse .send()
fonksiyonunun bir istisna göndermediğini,
bu durumda false
döndürdüğünü unutmayın. Düşük seviyeli fonksiyonlar .call()
,
.delegatecall()
ve .staticcall()
da aynı şekilde davranırlar.
tx.origin
Doğrulama için asla tx.origin kullanmayın. Diyelim ki şöyle bir cüzdan sözleşmeniz var:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// BU SÖZLEŞME BUG İÇERİR : KULLANMAYIN
contract TxUserWallet {
address owner;
constructor() {
owner = msg.sender;
}
function transferTo(address payable dest, uint amount) public {
// BUG burada, tx.origin yerine msg.sender kullanın
require(tx.origin == owner);
dest.transfer(amount);
}
}
Şimdi birisi sizi bu saldırı cüzdanının adresine Ether göndermeniz için kandırıyor:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface TxUserWallet {
function transferTo(address payable dest, uint amount) external;
}
contract TxAttackWallet {
address payable owner;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}
Cüzdanınız doğrulama için msg.sender
adresini kontrol etseydi, sahibinin adresi
yerine saldırı cüzdanının adresini alırdı. Ancak tx.origin
adresini kontrol ederek,
işlemi başlatan orijinal adresi, yani hala sahibinin adresini alır. Saldırgan cüzdan
anında tüm paranızı çeker.
Two’s Complement / Underflows / Overflows
Birçok programlama dilinde olduğu gibi, Solidity’nin integer türleri aslında tam sayı değildir. Değerler küçük olduğunda tamsayılara benzerler, ancak keyfi olarak büyük sayıları temsil edemezler.
Aşağıdaki kod bir taşmaya neden olur çünkü toplama işleminin sonucu uint8
tipinde
saklanamayacak kadar büyüktür:
uint8 x = 255;
uint8 y = 1;
return x + y;
Solidity’nin bu taşmaları ele aldığı iki modu bulunmaktadır: Kontrollü ve Kontrolsüz veya “wrapping” modu.
Varsayılan kontrollü mod, taşmaları tespit eder ve başarısız bir doğrulamaya neden olur.
Bu kontrolü unchecked { ... }
kullanarak bu kontrolü devre dışı bırakabilir ve
taşmanın sessizce göz ardı edilmesine neden olabilirsiniz. Yukarıdaki kod unchecked { … }
içine sarılmış olsaydı 0
döndürürdü. .
Kontrollü modda bile, taşma hatalarından korunduğunuzu sanmayın. Bu modda, taşmalar her zaman geri döndürülecektir. Eğer taşmadan kaçınmak mümkün değilse, bu durum akıllı sözleşmenin belirli bir durumda takılı kalmasına neden olabilir.
Genel olarak, işaretli sayılar için bazı daha özel uç durumlara sahip olan ikiye tamamlayan sayı gösteriminin sınırları hakkında bilgi edinmelisiniz.
Girdilerin boyutunu makul bir aralıkla sınırlamak için require
kullanmayı deneyin ve olası
taşmaları bulmak için SMT checker kullanın.
Mappingleri Temizleme
Yalnızca depolama amaçlı bir anahtar-değer veri yapısı olan Solidity tipi mapping
(bkz. Eşleme Türleri), sıfır olmayan bir değer atanmış anahtarların kaydını tutmaz.
Bu nedenle, yazılan anahtarlar hakkında ekstra bilgi olmadan bir mapping’i temizlemek mümkün
değildir. Bir dinamik depolama dizisinin temel türü olarak bir mapping
kullanılıyorsa,
dizinin silinmesi veya boşaltılmasının mapping
elemanları üzerinde hiçbir etkisi olmayacaktır.
Aynı durum, örneğin, bir dinamik depolama dizisinin temel türü olan bir struct``ın eleman
türünün bir ``mapping
olması durumunda da geçerlidir. Bir mapping
içeren struct veya
dizilerin atamalarında da mapping
göz ardı edilir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Map {
mapping (uint => uint)[] array;
function allocate(uint newMaps) public {
for (uint i = 0; i < newMaps; i++)
array.push();
}
function writeMap(uint map, uint key, uint value) public {
array[map][key] = value;
}
function readMap(uint map, uint key) public view returns (uint) {
return array[map][key];
}
function eraseMaps() public {
delete array;
}
}
Yukarıdaki örneği ve aşağıdaki çağrı dizisini göz önünde bulundurun: allocate(10)
,
writeMap(4, 128, 256)
. Bu noktada, readMap(4, 128)
çağrısı 256 değerini döndürür.
Eğer eraseMaps
çağrısı yaparsak, array
durum değişkeninin uzunluğu sıfırlanır,
ancak mapping
elemanları sıfırlanamadığından, bilgileri sözleşmenin deposunda canlı
kalır. Diziyi sildikten sonra, allocate(5)
çağrısı array[4]
öğesine tekrar erişmemizi
sağlar ve readMap(4, 128)
çağrısı, başka bir writeMap
çağrısı olmadan bile 256 döndürür.
Eğer mapping
bilgilerinizin silinmesi gerekiyorsa, iterable mapping <https://github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol>`_
benzeri bir kütüphane kullanmayı düşünün, bu sayede anahtarlar arasında gezinebilir ve uygun
``mapping
içindeki değerleri silebilirsiniz.
Küçük Detaylar
Tam 32 baytı kaplamayan türler “kirli yüksek dereceli bitler” içerebilir. Bu durum özellikle
msg.data
türüne eriştiğinizde önemlidir - bu bir değiştirilebilirlik riski oluşturur: Birf(uint8 x)
fonksiyonunu0xff000001
ve0x00000001
ham bayt argümanı ile çağıran işlemler oluşturabilirsiniz. Her ikisi de sözleşmeye gönderilir vex
söz konusu olduğunda her ikisi de1
sayısı gibi görünecektir, ancakmsg.data
farklı olacaktır, bu nedenle herhangi bir şey içinkeccak256(msg.data)
kullanırsanız, farklı sonuçlar elde edersiniz.
Öneriler
Uyarıları Ciddiye Alın
Derleyici sizi bir konuda uyarıyorsa, bunu değiştirmelisiniz. Bu uyarının güvenlikle ilgili olduğunu düşünmeseniz bile, altında başka bir sorun yatıyor olabilir. Verdiğimiz herhangi bir derleyici uyarısı, kodda yapılacak küçük değişikliklerle giderilebilir.
Yeni eklenen tüm uyarılardan haberdar olmak için her zaman derleyicinin en son sürümünü kullanın.
Derleyici tarafından verilen info
türündeki mesajlar tehlikeli değildir ve sadece
derleyicinin kullanıcı için yararlı olabileceğini düşündüğü ekstra önerileri ve isteğe
bağlı bilgileri temsil eder.
Ether Miktarını Kısıtlayın
Akıllı bir sözleşmede saklanabilecek Ether (veya diğer tokenler) miktarını kısıtlayın. Kaynak kodunuzda, derleyicide veya platformda bir hata varsa, bu fonlar kaybolabilir. Kaybınızı sınırlamak istiyorsanız, Ether miktarını sınırlayın.
Küçük ve Modüler Tutun
Sözleşmelerinizi küçük ve kolayca anlaşılabilir tutun. Diğer sözleşmelerdeki veya kütüphanelerdeki ilgisiz fonksiyonları ayırın. Kaynak kod kalitesiyle ilgili genel tavsiyeler elbette geçerlidir: Yerel değişkenlerin miktarını, fonksiyonların uzunluğunu ve benzerlerini sınırlayın. Başkalarının niyetinizin ne olduğunu ve kodun yapıldığından farklı olup olmadığını görebilmesi için fonksiyonlarınızı belgeleyin.
Kontroller-Etkiler-Etkileşimler Modelini Kullanın
Çoğu fonksiyon önce bazı kontroller yapacaktır (fonksiyonu kim çağırdı, argümanlar aralıkta mı, yeterince Ether gönderdiler mi, kişinin tokenleri var mı, vb.) Bu kontroller önce yapılmalıdır.
İkinci adım olarak, tüm kontroller geçerse, mevcut sözleşmenin durum değişkenlerine etkiler yapılmalıdır. Diğer sözleşmelerle etkileşim herhangi bir fonksiyonda en son adım olmalıdır.
İlk sözleşmeler bazı etkileri geciktirir ve harici fonksiyon çağrılarının hatasız bir durumda dönmesini beklerdi. Bu, yukarıda açıklanan yeniden giriş sorunu nedeniyle genellikle ciddi bir hatadır.
Ayrıca, bilinen sözleşmelere yapılan çağrıların da bilinmeyen sözleşmelere çağrı yapılmasına neden olabileceğini unutmayın, bu nedenle bu kalıbı her zaman uygulamak her zaman daha iyidir.
Arızaya Karşı Güvenli Mod Ekleyin
Sisteminizi tamamen merkeziyetsiz hale getirmek herhangi bir aracıyı ortadan kaldıracak olsa da, özellikle yeni kodlar için bir tür arıza güvenliği mekanizması eklemek iyi bir fikir olabilir:
Akıllı sözleşmenize “Herhangi bir Ether sızdı mı?”, “Tokenların toplamı sözleşmenin bakiyesine eşit mi?” gibi kendi kendine kontroller gerçekleştiren bir fonksiyon ekleyebilirsiniz. Bunun için çok fazla gaz kullanamayacağınızı unutmayın, bu nedenle zincir dışı hesaplamalar yoluyla yardım gerekebilir.
Kendi kendine kontrol başarısız olursa, sözleşme otomatik olarak bir tür “arıza emniyetli” moda geçer; örneğin, özelliklerin çoğunu devre dışı bırakır, kontrolü sabit ve güvenilir bir üçüncü tarafa devreder veya sözleşmeyi basit bir “paramı geri ver” sözleşmesine dönüştürür.
Peer İncelemesi İsteyin
Bir kod parçası ne kadar çok kişi tarafından incelenirse, o kadar çok sorun bulunur. İnsanlardan kodunuzu incelemelerini istemek, kodunuzun kolay anlaşılır olup olmadığını anlamak için bir çapraz kontrol olarak da yardımcı olur - iyi akıllı sözleşmeler için çok önemli bir kriterdir.